├── ios ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── 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-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Runner.xcworkspace │ └── contents.xcworkspacedata ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ └── project.pbxproj └── .gitignore ├── media ├── flavor-step-1.png ├── flavor-step-1b.png ├── flavor-step-2.png ├── flavor-step-3.png ├── flavor-step-4.png ├── flavor-step-4b.png ├── flavor-step-5.png ├── flavor-step-5b.png ├── how-to-use-step2.png ├── how-to-use-step4a.png ├── how-to-use-step4b.png ├── how-to-use-step5.png ├── how-to-use-step6.png ├── how-to-use-step7.png ├── how-to-use-step8.png ├── how-to-use-step9.png ├── language_control.gif └── todo-app-screenshots.png ├── assets ├── fonts │ ├── Product-Sans-Bold.ttf │ ├── Product-Sans-Italic.ttf │ ├── Product-Sans-Regular.ttf │ └── Product-Sans-Bold-Italic.ttf └── lang │ ├── zh.json │ └── en.json ├── android ├── gradle.properties ├── .gitignore ├── app │ ├── src │ │ ├── dev │ │ │ ├── res │ │ │ │ ├── values │ │ │ │ │ └── strings.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 │ │ │ └── google-services.json │ │ ├── main │ │ │ ├── res │ │ │ │ ├── values │ │ │ │ │ ├── strings.xml │ │ │ │ │ └── styles.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 │ │ │ │ └── drawable │ │ │ │ │ └── launch_background.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── venturearkstudio │ │ │ │ │ └── noteapp │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── prod │ │ │ ├── res │ │ │ │ ├── values │ │ │ │ │ └── strings.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 │ │ │ └── google-services.json │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── build.gradle ├── lib ├── constants │ ├── app_font_family.dart │ └── app_themes.dart ├── flavor.dart ├── models │ ├── user_model.dart │ └── todo_model.dart ├── ui │ ├── home │ │ └── home.dart │ ├── todo │ │ ├── empty_content.dart │ │ ├── todos_extra_actions.dart │ │ ├── create_edit_todo_screen.dart │ │ └── todos_screen.dart │ ├── splash │ │ └── splash_screen.dart │ ├── setting │ │ ├── setting_language_actions.dart │ │ └── setting_screen.dart │ └── auth │ │ ├── register_screen.dart │ │ └── sign_in_screen.dart ├── services │ ├── firestore_path.dart │ ├── firestore_service.dart │ └── firestore_database.dart ├── providers │ ├── theme_provider.dart │ ├── language_provider.dart │ └── auth_provider.dart ├── caches │ └── sharedpref │ │ └── shared_preference_helper.dart ├── routes.dart ├── main.dart ├── main_prod.dart ├── auth_widget_builder.dart ├── app_localizations.dart └── my_app.dart ├── .metadata ├── .gitignore ├── LICENSE.md ├── pubspec.yaml ├── pubspec.lock └── README.md /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /media/flavor-step-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/media/flavor-step-1.png -------------------------------------------------------------------------------- /media/flavor-step-1b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/media/flavor-step-1b.png -------------------------------------------------------------------------------- /media/flavor-step-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/media/flavor-step-2.png -------------------------------------------------------------------------------- /media/flavor-step-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/media/flavor-step-3.png -------------------------------------------------------------------------------- /media/flavor-step-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/media/flavor-step-4.png -------------------------------------------------------------------------------- /media/flavor-step-4b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/media/flavor-step-4b.png -------------------------------------------------------------------------------- /media/flavor-step-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/media/flavor-step-5.png -------------------------------------------------------------------------------- /media/flavor-step-5b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/media/flavor-step-5b.png -------------------------------------------------------------------------------- /media/how-to-use-step2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/media/how-to-use-step2.png -------------------------------------------------------------------------------- /media/how-to-use-step4a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/media/how-to-use-step4a.png -------------------------------------------------------------------------------- /media/how-to-use-step4b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/media/how-to-use-step4b.png -------------------------------------------------------------------------------- /media/how-to-use-step5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/media/how-to-use-step5.png -------------------------------------------------------------------------------- /media/how-to-use-step6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/media/how-to-use-step6.png -------------------------------------------------------------------------------- /media/how-to-use-step7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/media/how-to-use-step7.png -------------------------------------------------------------------------------- /media/how-to-use-step8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/media/how-to-use-step8.png -------------------------------------------------------------------------------- /media/how-to-use-step9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/media/how-to-use-step9.png -------------------------------------------------------------------------------- /media/language_control.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/media/language_control.gif -------------------------------------------------------------------------------- /media/todo-app-screenshots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/media/todo-app-screenshots.png -------------------------------------------------------------------------------- /assets/fonts/Product-Sans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/assets/fonts/Product-Sans-Bold.ttf -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /assets/fonts/Product-Sans-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/assets/fonts/Product-Sans-Italic.ttf -------------------------------------------------------------------------------- /assets/fonts/Product-Sans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/assets/fonts/Product-Sans-Regular.ttf -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | -------------------------------------------------------------------------------- /assets/fonts/Product-Sans-Bold-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/assets/fonts/Product-Sans-Bold-Italic.ttf -------------------------------------------------------------------------------- /android/app/src/dev/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Note-Dev 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Default App Name 4 | -------------------------------------------------------------------------------- /android/app/src/prod/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Note-Prod 4 | -------------------------------------------------------------------------------- /android/app/src/dev/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/android/app/src/dev/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/dev/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/android/app/src/dev/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/dev/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/android/app/src/dev/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/prod/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/android/app/src/prod/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/prod/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/android/app/src/prod/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /lib/constants/app_font_family.dart: -------------------------------------------------------------------------------- 1 | class AppFontFamily{ 2 | AppFontFamily._(); 3 | 4 | static String productSans = "ProductSans"; 5 | static String roboto = "Roboto"; 6 | } -------------------------------------------------------------------------------- /android/app/src/dev/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/prod/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/android/app/src/prod/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/prod/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/android/app/src/prod/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/prod/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/android/app/src/prod/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /lib/flavor.dart: -------------------------------------------------------------------------------- 1 | /* 2 | Available commands: 3 | 4 | flutter run --flavor dev -t lib/main.dart 5 | flutter run --flavor prod -t lib/main_prod.dart 6 | */ 7 | 8 | enum Flavor {dev, prod} -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/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/KenAragorn/create_flutter_provider_app/HEAD/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/KenAragorn/create_flutter_provider_app/HEAD/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/KenAragorn/create_flutter_provider_app/HEAD/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/KenAragorn/create_flutter_provider_app/HEAD/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/KenAragorn/create_flutter_provider_app/HEAD/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/KenAragorn/create_flutter_provider_app/HEAD/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/KenAragorn/create_flutter_provider_app/HEAD/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/KenAragorn/create_flutter_provider_app/HEAD/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/KenAragorn/create_flutter_provider_app/HEAD/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/KenAragorn/create_flutter_provider_app/HEAD/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/KenAragorn/create_flutter_provider_app/HEAD/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/KenAragorn/create_flutter_provider_app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KenAragorn/create_flutter_provider_app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip 7 | -------------------------------------------------------------------------------- /lib/models/user_model.dart: -------------------------------------------------------------------------------- 1 | class UserModel { 2 | String uid; 3 | String? email; 4 | String? displayName; 5 | String? phoneNumber; 6 | String? photoUrl; 7 | 8 | UserModel( 9 | {required this.uid, 10 | this.email, 11 | this.displayName, 12 | this.phoneNumber, 13 | this.photoUrl}); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /.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: 0b8abb4724aa590dd0f429683339b1e045a1594d 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /lib/ui/home/home.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:noteapp/ui/splash/splash_screen.dart'; 3 | 4 | class HomeScreen extends StatefulWidget { 5 | @override 6 | _HomeScreenState createState() => _HomeScreenState(); 7 | } 8 | 9 | class _HomeScreenState extends State { 10 | @override 11 | Widget build(BuildContext context) { 12 | return SplashScreen(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/services/firestore_path.dart: -------------------------------------------------------------------------------- 1 | /* 2 | This class defines all the possible read/write locations from the FirebaseFirestore database. 3 | In future, any new path can be added here. 4 | This class work together with FirestoreService and FirestoreDatabase. 5 | */ 6 | 7 | class FirestorePath { 8 | static String todo(String uid, String todoId) => 'users/$uid/todos/$todoId'; 9 | static String todos(String uid) => 'users/$uid/todos'; 10 | } 11 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/venturearkstudio/noteapp/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.venturearkstudio.noteapp 2 | 3 | import androidx.annotation.NonNull; 4 | import io.flutter.embedding.android.FlutterActivity 5 | import io.flutter.embedding.engine.FlutterEngine 6 | import io.flutter.plugins.GeneratedPluginRegistrant 7 | 8 | class MainActivity: FlutterActivity() { 9 | override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { 10 | GeneratedPluginRegistrant.registerWith(flutterEngine); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /lib/ui/todo/empty_content.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class EmptyContentWidget extends StatelessWidget { 4 | final String title; 5 | final String message; 6 | 7 | EmptyContentWidget( 8 | {required Key key, 9 | required this.title, 10 | required this.message}) 11 | : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Center( 16 | child: Column( 17 | mainAxisAlignment: MainAxisAlignment.center, 18 | children: [ 19 | Text(title), 20 | Text(message), 21 | ], 22 | ), 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.5.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | classpath 'com.google.gms:google-services:4.3.3' 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | } 20 | } 21 | 22 | rootProject.buildDir = '../build' 23 | subprojects { 24 | project.buildDir = "${rootProject.buildDir}/${project.name}" 25 | } 26 | subprojects { 27 | project.evaluationDependsOn(':app') 28 | } 29 | 30 | task clean(type: Delete) { 31 | delete rootProject.buildDir 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | /android/app/google-services.json 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Exceptions to above rules. 38 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 39 | -------------------------------------------------------------------------------- /lib/models/todo_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | class TodoModel { 4 | final String id; 5 | final String task; 6 | final String extraNote; 7 | final bool complete; 8 | 9 | TodoModel( 10 | {required this.id, 11 | required this.task, 12 | required this.extraNote, 13 | required this.complete}); 14 | 15 | factory TodoModel.fromMap(Map data, String documentId) { 16 | String task = data['task']; 17 | String extraNote = data['extraNote']; 18 | bool complete = data['complete']; 19 | 20 | return TodoModel( 21 | id: documentId, task: task, extraNote: extraNote, complete: complete); 22 | } 23 | 24 | Map toMap() { 25 | return { 26 | 'task': task, 27 | 'extraNote': extraNote, 28 | 'complete': complete, 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/providers/theme_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:noteapp/caches/sharedpref/shared_preference_helper.dart'; 3 | 4 | class ThemeProvider extends ChangeNotifier { 5 | // shared pref object 6 | late SharedPreferenceHelper _sharedPrefsHelper; 7 | 8 | bool _isDarkModeOn = false; 9 | 10 | ThemeProvider() { 11 | _sharedPrefsHelper = SharedPreferenceHelper(); 12 | } 13 | 14 | bool get isDarkModeOn { 15 | _sharedPrefsHelper.isDarkMode.then((statusValue) { 16 | _isDarkModeOn = statusValue; 17 | }); 18 | 19 | return _isDarkModeOn; 20 | } 21 | 22 | void updateTheme(bool isDarkModeOn) { 23 | _sharedPrefsHelper.changeTheme(isDarkModeOn); 24 | _sharedPrefsHelper.isDarkMode.then((darkModeStatus) { 25 | _isDarkModeOn = darkModeStatus; 26 | }); 27 | 28 | notifyListeners(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/providers/language_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:noteapp/caches/sharedpref/shared_preference_helper.dart'; 3 | 4 | class LanguageProvider extends ChangeNotifier { 5 | // shared pref object 6 | late SharedPreferenceHelper _sharedPrefsHelper; 7 | 8 | Locale _appLocale = Locale('en'); 9 | 10 | LanguageProvider() { 11 | _sharedPrefsHelper = SharedPreferenceHelper(); 12 | } 13 | 14 | Locale get appLocale { 15 | _sharedPrefsHelper.appLocale?.then((localeValue) { 16 | _appLocale = Locale(localeValue); 17 | }); 18 | 19 | return _appLocale; 20 | } 21 | 22 | void updateLanguage(String languageCode) { 23 | if (languageCode == "zh") { 24 | _appLocale = Locale("zh"); 25 | } else { 26 | _appLocale = Locale("en"); 27 | } 28 | 29 | _sharedPrefsHelper.changeLanguage(languageCode); 30 | notifyListeners(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Ken Lee Chiaw Huat [kenaragorn@gmail.com](mailto:kenaragorn@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. -------------------------------------------------------------------------------- /lib/caches/sharedpref/shared_preference_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | class SharedPreferenceHelper { 4 | Future? _sharedPreference; 5 | static const String is_dark_mode = "is_dark_mode"; 6 | static const String language_code = "language_code"; 7 | 8 | SharedPreferenceHelper() { 9 | _sharedPreference = SharedPreferences.getInstance(); 10 | } 11 | 12 | //Theme module 13 | Future changeTheme(bool value) { 14 | return _sharedPreference!.then((prefs) { 15 | return prefs.setBool(is_dark_mode, value); 16 | }); 17 | } 18 | 19 | Future get isDarkMode { 20 | return _sharedPreference!.then((prefs) { 21 | return prefs.getBool(is_dark_mode) ?? false; 22 | }); 23 | } 24 | 25 | //Locale module 26 | Future? get appLocale { 27 | return _sharedPreference?.then((prefs) { 28 | return prefs.getString(language_code) ?? 'en'; 29 | }); 30 | } 31 | 32 | Future changeLanguage(String value) { 33 | return _sharedPreference!.then((prefs) { 34 | return prefs.setString(language_code, value); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/routes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:noteapp/ui/auth/register_screen.dart'; 3 | import 'package:noteapp/ui/auth/sign_in_screen.dart'; 4 | import 'package:noteapp/ui/setting/setting_screen.dart'; 5 | import 'package:noteapp/ui/splash/splash_screen.dart'; 6 | import 'package:noteapp/ui/todo/create_edit_todo_screen.dart'; 7 | import 'package:noteapp/ui/todo/todos_screen.dart'; 8 | 9 | class Routes { 10 | Routes._(); //this is to prevent anyone from instantiate this object 11 | 12 | static const String splash = '/splash'; 13 | static const String login = '/login'; 14 | static const String register = '/register'; 15 | static const String home = '/home'; 16 | static const String setting = '/setting'; 17 | static const String create_edit_todo = '/create_edit_todo'; 18 | 19 | static final routes = { 20 | splash: (BuildContext context) => SplashScreen(), 21 | login: (BuildContext context) => SignInScreen(), 22 | register: (BuildContext context) => RegisterScreen(), 23 | home: (BuildContext context) => TodosScreen(), 24 | setting: (BuildContext context) => SettingScreen(), 25 | create_edit_todo: (BuildContext context) => CreateEditTodoScreen(), 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /android/app/src/dev/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "747799024060", 4 | "firebase_url": "https://note-app-dev.firebaseio.com", 5 | "project_id": "note-app-dev", 6 | "storage_bucket": "note-app-dev.appspot.com" 7 | }, 8 | "client": [ 9 | { 10 | "client_info": { 11 | "mobilesdk_app_id": "1:747799024060:android:12a44c09b32bf75c3d5bb9", 12 | "android_client_info": { 13 | "package_name": "com.example.create_flutter_provider_app.dev" 14 | } 15 | }, 16 | "oauth_client": [ 17 | { 18 | "client_id": "747799024060-0i7261mp5tipeb817cpcr7s0cn29ck0a.apps.googleusercontent.com", 19 | "client_type": 3 20 | } 21 | ], 22 | "api_key": [ 23 | { 24 | "current_key": "AIzaSyA_mDikkCUSSDXiwFKly7GlC25LJEJHsFA" 25 | } 26 | ], 27 | "services": { 28 | "appinvite_service": { 29 | "other_platform_oauth_client": [ 30 | { 31 | "client_id": "747799024060-0i7261mp5tipeb817cpcr7s0cn29ck0a.apps.googleusercontent.com", 32 | "client_type": 3 33 | } 34 | ] 35 | } 36 | } 37 | } 38 | ], 39 | "configuration_version": "1" 40 | } -------------------------------------------------------------------------------- /android/app/src/prod/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "514413640785", 4 | "firebase_url": "https://note-app-prod-aa3d9.firebaseio.com", 5 | "project_id": "note-app-prod-aa3d9", 6 | "storage_bucket": "note-app-prod-aa3d9.appspot.com" 7 | }, 8 | "client": [ 9 | { 10 | "client_info": { 11 | "mobilesdk_app_id": "1:514413640785:android:2a7848a3d2174f40098c70", 12 | "android_client_info": { 13 | "package_name": "com.example.create_flutter_provider_app.prod" 14 | } 15 | }, 16 | "oauth_client": [ 17 | { 18 | "client_id": "514413640785-pb983pp7c9kdgthvq1vqp6q3fstqnusa.apps.googleusercontent.com", 19 | "client_type": 3 20 | } 21 | ], 22 | "api_key": [ 23 | { 24 | "current_key": "AIzaSyB9uzuxTzPD9HeR_h_I7yFoiQ8Nmo7Hirs" 25 | } 26 | ], 27 | "services": { 28 | "appinvite_service": { 29 | "other_platform_oauth_client": [ 30 | { 31 | "client_id": "514413640785-pb983pp7c9kdgthvq1vqp6q3fstqnusa.apps.googleusercontent.com", 32 | "client_type": 3 33 | } 34 | ] 35 | } 36 | } 37 | } 38 | ], 39 | "configuration_version": "1" 40 | } -------------------------------------------------------------------------------- /lib/ui/splash/splash_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:noteapp/app_localizations.dart'; 4 | import 'package:noteapp/routes.dart'; 5 | 6 | class SplashScreen extends StatefulWidget { 7 | @override 8 | _SplashScreenState createState() => _SplashScreenState(); 9 | } 10 | 11 | class _SplashScreenState extends State { 12 | @override 13 | void initState() { 14 | super.initState(); 15 | startTimer(); 16 | } 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return Scaffold( 21 | body: Center( 22 | child: Column( 23 | mainAxisAlignment: MainAxisAlignment.center, 24 | crossAxisAlignment: CrossAxisAlignment.stretch, 25 | children: [ 26 | Center( 27 | child: Text( 28 | AppLocalizations.of(context).translate("splashTitle"), 29 | style: TextStyle( 30 | fontSize: Theme.of(context).textTheme.headline!.fontSize, 31 | ), 32 | )), 33 | FlutterLogo( 34 | size: 128, 35 | ), 36 | ], 37 | ))); 38 | } 39 | 40 | startTimer() { 41 | var duration = Duration(milliseconds: 3000); 42 | return Timer(duration, redirect); 43 | } 44 | 45 | redirect() async { 46 | Navigator.of(context).pushReplacementNamed(Routes.home); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/ui/todo/todos_extra_actions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:noteapp/app_localizations.dart'; 3 | import 'package:noteapp/services/firestore_database.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | enum TodosActions { toggleAllComplete, clearCompleted } 7 | 8 | class TodosExtraActions extends StatelessWidget { 9 | @override 10 | Widget build(BuildContext context) { 11 | FirestoreDatabase firestoreDatabase = Provider.of(context); 12 | 13 | return PopupMenuButton( 14 | icon: Icon(Icons.more_horiz), 15 | onSelected: (TodosActions result) { 16 | switch (result) { 17 | case TodosActions.toggleAllComplete: 18 | firestoreDatabase.setAllTodoComplete(); 19 | break; 20 | case TodosActions.clearCompleted: 21 | firestoreDatabase.deleteAllTodoWithComplete(); 22 | } 23 | }, 24 | itemBuilder: (BuildContext context) => >[ 25 | PopupMenuItem( 26 | value: TodosActions.toggleAllComplete, 27 | child: Text(AppLocalizations.of(context) 28 | .translate("todosPopUpToggleAllComplete")), 29 | ), 30 | PopupMenuItem( 31 | value: TodosActions.clearCompleted, 32 | child: Text(AppLocalizations.of(context) 33 | .translate("todosPopUpToggleClearCompleted")), 34 | ), 35 | ], 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:noteapp/flavor.dart'; 4 | import 'package:noteapp/my_app.dart'; 5 | import 'package:noteapp/providers/auth_provider.dart'; 6 | import 'package:noteapp/providers/language_provider.dart'; 7 | import 'package:noteapp/providers/theme_provider.dart'; 8 | import 'package:noteapp/services/firestore_database.dart'; 9 | import 'package:provider/provider.dart'; 10 | 11 | void main() { 12 | WidgetsFlutterBinding.ensureInitialized(); 13 | SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]) 14 | .then((_) async { 15 | runApp( 16 | /* 17 | * MultiProvider for top services that do not depends on any runtime values 18 | * such as user uid/email. 19 | */ 20 | MultiProvider( 21 | providers: [ 22 | Provider.value(value: Flavor.dev), 23 | ChangeNotifierProvider( 24 | create: (context) => ThemeProvider(), 25 | ), 26 | ChangeNotifierProvider( 27 | create: (context) => AuthProvider(), 28 | ), 29 | ChangeNotifierProvider( 30 | create: (context) => LanguageProvider(), 31 | ), 32 | ], 33 | child: MyApp( 34 | databaseBuilder: (_, uid) => FirestoreDatabase(uid: uid), 35 | key: Key('MyApp'), 36 | ), 37 | ), 38 | ); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /lib/main_prod.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:noteapp/flavor.dart'; 4 | import 'package:noteapp/my_app.dart'; 5 | import 'package:noteapp/providers/auth_provider.dart'; 6 | import 'package:noteapp/providers/language_provider.dart'; 7 | import 'package:noteapp/providers/theme_provider.dart'; 8 | import 'package:noteapp/services/firestore_database.dart'; 9 | import 'package:provider/provider.dart'; 10 | 11 | void main() { 12 | WidgetsFlutterBinding.ensureInitialized(); 13 | SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]) 14 | .then((_) async { 15 | runApp( 16 | /* 17 | * MultiProvider for top services that do not depends on any runtime values 18 | * such as user uid/email. 19 | */ 20 | MultiProvider( 21 | providers: [ 22 | Provider.value(value: Flavor.prod), 23 | ChangeNotifierProvider( 24 | create: (context) => ThemeProvider(), 25 | ), 26 | ChangeNotifierProvider( 27 | create: (context) => AuthProvider(), 28 | ), 29 | ChangeNotifierProvider( 30 | create: (context) => LanguageProvider(), 31 | ), 32 | ], 33 | child: MyApp( 34 | databaseBuilder: (_, uid) => FirestoreDatabase(uid: uid), 35 | key: Key('SimpleFinance'), 36 | ), 37 | ), 38 | ); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /lib/ui/setting/setting_language_actions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:noteapp/app_localizations.dart'; 3 | import 'package:noteapp/providers/language_provider.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | enum LanguagesActions { english, chinese } 7 | 8 | class SettingLanguageActions extends StatelessWidget { 9 | @override 10 | Widget build(BuildContext context) { 11 | LanguageProvider languageProvider = Provider.of(context); 12 | Locale _appCurrentLocale = languageProvider.appLocale; 13 | 14 | return PopupMenuButton( 15 | icon: Icon(Icons.language), 16 | onSelected: (LanguagesActions result) { 17 | switch (result) { 18 | case LanguagesActions.english: 19 | languageProvider.updateLanguage("en"); 20 | break; 21 | case LanguagesActions.chinese: 22 | languageProvider.updateLanguage("zh"); 23 | } 24 | }, 25 | itemBuilder: (BuildContext context) => >[ 26 | PopupMenuItem( 27 | value: LanguagesActions.english, 28 | enabled: _appCurrentLocale == Locale("en") ? false : true, 29 | child: Text(AppLocalizations.of(context) 30 | .translate("settingPopUpToggleEnglish")), 31 | ), 32 | PopupMenuItem( 33 | value: LanguagesActions.chinese, 34 | enabled: _appCurrentLocale == Locale("zh") ? false : true, 35 | child: Text(AppLocalizations.of(context) 36 | .translate("settingPopUpToggleChinese")), 37 | ), 38 | ], 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 12 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /assets/lang/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "splashTitle": "欢迎来到 ", 3 | "alertDialogTitle": "警报", 4 | "alertDialogMessage": "这将注销。 你确定吗?", 5 | "alertDialogCancelBtn": "取消", 6 | "alertDialogYesBtn": "是", 7 | "loginTxtEmail": "电子邮件", 8 | "loginTxtPassword": "密码", 9 | "loginBtnSignIn": "登入", 10 | "loginBtnSignUp": "注册", 11 | "loginTxtDontHaveAccount": "还没有帐号?", 12 | "loginTxtHaveAccount": "已经有帐号了?", 13 | "loginBtnLinkCreateAccount": "创建帐号", 14 | "loginBtnLinkSignIn": "登入", 15 | "loginTxtErrorEmail": "请输入电子邮件", 16 | "loginTxtErrorPassword": "请输入超过6个字符的密码", 17 | "loginTxtErrorSignIn": "无效的电子邮件和/或密码", 18 | "homeAppBarTitle": "全部", 19 | "settingAppTitle": "设置", 20 | "settingThemeListTitle": "黑暗主题", 21 | "settingThemeListSubTitle": "打开黑暗的一面", 22 | "settingLogoutListTitle": "登出", 23 | "settingLogoutListSubTitle": "从这里注销我", 24 | "settingLogoutButton": "登出", 25 | "settingLanguageListTitle": "语言", 26 | "settingLanguageListSubTitle": "设定您的偏好语言", 27 | "settingPopUpToggleEnglish": "改成英文", 28 | "settingPopUpToggleChinese": "改成中文", 29 | "todosSnackBarContent": "已删除 ", 30 | "todosSnackBarActionLbl": "撤消", 31 | "todosErrorTopMsgTxt": "出问题了", 32 | "todosErrorBottomMsgTxt": "现在无法加载数据", 33 | "todosDismissibleMsgTxt": "删除", 34 | "todosPopUpToggleAllComplete": "标记所有完成", 35 | "todosPopUpToggleClearCompleted": "清除所有已完成", 36 | "todosCreateEditAppBarTitleNewTxt": "新渡渡鸟", 37 | "todosCreateEditAppBarTitleEditTxt": "编辑一切", 38 | "todosCreateEditTaskNameTxt": "待办事项名称", 39 | "todosCreateEditNotesTxt": "笔记", 40 | "todosCreateEditTaskNameValidatorMsg": "名称不能为空", 41 | "todosCreateEditCompletedTxt": "完成了吗 ?", 42 | "todosEmptyTopMsgDefaultTxt": "这里没有什么", 43 | "todosEmptyBottomDefaultMsgTxt": "添加新项目以开始" 44 | } -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | noteapp 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /assets/lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "splashTitle": "Welcome to ", 3 | "alertDialogTitle": "Alert", 4 | "alertDialogMessage": "This will logout. Are you sure?", 5 | "alertDialogCancelBtn": "Cancel", 6 | "alertDialogYesBtn": "Yes", 7 | "loginTxtEmail": "Email", 8 | "loginTxtPassword": "Password", 9 | "loginBtnSignIn": "Sign In", 10 | "loginBtnSignUp": "Sign Up", 11 | "loginTxtDontHaveAccount": "Don't have an account?", 12 | "loginTxtHaveAccount": "Already have an account?", 13 | "loginBtnLinkCreateAccount": "Create account", 14 | "loginBtnLinkSignIn": "Sign in", 15 | "loginTxtErrorEmail": "Please enter an email", 16 | "loginTxtErrorPassword": "Please enter a password with 6+ chars long", 17 | "loginTxtErrorSignIn": "Invalid email and/or password", 18 | "homeAppBarTitle": "Todos", 19 | "settingAppTitle": "Setting", 20 | "settingThemeListTitle": "Dark theme", 21 | "settingThemeListSubTitle": "Turn On the Dark Side", 22 | "settingLogoutListTitle": "Logout", 23 | "settingLogoutListSubTitle": "Log me out from here", 24 | "settingLogoutButton": "Logout", 25 | "settingLanguageListTitle": "Language", 26 | "settingLanguageListSubTitle": "Set Your Prefer Language", 27 | "settingPopUpToggleEnglish": "Change to English", 28 | "settingPopUpToggleChinese": "Change to Chinese", 29 | "todosSnackBarContent": "Deleted ", 30 | "todosSnackBarActionLbl": "Undo", 31 | "todosErrorTopMsgTxt": "Something went wrong", 32 | "todosErrorBottomMsgTxt": "Can't load data right now", 33 | "todosDismissibleMsgTxt": "Delete", 34 | "todosPopUpToggleAllComplete": "Mark all complete", 35 | "todosPopUpToggleClearCompleted": "Clear all completed", 36 | "todosCreateEditAppBarTitleNewTxt": "New Todo", 37 | "todosCreateEditAppBarTitleEditTxt": "Edit Todo", 38 | "todosCreateEditTaskNameTxt": "Todo Name", 39 | "todosCreateEditNotesTxt": "Notes", 40 | "todosCreateEditTaskNameValidatorMsg": "Name can't be empty", 41 | "todosCreateEditCompletedTxt": "Completed ?", 42 | "todosEmptyTopMsgDefaultTxt": "Nothing here", 43 | "todosEmptyBottomDefaultMsgTxt": "Add a new item to get started" 44 | } -------------------------------------------------------------------------------- /lib/auth_widget_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:noteapp/models/user_model.dart'; 3 | import 'package:noteapp/providers/auth_provider.dart'; 4 | import 'package:noteapp/services/firestore_database.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | /* 8 | * This class is mainly to help with creating user dependent object that 9 | * need to be available by all downstream widgets. 10 | * Thus, this widget builder is a must to live above [MaterialApp]. 11 | * As we rely on uid to decide which main screen to display (eg: Home or Sign In), 12 | * this class will helps to create all providers needed that depends on 13 | * the user logged data uid. 14 | */ 15 | class AuthWidgetBuilder extends StatelessWidget { 16 | const AuthWidgetBuilder( 17 | {required Key key, required this.builder, required this.databaseBuilder}) 18 | : super(key: key); 19 | final Widget Function(BuildContext, AsyncSnapshot) builder; 20 | final FirestoreDatabase Function(BuildContext context, String uid) 21 | databaseBuilder; 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | final authService = Provider.of(context, listen: false); 26 | return StreamBuilder( 27 | stream: authService.user, 28 | builder: (BuildContext context, AsyncSnapshot snapshot) { 29 | final UserModel? user = snapshot.data; 30 | if (user != null) { 31 | /* 32 | * For any other Provider services that rely on user data can be 33 | * added to the following MultiProvider list. 34 | * Once a user has been detected, a re-build will be initiated. 35 | */ 36 | return MultiProvider( 37 | providers: [ 38 | Provider.value(value: user), 39 | Provider( 40 | create: (context) => databaseBuilder(context, user.uid), 41 | ), 42 | ], 43 | child: builder(context, snapshot), 44 | ); 45 | } 46 | return builder(context, snapshot); 47 | }, 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/app_localizations.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart'; 6 | 7 | class AppLocalizations { 8 | final Locale locale; 9 | 10 | AppLocalizations(this.locale); 11 | 12 | // Helper method to keep the code in the widgets concise 13 | // Localizations are accessed using an InheritedWidget "of" syntax 14 | static AppLocalizations of(BuildContext context) { 15 | return Localizations.of(context, AppLocalizations)!; 16 | } 17 | 18 | //This is the static member for allowing simple access to the delegate from the MaterialApp 19 | static const LocalizationsDelegate delegate = 20 | _AppLocalizationsDelegate(); 21 | 22 | Map _localizedStrings = Map(); 23 | 24 | Future load() async { 25 | // Load the language JSON file from the "lang" folder 26 | String jsonString = 27 | await rootBundle.loadString('lang/${locale.languageCode}.json'); 28 | Map jsonMap = json.decode(jsonString); 29 | 30 | _localizedStrings = jsonMap.map((key, value) { 31 | return MapEntry(key, value.toString()); 32 | }); 33 | 34 | return true; 35 | } 36 | 37 | // This method will be called from every widgets which needs a localized text 38 | String translate(String key) { 39 | return _localizedStrings[key] ?? key; 40 | } 41 | } 42 | 43 | // LocalizationsDelegate is a factory for a set of localized resources 44 | // In this case, the localized strings will be gotten in an AppLocalizations object 45 | class _AppLocalizationsDelegate 46 | extends LocalizationsDelegate { 47 | const _AppLocalizationsDelegate(); 48 | 49 | @override 50 | bool isSupported(Locale locale) { 51 | // Include all of your supported language codes here 52 | return ['en', 'zh'].contains(locale.languageCode); 53 | } 54 | 55 | @override 56 | Future load(Locale locale) async { 57 | // AppLocalizations class is where the JSON loading actually runs 58 | AppLocalizations localizations = new AppLocalizations(locale); 59 | await localizations.load(); 60 | return localizations; 61 | } 62 | 63 | @override 64 | bool shouldReload(_AppLocalizationsDelegate old) => false; 65 | } 66 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/services/firestore_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | 3 | /* 4 | This class represent all possible CRUD operation for FirebaseFirestore. 5 | It contains all generic implementation needed based on the provided document 6 | path and documentID,since most of the time in FirebaseFirestore design, we will have 7 | documentID and path for any document and collections. 8 | */ 9 | class FirestoreService { 10 | FirestoreService._(); 11 | static final instance = FirestoreService._(); 12 | 13 | Future set({ 14 | required String path, 15 | required Map data, 16 | bool merge = false, 17 | }) async { 18 | final reference = FirebaseFirestore.instance.doc(path); 19 | print('$path: $data'); 20 | await reference.set(data); 21 | } 22 | 23 | Future bulkSet({ 24 | required String path, 25 | required List> datas, 26 | bool merge = false, 27 | }) async { 28 | final reference = FirebaseFirestore.instance.doc(path); 29 | final batchSet = FirebaseFirestore.instance.batch(); 30 | 31 | // for() 32 | // batchSet. 33 | 34 | print('$path: $datas'); 35 | } 36 | 37 | Future deleteData({required String path}) async { 38 | final reference = FirebaseFirestore.instance.doc(path); 39 | print('delete: $path'); 40 | await reference.delete(); 41 | } 42 | 43 | Stream> collectionStream({ 44 | required String path, 45 | required T builder(Map data, String documentID), 46 | Query queryBuilder(Query query)?, 47 | int sort(T lhs, T rhs)?, 48 | }) { 49 | Query query = FirebaseFirestore.instance.collection(path); 50 | if (queryBuilder != null) { 51 | query = queryBuilder(query); 52 | } 53 | final Stream snapshots = query.snapshots(); 54 | return snapshots.map((snapshot) { 55 | final result = snapshot.docs 56 | .map((snapshot) => 57 | builder(snapshot.data() as Map, snapshot.id)) 58 | .where((value) => value != null) 59 | .toList(); 60 | if (sort != null) { 61 | result.sort(sort); 62 | } 63 | return result; 64 | }); 65 | } 66 | 67 | Stream documentStream({ 68 | required String path, 69 | required T builder(Map data, String documentID), 70 | }) { 71 | final DocumentReference reference = FirebaseFirestore.instance.doc(path); 72 | final Stream snapshots = reference.snapshots(); 73 | return snapshots.map((snapshot) => 74 | builder(snapshot.data() as Map, snapshot.id)); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 28 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | lintOptions { 36 | disable 'InvalidPackage' 37 | } 38 | 39 | defaultConfig { 40 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 41 | applicationId "com.example.create_flutter_provider_app"//com.example.create_flutter_provider_app 42 | minSdkVersion 16 43 | targetSdkVersion 28 44 | versionCode flutterVersionCode.toInteger() 45 | versionName flutterVersionName 46 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 47 | multiDexEnabled true 48 | } 49 | 50 | buildTypes { 51 | release { 52 | // TODO: Add your own signing config for the release build. 53 | // Signing with the debug keys for now, so `flutter run --release` works. 54 | signingConfig signingConfigs.debug 55 | } 56 | } 57 | 58 | flavorDimensions "flavor-type" 59 | 60 | productFlavors { 61 | dev { 62 | dimension "flavor-type" 63 | applicationId "com.example.create_flutter_provider_app.dev" 64 | versionCode 1 65 | versionName "1.0" 66 | } 67 | prod{ 68 | dimension "flavor-type" 69 | applicationId "com.example.create_flutter_provider_app.prod" 70 | versionCode 1 71 | versionName "1.0" 72 | } 73 | } 74 | } 75 | 76 | flutter { 77 | source '../..' 78 | } 79 | 80 | dependencies { 81 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 82 | implementation 'com.android.support:multidex:1.0.3' 83 | testImplementation 'junit:junit:4.12' 84 | androidTestImplementation 'androidx.test:runner:1.1.1' 85 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 86 | } 87 | 88 | 89 | apply plugin: 'com.google.gms.google-services' -------------------------------------------------------------------------------- /lib/services/firestore_database.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:noteapp/models/todo_model.dart'; 4 | import 'package:noteapp/services/firestore_path.dart'; 5 | import 'package:cloud_firestore/cloud_firestore.dart'; 6 | 7 | import 'package:noteapp/services/firestore_service.dart'; 8 | 9 | String documentIdFromCurrentDate() => DateTime.now().toIso8601String(); 10 | 11 | /* 12 | This is the main class access/call for any UI widgets that require to perform 13 | any CRUD activities operation in FirebaseFirestore database. 14 | This class work hand-in-hand with FirestoreService and FirestorePath. 15 | 16 | Notes: 17 | For cases where you need to have a special method such as bulk update specifically 18 | on a field, then is ok to use custom code and write it here. For example, 19 | setAllTodoComplete is require to change all todos item to have the complete status 20 | changed to true. 21 | 22 | */ 23 | class FirestoreDatabase { 24 | FirestoreDatabase({required this.uid}) : assert(uid != null); 25 | final String uid; 26 | 27 | final _firestoreService = FirestoreService.instance; 28 | 29 | //Method to create/update todoModel 30 | Future setTodo(TodoModel todo) async => await _firestoreService.set( 31 | path: FirestorePath.todo(uid, todo.id), 32 | data: todo.toMap(), 33 | ); 34 | 35 | //Method to delete todoModel entry 36 | Future deleteTodo(TodoModel todo) async { 37 | await _firestoreService.deleteData(path: FirestorePath.todo(uid, todo.id)); 38 | } 39 | 40 | //Method to retrieve todoModel object based on the given todoId 41 | Stream todoStream({required String todoId}) => 42 | _firestoreService.documentStream( 43 | path: FirestorePath.todo(uid, todoId), 44 | builder: (data, documentId) => TodoModel.fromMap(data, documentId), 45 | ); 46 | 47 | //Method to retrieve all todos item from the same user based on uid 48 | Stream> todosStream() => _firestoreService.collectionStream( 49 | path: FirestorePath.todos(uid), 50 | builder: (data, documentId) => TodoModel.fromMap(data, documentId), 51 | ); 52 | 53 | //Method to mark all todoModel to be complete 54 | Future setAllTodoComplete() async { 55 | final batchUpdate = FirebaseFirestore.instance.batch(); 56 | 57 | final querySnapshot = await FirebaseFirestore.instance 58 | .collection(FirestorePath.todos(uid)) 59 | .get(); 60 | 61 | for (DocumentSnapshot ds in querySnapshot.docs) { 62 | batchUpdate.update(ds.reference, {'complete': true}); 63 | } 64 | await batchUpdate.commit(); 65 | } 66 | 67 | Future deleteAllTodoWithComplete() async { 68 | final batchDelete = FirebaseFirestore.instance.batch(); 69 | 70 | final querySnapshot = await FirebaseFirestore.instance 71 | .collection(FirestorePath.todos(uid)) 72 | .where('complete', isEqualTo: true) 73 | .get(); 74 | 75 | for (DocumentSnapshot ds in querySnapshot.docs) { 76 | batchDelete.delete(ds.reference); 77 | } 78 | await batchDelete.commit(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: noteapp 2 | description: A new Flutter application. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.0+1 19 | 20 | environment: 21 | sdk: ">=2.12.0 <3.0.0" 22 | 23 | dependencies: 24 | flutter: 25 | sdk: flutter 26 | flutter_localizations: 27 | sdk: flutter 28 | 29 | 30 | # The following adds the Cupertino Icons font to your application. 31 | # Use with the CupertinoIcons class for iOS style icons. 32 | cupertino_icons: ^1.0.2 33 | provider: ^6.0.0 34 | shared_preferences: ^2.0.6 35 | firebase_auth: ^3.0.2 36 | cloud_firestore: ^2.5.0 37 | flutter_platform_widgets: ^1.9.5 38 | 39 | dev_dependencies: 40 | flutter_test: 41 | sdk: flutter 42 | 43 | # For information on the generic Dart part of this file, see the 44 | # following page: https://dart.dev/tools/pub/pubspec 45 | 46 | # The following section is specific to Flutter. 47 | flutter: 48 | 49 | # The following line ensures that the Material Icons font is 50 | # included with your application, so that you can use the icons in 51 | # the material Icons class. 52 | uses-material-design: true 53 | 54 | # To add assets to your application, add an assets section, like this: 55 | # assets: 56 | # - images/a_dot_burr.jpeg 57 | # - images/a_dot_ham.jpeg 58 | 59 | # An image asset can refer to one or more resolution-specific "variants", see 60 | # https://flutter.dev/assets-and-images/#resolution-aware. 61 | 62 | # For details regarding adding assets from package dependencies, see 63 | # https://flutter.dev/assets-and-images/#from-packages 64 | 65 | # To add custom fonts to your application, add a fonts section here, 66 | # in this "flutter" section. Each entry in this list should have a 67 | # "family" key with the font family name, and a "fonts" key with a 68 | # list giving the asset and other descriptors for the font. For 69 | # example: 70 | # fonts: 71 | # - family: Schyler 72 | # fonts: 73 | # - asset: fonts/Schyler-Regular.ttf 74 | # - asset: fonts/Schyler-Italic.ttf 75 | # style: italic 76 | # - family: Trajan Pro 77 | # fonts: 78 | # - asset: fonts/TrajanPro.ttf 79 | # - asset: fonts/TrajanPro_Bold.ttf 80 | # weight: 700 81 | # 82 | # For details regarding fonts from package dependencies, 83 | # see https://flutter.dev/custom-fonts/#from-packages 84 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /lib/ui/setting/setting_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; 3 | import 'package:noteapp/app_localizations.dart'; 4 | import 'package:noteapp/providers/auth_provider.dart'; 5 | import 'package:noteapp/providers/theme_provider.dart'; 6 | import 'package:noteapp/routes.dart'; 7 | import 'package:noteapp/ui/setting/setting_language_actions.dart'; 8 | import 'package:provider/provider.dart'; 9 | 10 | class SettingScreen extends StatelessWidget { 11 | @override 12 | Widget build(BuildContext context) { 13 | return Scaffold( 14 | appBar: AppBar( 15 | title: Text(AppLocalizations.of(context).translate("settingAppTitle")), 16 | ), 17 | body: _buildLayoutSection(context), 18 | ); 19 | } 20 | 21 | Widget _buildLayoutSection(BuildContext context) { 22 | return ListView( 23 | children: [ 24 | ListTile( 25 | title: Text( 26 | AppLocalizations.of(context).translate("settingThemeListTitle")), 27 | subtitle: Text(AppLocalizations.of(context) 28 | .translate("settingThemeListSubTitle")), 29 | trailing: Switch( 30 | activeColor: Theme.of(context).appBarTheme.color, 31 | activeTrackColor: Theme.of(context).textTheme.title!.color, 32 | value: Provider.of(context).isDarkModeOn, 33 | onChanged: (booleanValue) { 34 | Provider.of(context, listen: false) 35 | .updateTheme(booleanValue); 36 | }, 37 | ), 38 | ), 39 | ListTile( 40 | title: Text(AppLocalizations.of(context) 41 | .translate("settingLanguageListTitle")), 42 | subtitle: Text(AppLocalizations.of(context) 43 | .translate("settingLanguageListSubTitle")), 44 | trailing: SettingLanguageActions(), 45 | ), 46 | ListTile( 47 | title: Text( 48 | AppLocalizations.of(context).translate("settingLogoutListTitle")), 49 | subtitle: Text(AppLocalizations.of(context) 50 | .translate("settingLogoutListSubTitle")), 51 | trailing: RaisedButton( 52 | onPressed: () { 53 | _confirmSignOut(context); 54 | }, 55 | child: Text(AppLocalizations.of(context) 56 | .translate("settingLogoutButton"))), 57 | ) 58 | ], 59 | ); 60 | } 61 | 62 | _confirmSignOut(BuildContext context) { 63 | showPlatformDialog( 64 | context: context, 65 | builder: (_) => PlatformAlertDialog( 66 | material: (_, PlatformTarget target) => MaterialAlertDialogData( 67 | backgroundColor: Theme.of(context).appBarTheme.color), 68 | title: Text( 69 | AppLocalizations.of(context).translate("alertDialogTitle")), 70 | content: Text( 71 | AppLocalizations.of(context).translate("alertDialogMessage")), 72 | actions: [ 73 | PlatformDialogAction( 74 | child: PlatformText(AppLocalizations.of(context) 75 | .translate("alertDialogCancelBtn")), 76 | onPressed: () => Navigator.pop(context), 77 | ), 78 | PlatformDialogAction( 79 | child: PlatformText(AppLocalizations.of(context) 80 | .translate("alertDialogYesBtn")), 81 | onPressed: () { 82 | final authProvider = 83 | Provider.of(context, listen: false); 84 | 85 | authProvider.signOut(); 86 | 87 | Navigator.pop(context); 88 | Navigator.of(context).pushNamedAndRemoveUntil( 89 | Routes.login, ModalRoute.withName(Routes.login)); 90 | }, 91 | ) 92 | ], 93 | )); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/providers/auth_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | import 'package:noteapp/models/user_model.dart'; 4 | 5 | enum Status { 6 | Uninitialized, 7 | Authenticated, 8 | Authenticating, 9 | Unauthenticated, 10 | Registering 11 | } 12 | /* 13 | The UI will depends on the Status to decide which screen/action to be done. 14 | 15 | - Uninitialized - Checking user is logged or not, the Splash Screen will be shown 16 | - Authenticated - User is authenticated successfully, Home Page will be shown 17 | - Authenticating - Sign In button just been pressed, progress bar will be shown 18 | - Unauthenticated - User is not authenticated, login page will be shown 19 | - Registering - User just pressed registering, progress bar will be shown 20 | 21 | Take note, this is just an idea. You can remove or further add more different 22 | status for your UI or widgets to listen. 23 | */ 24 | 25 | class AuthProvider extends ChangeNotifier { 26 | //Firebase Auth object 27 | late FirebaseAuth _auth; 28 | 29 | //Default status 30 | Status _status = Status.Uninitialized; 31 | 32 | Status get status => _status; 33 | 34 | Stream get user => _auth.authStateChanges().map(_userFromFirebase); 35 | 36 | AuthProvider() { 37 | //initialise object 38 | _auth = FirebaseAuth.instance; 39 | 40 | //listener for authentication changes such as user sign in and sign out 41 | _auth.authStateChanges().listen(onAuthStateChanged); 42 | } 43 | 44 | //Create user object based on the given User 45 | UserModel _userFromFirebase(User? user) { 46 | if (user == null) { 47 | return UserModel(displayName: 'Null', uid: 'null'); 48 | } 49 | 50 | return UserModel( 51 | uid: user.uid, 52 | email: user.email, 53 | displayName: user.displayName, 54 | phoneNumber: user.phoneNumber, 55 | photoUrl: user.photoURL); 56 | } 57 | 58 | //Method to detect live auth changes such as user sign in and sign out 59 | Future onAuthStateChanged(User? firebaseUser) async { 60 | if (firebaseUser == null) { 61 | _status = Status.Unauthenticated; 62 | } else { 63 | _userFromFirebase(firebaseUser); 64 | _status = Status.Authenticated; 65 | } 66 | notifyListeners(); 67 | } 68 | 69 | //Method for new user registration using email and password 70 | Future registerWithEmailAndPassword( 71 | String email, String password) async { 72 | try { 73 | _status = Status.Registering; 74 | notifyListeners(); 75 | final UserCredential result = await _auth.createUserWithEmailAndPassword( 76 | email: email, password: password); 77 | 78 | return _userFromFirebase(result.user); 79 | } catch (e) { 80 | print("Error on the new user registration = " + e.toString()); 81 | _status = Status.Unauthenticated; 82 | notifyListeners(); 83 | return UserModel(displayName: 'Null', uid: 'null'); 84 | } 85 | } 86 | 87 | //Method to handle user sign in using email and password 88 | Future signInWithEmailAndPassword(String email, String password) async { 89 | try { 90 | _status = Status.Authenticating; 91 | notifyListeners(); 92 | await _auth.signInWithEmailAndPassword(email: email, password: password); 93 | return true; 94 | } catch (e) { 95 | print("Error on the sign in = " + e.toString()); 96 | _status = Status.Unauthenticated; 97 | notifyListeners(); 98 | return false; 99 | } 100 | } 101 | 102 | //Method to handle password reset email 103 | Future sendPasswordResetEmail(String email) async { 104 | await _auth.sendPasswordResetEmail(email: email); 105 | } 106 | 107 | //Method to handle user signing out 108 | Future signOut() async { 109 | _auth.signOut(); 110 | _status = Status.Unauthenticated; 111 | notifyListeners(); 112 | return Future.delayed(Duration.zero); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /lib/my_app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:noteapp/app_localizations.dart'; 3 | import 'package:noteapp/auth_widget_builder.dart'; 4 | import 'package:noteapp/constants/app_themes.dart'; 5 | import 'package:noteapp/flavor.dart'; 6 | import 'package:noteapp/models/user_model.dart'; 7 | import 'package:noteapp/providers/auth_provider.dart'; 8 | import 'package:noteapp/providers/language_provider.dart'; 9 | import 'package:noteapp/providers/theme_provider.dart'; 10 | import 'package:noteapp/routes.dart'; 11 | import 'package:noteapp/services/firestore_database.dart'; 12 | import 'package:noteapp/ui/auth/sign_in_screen.dart'; 13 | import 'package:noteapp/ui/home/home.dart'; 14 | import 'package:provider/provider.dart'; 15 | import 'package:flutter_localizations/flutter_localizations.dart'; 16 | 17 | class MyApp extends StatelessWidget { 18 | const MyApp({required Key key, required this.databaseBuilder}) 19 | : super(key: key); 20 | 21 | // Expose builders for 3rd party services at the root of the widget tree 22 | // This is useful when mocking services while testing 23 | final FirestoreDatabase Function(BuildContext context, String uid) 24 | databaseBuilder; 25 | 26 | // This widget is the root of your application. 27 | @override 28 | Widget build(BuildContext context) { 29 | return Consumer( 30 | builder: (_, themeProviderRef, __) { 31 | //{context, data, child} 32 | return Consumer( 33 | builder: (_, languageProviderRef, __) { 34 | return AuthWidgetBuilder( 35 | databaseBuilder: databaseBuilder, 36 | builder: (BuildContext context, 37 | AsyncSnapshot userSnapshot) { 38 | return MaterialApp( 39 | debugShowCheckedModeBanner: false, 40 | locale: languageProviderRef.appLocale, 41 | //List of all supported locales 42 | supportedLocales: [ 43 | Locale('en', 'US'), 44 | Locale('zh', 'CN'), 45 | ], 46 | //These delegates make sure that the localization data for the proper language is loaded 47 | localizationsDelegates: [ 48 | //A class which loads the translations from JSON files 49 | AppLocalizations.delegate, 50 | //Built-in localization of basic text for Material widgets (means those default Material widget such as alert dialog icon text) 51 | GlobalMaterialLocalizations.delegate, 52 | //Built-in localization for text direction LTR/RTL 53 | GlobalWidgetsLocalizations.delegate, 54 | ], 55 | //return a locale which will be used by the app 56 | localeResolutionCallback: (locale, supportedLocales) { 57 | //check if the current device locale is supported or not 58 | for (var supportedLocale in supportedLocales) { 59 | if (supportedLocale.languageCode == 60 | locale?.languageCode || 61 | supportedLocale.countryCode == locale?.countryCode) { 62 | return supportedLocale; 63 | } 64 | } 65 | //if the locale from the mobile device is not supported yet, 66 | //user the first one from the list (in our case, that will be English) 67 | return supportedLocales.first; 68 | }, 69 | title: Provider.of(context).toString(), 70 | routes: Routes.routes, 71 | theme: AppThemes.lightTheme, 72 | darkTheme: AppThemes.darkTheme, 73 | themeMode: themeProviderRef.isDarkModeOn 74 | ? ThemeMode.dark 75 | : ThemeMode.light, 76 | home: Consumer( 77 | builder: (_, authProviderRef, __) { 78 | if (userSnapshot.connectionState == 79 | ConnectionState.active) { 80 | return userSnapshot.hasData 81 | ? HomeScreen() 82 | : SignInScreen(); 83 | } 84 | 85 | return Material( 86 | child: CircularProgressIndicator(), 87 | ); 88 | }, 89 | ), 90 | ); 91 | }, 92 | key: Key('AuthWidget'), 93 | ); 94 | }, 95 | ); 96 | }, 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/ui/todo/create_edit_todo_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:noteapp/app_localizations.dart'; 3 | import 'package:noteapp/models/todo_model.dart'; 4 | import 'package:noteapp/services/firestore_database.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class CreateEditTodoScreen extends StatefulWidget { 8 | @override 9 | _CreateEditTodoScreenState createState() => _CreateEditTodoScreenState(); 10 | } 11 | 12 | class _CreateEditTodoScreenState extends State { 13 | late TextEditingController _taskController; 14 | late TextEditingController _extraNoteController; 15 | final _formKey = GlobalKey(); 16 | final _scaffoldKey = GlobalKey(); 17 | TodoModel? _todo; 18 | late bool _checkboxCompleted; 19 | 20 | @override 21 | void initState() { 22 | super.initState(); 23 | } 24 | 25 | @override 26 | void didChangeDependencies() { 27 | super.didChangeDependencies(); 28 | final TodoModel? _todoModel = ModalRoute.of(context)?.settings.arguments as TodoModel?; 29 | if (_todoModel != null) { 30 | _todo = _todoModel; 31 | } 32 | 33 | _taskController = 34 | TextEditingController(text: _todo?.task ?? ""); 35 | _extraNoteController = 36 | TextEditingController(text: _todo?.extraNote ?? ""); 37 | 38 | _checkboxCompleted = _todo?.complete ?? false; 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | return Scaffold( 44 | key: _scaffoldKey, 45 | appBar: AppBar( 46 | leading: IconButton( 47 | icon: Icon(Icons.cancel), 48 | onPressed: () { 49 | Navigator.of(context).pop(); 50 | }, 51 | ), 52 | title: Text(_todo != null 53 | ? AppLocalizations.of(context) 54 | .translate("todosCreateEditAppBarTitleEditTxt") 55 | : AppLocalizations.of(context) 56 | .translate("todosCreateEditAppBarTitleNewTxt")), 57 | actions: [ 58 | FlatButton( 59 | onPressed: () { 60 | if (_formKey.currentState!.validate()) { 61 | FocusScope.of(context).unfocus(); 62 | 63 | final firestoreDatabase = 64 | Provider.of(context, listen: false); 65 | 66 | firestoreDatabase.setTodo(TodoModel( 67 | id: _todo?.id ?? documentIdFromCurrentDate(), 68 | task: _taskController.text, 69 | extraNote: _extraNoteController.text.length > 0 70 | ? _extraNoteController.text 71 | : "", 72 | complete: _checkboxCompleted)); 73 | 74 | Navigator.of(context).pop(); 75 | } 76 | }, 77 | child: Text("Save")) 78 | ], 79 | ), 80 | body: Center( 81 | child: _buildForm(context), 82 | ), 83 | ); 84 | } 85 | 86 | @override 87 | void dispose() { 88 | _taskController.dispose(); 89 | _extraNoteController.dispose(); 90 | super.dispose(); 91 | } 92 | 93 | Widget _buildForm(BuildContext context) { 94 | return Form( 95 | key: _formKey, 96 | child: SingleChildScrollView( 97 | child: Padding( 98 | padding: const EdgeInsets.all(16), 99 | child: Column( 100 | mainAxisSize: MainAxisSize.max, 101 | crossAxisAlignment: CrossAxisAlignment.stretch, 102 | children: [ 103 | TextFormField( 104 | controller: _taskController, 105 | style: Theme.of(context).textTheme.body1, 106 | validator: (value) => value!.isEmpty 107 | ? AppLocalizations.of(context) 108 | .translate("todosCreateEditTaskNameValidatorMsg") 109 | : null, 110 | decoration: InputDecoration( 111 | enabledBorder: OutlineInputBorder( 112 | borderSide: BorderSide( 113 | color: Theme.of(context).iconTheme.color!, width: 2)), 114 | labelText: AppLocalizations.of(context) 115 | .translate("todosCreateEditTaskNameTxt"), 116 | ), 117 | ), 118 | Padding( 119 | padding: const EdgeInsets.symmetric(vertical: 16), 120 | child: TextFormField( 121 | controller: _extraNoteController, 122 | style: Theme.of(context).textTheme.body1, 123 | maxLines: 15, 124 | decoration: InputDecoration( 125 | enabledBorder: OutlineInputBorder( 126 | borderSide: BorderSide( 127 | color: Theme.of(context).iconTheme.color!, 128 | width: 2)), 129 | labelText: AppLocalizations.of(context) 130 | .translate("todosCreateEditNotesTxt"), 131 | alignLabelWithHint: true, 132 | contentPadding: new EdgeInsets.symmetric( 133 | vertical: 10.0, horizontal: 10.0), 134 | ), 135 | ), 136 | ), 137 | Padding( 138 | padding: const EdgeInsets.only(left: 8), 139 | child: Row( 140 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 141 | children: [ 142 | Text(AppLocalizations.of(context) 143 | .translate("todosCreateEditCompletedTxt")), 144 | Checkbox( 145 | value: _checkboxCompleted, 146 | onChanged: (value) { 147 | setState(() { 148 | _checkboxCompleted = value!; 149 | }); 150 | }) 151 | ], 152 | ), 153 | ) 154 | ], 155 | ), 156 | ), 157 | ), 158 | ); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /lib/constants/app_themes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:noteapp/constants/app_font_family.dart'; 3 | 4 | class AppThemes { 5 | AppThemes._(); 6 | 7 | //constants color range for light theme 8 | static const Color _lightPrimaryColor = Colors.black; 9 | static const Color _lightPrimaryVariantColor = Colors.white; 10 | static const Color _lightSecondaryColor = Colors.green; 11 | static const Color _lightOnPrimaryColor = Colors.black; 12 | static const Color _lightButtonPrimaryColor = Colors.orangeAccent; 13 | static const Color _lightAppBarColor = Colors.orangeAccent; 14 | static Color _lightIconColor = Colors.orangeAccent; 15 | static Color _lightSnackBarBackgroundErrorColor = Colors.redAccent; 16 | 17 | //text theme for light theme 18 | static final TextStyle _lightScreenHeadingTextStyle = 19 | TextStyle(fontSize: 20.0, color: _lightOnPrimaryColor); 20 | static final TextStyle _lightScreenTaskNameTextStyle = 21 | TextStyle(fontSize: 16.0, color: _lightOnPrimaryColor); 22 | static final TextStyle _lightScreenTaskDurationTextStyle = 23 | TextStyle(fontSize: 14.0, color: Colors.grey); 24 | static final TextStyle _lightScreenButtonTextStyle = TextStyle( 25 | fontSize: 14.0, color: _lightOnPrimaryColor, fontWeight: FontWeight.w500); 26 | static final TextStyle _lightScreenCaptionTextStyle = TextStyle( 27 | fontSize: 12.0, color: _lightAppBarColor, fontWeight: FontWeight.w100); 28 | 29 | static final TextTheme _lightTextTheme = TextTheme( 30 | headline: _lightScreenHeadingTextStyle, 31 | body1: _lightScreenTaskNameTextStyle, 32 | body2: _lightScreenTaskDurationTextStyle, 33 | button: _lightScreenButtonTextStyle, 34 | title: _lightScreenTaskNameTextStyle, 35 | subhead: _lightScreenTaskNameTextStyle, 36 | caption: _lightScreenCaptionTextStyle, 37 | ); 38 | 39 | //constants color range for dark theme 40 | static const Color _darkPrimaryColor = Colors.white; 41 | static const Color _darkPrimaryVariantColor = Colors.black; 42 | static const Color _darkSecondaryColor = Colors.white; 43 | static const Color _darkOnPrimaryColor = Colors.white; 44 | static const Color _darkButtonPrimaryColor = Colors.deepPurpleAccent; 45 | static const Color _darkAppBarColor = Colors.deepPurpleAccent; 46 | static Color _darkIconColor = Colors.deepPurpleAccent; 47 | static Color _darkSnackBarBackgroundErrorColor = Colors.redAccent; 48 | 49 | //text theme for dark theme 50 | static final TextStyle _darkScreenHeadingTextStyle = 51 | _lightScreenHeadingTextStyle.copyWith(color: _darkOnPrimaryColor); 52 | static final TextStyle _darkScreenTaskNameTextStyle = 53 | _lightScreenTaskNameTextStyle.copyWith(color: _darkOnPrimaryColor); 54 | static final TextStyle _darkScreenTaskDurationTextStyle = 55 | _lightScreenTaskDurationTextStyle; 56 | static final TextStyle _darkScreenButtonTextStyle = TextStyle( 57 | fontSize: 14.0, color: _darkOnPrimaryColor, fontWeight: FontWeight.w500); 58 | static final TextStyle _darkScreenCaptionTextStyle = TextStyle( 59 | fontSize: 12.0, color: _darkAppBarColor, fontWeight: FontWeight.w100); 60 | 61 | static final TextTheme _darkTextTheme = TextTheme( 62 | headline: _darkScreenHeadingTextStyle, 63 | body1: _darkScreenTaskNameTextStyle, 64 | body2: _darkScreenTaskDurationTextStyle, 65 | button: _darkScreenButtonTextStyle, 66 | title: _darkScreenTaskNameTextStyle, 67 | subhead: _darkScreenTaskNameTextStyle, 68 | caption: _darkScreenCaptionTextStyle, 69 | ); 70 | 71 | //the light theme 72 | static final ThemeData lightTheme = ThemeData( 73 | fontFamily: AppFontFamily.productSans, 74 | scaffoldBackgroundColor: _lightPrimaryVariantColor, 75 | floatingActionButtonTheme: FloatingActionButtonThemeData( 76 | backgroundColor: _lightButtonPrimaryColor, 77 | ), 78 | appBarTheme: AppBarTheme( 79 | color: _lightAppBarColor, 80 | iconTheme: IconThemeData(color: _lightOnPrimaryColor), 81 | textTheme: _lightTextTheme, 82 | ), 83 | colorScheme: ColorScheme.light( 84 | primary: _lightPrimaryColor, 85 | primaryVariant: _lightPrimaryVariantColor, 86 | secondary: _lightSecondaryColor, 87 | onPrimary: _lightOnPrimaryColor, 88 | ), 89 | snackBarTheme: 90 | SnackBarThemeData(backgroundColor: _lightSnackBarBackgroundErrorColor), 91 | iconTheme: IconThemeData( 92 | color: _lightIconColor, 93 | ), 94 | popupMenuTheme: PopupMenuThemeData(color: _lightAppBarColor), 95 | textTheme: _lightTextTheme, 96 | buttonTheme: ButtonThemeData( 97 | buttonColor: _lightButtonPrimaryColor, 98 | textTheme: ButtonTextTheme.primary), 99 | unselectedWidgetColor: _lightPrimaryColor, 100 | inputDecorationTheme: InputDecorationTheme( 101 | fillColor: _lightPrimaryColor, 102 | labelStyle: TextStyle( 103 | color: _lightPrimaryColor, 104 | )), 105 | ); 106 | 107 | //the dark theme 108 | static final ThemeData darkTheme = ThemeData( 109 | fontFamily: AppFontFamily.productSans, 110 | scaffoldBackgroundColor: _darkPrimaryVariantColor, 111 | floatingActionButtonTheme: FloatingActionButtonThemeData( 112 | backgroundColor: _darkButtonPrimaryColor, 113 | ), 114 | appBarTheme: AppBarTheme( 115 | color: _darkAppBarColor, 116 | iconTheme: IconThemeData(color: _darkOnPrimaryColor), 117 | textTheme: _darkTextTheme, 118 | ), 119 | colorScheme: ColorScheme.light( 120 | primary: _darkPrimaryColor, 121 | primaryVariant: _darkPrimaryVariantColor, 122 | secondary: _darkSecondaryColor, 123 | onPrimary: _darkOnPrimaryColor, 124 | ), 125 | snackBarTheme: 126 | SnackBarThemeData(backgroundColor: _darkSnackBarBackgroundErrorColor), 127 | iconTheme: IconThemeData( 128 | color: _darkIconColor, 129 | ), 130 | popupMenuTheme: PopupMenuThemeData(color: _darkAppBarColor), 131 | textTheme: _darkTextTheme, 132 | buttonTheme: ButtonThemeData( 133 | buttonColor: _darkButtonPrimaryColor, 134 | textTheme: ButtonTextTheme.primary), 135 | unselectedWidgetColor: _darkPrimaryColor, 136 | inputDecorationTheme: InputDecorationTheme( 137 | fillColor: _darkPrimaryColor, 138 | labelStyle: TextStyle( 139 | color: _darkPrimaryColor, 140 | )), 141 | ); 142 | } 143 | -------------------------------------------------------------------------------- /lib/ui/todo/todos_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:noteapp/app_localizations.dart'; 3 | import 'package:noteapp/models/todo_model.dart'; 4 | import 'package:noteapp/models/user_model.dart'; 5 | import 'package:noteapp/providers/auth_provider.dart'; 6 | import 'package:noteapp/routes.dart'; 7 | import 'package:noteapp/services/firestore_database.dart'; 8 | import 'package:noteapp/ui/todo/empty_content.dart'; 9 | import 'package:noteapp/ui/todo/todos_extra_actions.dart'; 10 | import 'package:provider/provider.dart'; 11 | 12 | class TodosScreen extends StatelessWidget { 13 | final _scaffoldKey = GlobalKey(); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | final authProvider = Provider.of(context); 18 | final firestoreDatabase = 19 | Provider.of(context, listen: false); 20 | 21 | return Scaffold( 22 | key: _scaffoldKey, 23 | appBar: AppBar( 24 | title: StreamBuilder( 25 | stream: authProvider.user, 26 | builder: (context, snapshot) { 27 | final UserModel? user = snapshot.data as UserModel?; 28 | return Text(user != null 29 | ? user.email! + 30 | " - " + 31 | AppLocalizations.of(context).translate("homeAppBarTitle") 32 | : AppLocalizations.of(context).translate("homeAppBarTitle")); 33 | }), 34 | actions: [ 35 | StreamBuilder( 36 | stream: firestoreDatabase.todosStream(), 37 | builder: (context, snapshot) { 38 | if (snapshot.hasData) { 39 | List todos = snapshot.data as List; 40 | return Visibility( 41 | visible: todos.isNotEmpty ? true : false, 42 | child: TodosExtraActions()); 43 | } else { 44 | return Container( 45 | width: 0, 46 | height: 0, 47 | ); 48 | } 49 | }), 50 | IconButton( 51 | icon: Icon(Icons.settings), 52 | onPressed: () { 53 | Navigator.of(context).pushNamed(Routes.setting); 54 | }), 55 | ], 56 | ), 57 | floatingActionButton: FloatingActionButton( 58 | child: Icon(Icons.add), 59 | onPressed: () { 60 | Navigator.of(context).pushNamed( 61 | Routes.create_edit_todo, 62 | ); 63 | }, 64 | ), 65 | body: WillPopScope( 66 | onWillPop: () async => false, child: _buildBodySection(context)), 67 | ); 68 | } 69 | 70 | Widget _buildBodySection(BuildContext context) { 71 | final firestoreDatabase = 72 | Provider.of(context, listen: false); 73 | 74 | return StreamBuilder( 75 | stream: firestoreDatabase.todosStream(), 76 | builder: (context, snapshot) { 77 | if (snapshot.hasData) { 78 | List todos = snapshot.data as List; 79 | if (todos.isNotEmpty) { 80 | return ListView.separated( 81 | itemCount: todos.length, 82 | itemBuilder: (context, index) { 83 | return Dismissible( 84 | background: Container( 85 | color: Colors.red, 86 | child: Center( 87 | child: Text( 88 | AppLocalizations.of(context) 89 | .translate("todosDismissibleMsgTxt"), 90 | style: TextStyle(color: Theme.of(context).canvasColor), 91 | )), 92 | ), 93 | key: Key(todos[index].id), 94 | onDismissed: (direction) { 95 | firestoreDatabase.deleteTodo(todos[index]); 96 | 97 | _scaffoldKey.currentState!.showSnackBar(SnackBar( 98 | backgroundColor: Theme.of(context).appBarTheme.color, 99 | content: Text( 100 | AppLocalizations.of(context) 101 | .translate("todosSnackBarContent") + 102 | todos[index].task, 103 | style: 104 | TextStyle(color: Theme.of(context).canvasColor), 105 | ), 106 | duration: Duration(seconds: 3), 107 | action: SnackBarAction( 108 | label: AppLocalizations.of(context) 109 | .translate("todosSnackBarActionLbl"), 110 | textColor: Theme.of(context).canvasColor, 111 | onPressed: () { 112 | firestoreDatabase.setTodo(todos[index]); 113 | }, 114 | ), 115 | )); 116 | }, 117 | child: ListTile( 118 | leading: Checkbox( 119 | value: todos[index].complete, 120 | onChanged: (value) { 121 | TodoModel todo = TodoModel( 122 | id: todos[index].id, 123 | task: todos[index].task, 124 | extraNote: todos[index].extraNote, 125 | complete: value!); 126 | firestoreDatabase.setTodo(todo); 127 | }), 128 | title: Text(todos[index].task), 129 | onTap: () { 130 | Navigator.of(context).pushNamed(Routes.create_edit_todo, 131 | arguments: todos[index]); 132 | }, 133 | ), 134 | ); 135 | }, 136 | separatorBuilder: (context, index) { 137 | return Divider(height: 0.5); 138 | }, 139 | ); 140 | } else { 141 | return EmptyContentWidget( 142 | title: AppLocalizations.of(context) 143 | .translate("todosEmptyTopMsgDefaultTxt"), 144 | message: AppLocalizations.of(context) 145 | .translate("todosEmptyBottomDefaultMsgTxt"), 146 | key: Key('EmptyContentWidget'), 147 | ); 148 | } 149 | } else if (snapshot.hasError) { 150 | return EmptyContentWidget( 151 | title: 152 | AppLocalizations.of(context).translate("todosErrorTopMsgTxt"), 153 | message: AppLocalizations.of(context) 154 | .translate("todosErrorBottomMsgTxt"), 155 | key: Key('EmptyContentWidget'), 156 | ); 157 | } 158 | return Center(child: CircularProgressIndicator()); 159 | }); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /lib/ui/auth/register_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:noteapp/app_localizations.dart'; 3 | import 'package:noteapp/models/user_model.dart'; 4 | import 'package:noteapp/providers/auth_provider.dart'; 5 | import 'package:noteapp/routes.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | class RegisterScreen extends StatefulWidget { 9 | @override 10 | _RegisterScreenState createState() => _RegisterScreenState(); 11 | } 12 | 13 | class _RegisterScreenState extends State { 14 | late TextEditingController _emailController; 15 | late TextEditingController _passwordController; 16 | final _formKey = GlobalKey(); 17 | final _scaffoldKey = GlobalKey(); 18 | 19 | @override 20 | void initState() { 21 | super.initState(); 22 | _emailController = TextEditingController(text: ""); 23 | _passwordController = TextEditingController(text: ""); 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return Scaffold( 29 | key: _scaffoldKey, 30 | body: Stack( 31 | children: [ 32 | _buildBackground(), 33 | Align( 34 | alignment: Alignment.center, 35 | child: _buildForm(context), 36 | ), 37 | ], 38 | ), 39 | ); 40 | } 41 | 42 | @override 43 | void dispose() { 44 | _emailController.dispose(); 45 | _passwordController.dispose(); 46 | super.dispose(); 47 | } 48 | 49 | Widget _buildForm(BuildContext context) { 50 | final authProvider = Provider.of(context); 51 | 52 | return Form( 53 | key: _formKey, 54 | child: SingleChildScrollView( 55 | child: Padding( 56 | padding: const EdgeInsets.symmetric(horizontal: 24), 57 | child: Column( 58 | mainAxisSize: MainAxisSize.max, 59 | crossAxisAlignment: CrossAxisAlignment.stretch, 60 | mainAxisAlignment: MainAxisAlignment.center, 61 | children: [ 62 | Padding( 63 | padding: const EdgeInsets.all(8.0), 64 | child: FlutterLogo( 65 | size: 128, 66 | ), 67 | ), 68 | TextFormField( 69 | controller: _emailController, 70 | style: Theme.of(context).textTheme.body1, 71 | validator: (value) => value!.isEmpty 72 | ? AppLocalizations.of(context) 73 | .translate("loginTxtErrorEmail") 74 | : null, 75 | decoration: InputDecoration( 76 | prefixIcon: Icon( 77 | Icons.email, 78 | color: Theme.of(context).iconTheme.color, 79 | ), 80 | labelText: AppLocalizations.of(context) 81 | .translate("loginTxtEmail"), 82 | border: OutlineInputBorder()), 83 | ), 84 | Padding( 85 | padding: const EdgeInsets.symmetric(vertical: 16), 86 | child: TextFormField( 87 | obscureText: true, 88 | maxLength: 12, 89 | controller: _passwordController, 90 | style: Theme.of(context).textTheme.body1, 91 | validator: (value) => value!.length < 6 92 | ? AppLocalizations.of(context) 93 | .translate("loginTxtErrorPassword") 94 | : null, 95 | decoration: InputDecoration( 96 | prefixIcon: Icon( 97 | Icons.lock, 98 | color: Theme.of(context).iconTheme.color, 99 | ), 100 | labelText: AppLocalizations.of(context) 101 | .translate("loginTxtPassword"), 102 | border: OutlineInputBorder()), 103 | ), 104 | ), 105 | authProvider.status == Status.Registering 106 | ? Center( 107 | child: CircularProgressIndicator(), 108 | ) 109 | : RaisedButton( 110 | child: Text( 111 | AppLocalizations.of(context) 112 | .translate("loginBtnSignUp"), 113 | style: Theme.of(context).textTheme.button, 114 | ), 115 | onPressed: () async { 116 | if (_formKey.currentState!.validate()) { 117 | FocusScope.of(context) 118 | .unfocus(); //to hide the keyboard - if any 119 | 120 | UserModel userModel = 121 | await authProvider.registerWithEmailAndPassword( 122 | _emailController.text, 123 | _passwordController.text); 124 | 125 | if (userModel == null) { 126 | _scaffoldKey.currentState!.showSnackBar(SnackBar( 127 | content: Text(AppLocalizations.of(context) 128 | .translate("loginTxtErrorSignIn")), 129 | )); 130 | } 131 | } 132 | }), 133 | authProvider.status == Status.Registering 134 | ? Center( 135 | child: null, 136 | ) 137 | : Padding( 138 | padding: const EdgeInsets.only(top: 48), 139 | child: Center( 140 | child: Text( 141 | AppLocalizations.of(context) 142 | .translate("loginTxtHaveAccount"), 143 | style: Theme.of(context).textTheme.button, 144 | )), 145 | ), 146 | authProvider.status == Status.Registering 147 | ? Center( 148 | child: null, 149 | ) 150 | : FlatButton( 151 | child: Text(AppLocalizations.of(context) 152 | .translate("loginBtnLinkSignIn")), 153 | textColor: Theme.of(context).iconTheme.color, 154 | onPressed: () { 155 | Navigator.of(context) 156 | .pushReplacementNamed(Routes.login); 157 | }, 158 | ), 159 | ], 160 | ), 161 | ), 162 | )); 163 | } 164 | 165 | Widget _buildBackground() { 166 | return ClipPath( 167 | clipper: SignInCustomClipper(), 168 | child: Container( 169 | width: MediaQuery.of(context).size.width, 170 | height: MediaQuery.of(context).size.height * 0.5, 171 | color: Theme.of(context).iconTheme.color, 172 | ), 173 | ); 174 | } 175 | } 176 | 177 | class SignInCustomClipper extends CustomClipper { 178 | @override 179 | Path getClip(Size size) { 180 | Path path = Path(); 181 | path.lineTo(0, size.height); 182 | 183 | var firstEndPoint = Offset(size.width / 2, size.height - 95); 184 | var firstControlPoint = Offset(size.width / 6, size.height * 0.45); 185 | 186 | path.quadraticBezierTo(firstControlPoint.dx, firstControlPoint.dy, 187 | firstEndPoint.dx, firstEndPoint.dy); 188 | 189 | var secondEndPoint = Offset(size.width, size.height / 2 - 50); 190 | var secondControlPoint = Offset(size.width, size.height + 15); 191 | 192 | path.quadraticBezierTo(secondControlPoint.dx, secondControlPoint.dy, 193 | secondEndPoint.dx, secondEndPoint.dy); 194 | 195 | path.lineTo(size.width, size.height / 2); 196 | path.lineTo(size.width, 0); 197 | path.close(); 198 | return path; 199 | } 200 | 201 | @override 202 | bool shouldReclip(CustomClipper oldClipper) { 203 | return true; 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /lib/ui/auth/sign_in_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:noteapp/app_localizations.dart'; 3 | import 'package:noteapp/flavor.dart'; 4 | import 'package:noteapp/providers/auth_provider.dart'; 5 | import 'package:noteapp/routes.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | class SignInScreen extends StatefulWidget { 9 | @override 10 | _SignInScreenState createState() => _SignInScreenState(); 11 | } 12 | 13 | class _SignInScreenState extends State { 14 | late TextEditingController _emailController; 15 | late TextEditingController _passwordController; 16 | final _formKey = GlobalKey(); 17 | final _scaffoldKey = GlobalKey(); 18 | 19 | @override 20 | void initState() { 21 | super.initState(); 22 | _emailController = TextEditingController(text: ""); 23 | _passwordController = TextEditingController(text: ""); 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return Scaffold( 29 | key: _scaffoldKey, 30 | body: Stack( 31 | children: [ 32 | _buildBackground(), 33 | Align( 34 | alignment: Alignment.center, 35 | child: _buildForm(context), 36 | ), 37 | ], 38 | ), 39 | ); 40 | } 41 | 42 | @override 43 | void dispose() { 44 | _emailController.dispose(); 45 | _passwordController.dispose(); 46 | super.dispose(); 47 | } 48 | 49 | Widget _buildForm(BuildContext context) { 50 | final authProvider = Provider.of(context); 51 | 52 | return Form( 53 | key: _formKey, 54 | child: SingleChildScrollView( 55 | child: Padding( 56 | padding: const EdgeInsets.symmetric(horizontal: 24), 57 | child: Column( 58 | mainAxisSize: MainAxisSize.max, 59 | crossAxisAlignment: CrossAxisAlignment.stretch, 60 | mainAxisAlignment: MainAxisAlignment.center, 61 | children: [ 62 | Padding( 63 | padding: const EdgeInsets.all(8.0), 64 | child: FlutterLogo( 65 | size: 128, 66 | ), 67 | ), 68 | TextFormField( 69 | controller: _emailController, 70 | style: Theme.of(context).textTheme.body1, 71 | validator: (value) => value!.isEmpty 72 | ? AppLocalizations.of(context) 73 | .translate("loginTxtErrorEmail") 74 | : null, 75 | decoration: InputDecoration( 76 | prefixIcon: Icon( 77 | Icons.email, 78 | color: Theme.of(context).iconTheme.color, 79 | ), 80 | labelText: AppLocalizations.of(context) 81 | .translate("loginTxtEmail"), 82 | border: OutlineInputBorder()), 83 | ), 84 | Padding( 85 | padding: const EdgeInsets.symmetric(vertical: 16), 86 | child: TextFormField( 87 | obscureText: true, 88 | maxLength: 12, 89 | controller: _passwordController, 90 | style: Theme.of(context).textTheme.body1, 91 | validator: (value) => value!.length < 6 92 | ? AppLocalizations.of(context) 93 | .translate("loginTxtErrorPassword") 94 | : null, 95 | decoration: InputDecoration( 96 | prefixIcon: Icon( 97 | Icons.lock, 98 | color: Theme.of(context).iconTheme.color, 99 | ), 100 | labelText: AppLocalizations.of(context) 101 | .translate("loginTxtPassword"), 102 | border: OutlineInputBorder()), 103 | ), 104 | ), 105 | authProvider.status == Status.Authenticating 106 | ? Center( 107 | child: CircularProgressIndicator(), 108 | ) 109 | : RaisedButton( 110 | child: Text( 111 | AppLocalizations.of(context) 112 | .translate("loginBtnSignIn"), 113 | style: Theme.of(context).textTheme.button, 114 | ), 115 | onPressed: () async { 116 | if (_formKey.currentState!.validate()) { 117 | FocusScope.of(context) 118 | .unfocus(); //to hide the keyboard - if any 119 | 120 | bool status = 121 | await authProvider.signInWithEmailAndPassword( 122 | _emailController.text, 123 | _passwordController.text); 124 | 125 | if (!status) { 126 | _scaffoldKey.currentState!.showSnackBar(SnackBar( 127 | content: Text(AppLocalizations.of(context) 128 | .translate("loginTxtErrorSignIn")), 129 | )); 130 | } else { 131 | Navigator.of(context) 132 | .pushReplacementNamed(Routes.home); 133 | } 134 | } 135 | }), 136 | authProvider.status == Status.Authenticating 137 | ? Center( 138 | child: null, 139 | ) 140 | : Padding( 141 | padding: const EdgeInsets.only(top: 48), 142 | child: Center( 143 | child: Text( 144 | AppLocalizations.of(context) 145 | .translate("loginTxtDontHaveAccount"), 146 | style: Theme.of(context).textTheme.button, 147 | )), 148 | ), 149 | authProvider.status == Status.Authenticating 150 | ? Center( 151 | child: null, 152 | ) 153 | : FlatButton( 154 | child: Text(AppLocalizations.of(context) 155 | .translate("loginBtnLinkCreateAccount")), 156 | textColor: Theme.of(context).iconTheme.color, 157 | onPressed: () { 158 | Navigator.of(context) 159 | .pushReplacementNamed(Routes.register); 160 | }, 161 | ), 162 | Center( 163 | child: Column( 164 | mainAxisAlignment: MainAxisAlignment.end, 165 | children: [ 166 | SizedBox( 167 | height: 70, 168 | ), 169 | Text( 170 | Provider.of(context).toString(), 171 | style: Theme.of(context).textTheme.body2, 172 | ), 173 | ], 174 | )), 175 | ], 176 | ), 177 | ), 178 | )); 179 | } 180 | 181 | Widget _buildBackground() { 182 | return ClipPath( 183 | clipper: SignInCustomClipper(), 184 | child: Container( 185 | width: MediaQuery.of(context).size.width, 186 | height: MediaQuery.of(context).size.height * 0.5, 187 | color: Theme.of(context).iconTheme.color, 188 | ), 189 | ); 190 | } 191 | } 192 | 193 | class SignInCustomClipper extends CustomClipper { 194 | @override 195 | Path getClip(Size size) { 196 | Path path = Path(); 197 | path.lineTo(0, size.height); 198 | 199 | var firstEndPoint = Offset(size.width / 2, size.height - 95); 200 | var firstControlPoint = Offset(size.width / 6, size.height * 0.45); 201 | 202 | path.quadraticBezierTo(firstControlPoint.dx, firstControlPoint.dy, 203 | firstEndPoint.dx, firstEndPoint.dy); 204 | 205 | var secondEndPoint = Offset(size.width, size.height / 2 - 50); 206 | var secondControlPoint = Offset(size.width, size.height + 15); 207 | 208 | path.quadraticBezierTo(secondControlPoint.dx, secondControlPoint.dy, 209 | secondEndPoint.dx, secondEndPoint.dy); 210 | 211 | path.lineTo(size.width, size.height / 2); 212 | path.lineTo(size.width, 0); 213 | path.close(); 214 | return path; 215 | } 216 | 217 | @override 218 | bool shouldReclip(CustomClipper oldClipper) { 219 | return true; 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.6.1" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "2.1.0" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.1.0" 25 | charcode: 26 | dependency: transitive 27 | description: 28 | name: charcode 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.2.0" 32 | clock: 33 | dependency: transitive 34 | description: 35 | name: clock 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.1.0" 39 | cloud_firestore: 40 | dependency: "direct main" 41 | description: 42 | name: cloud_firestore 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "2.5.0" 46 | cloud_firestore_platform_interface: 47 | dependency: transitive 48 | description: 49 | name: cloud_firestore_platform_interface 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "5.4.0" 53 | cloud_firestore_web: 54 | dependency: transitive 55 | description: 56 | name: cloud_firestore_web 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "2.4.0" 60 | collection: 61 | dependency: transitive 62 | description: 63 | name: collection 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "1.15.0" 67 | cupertino_icons: 68 | dependency: "direct main" 69 | description: 70 | name: cupertino_icons 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "1.0.3" 74 | fake_async: 75 | dependency: transitive 76 | description: 77 | name: fake_async 78 | url: "https://pub.dartlang.org" 79 | source: hosted 80 | version: "1.2.0" 81 | ffi: 82 | dependency: transitive 83 | description: 84 | name: ffi 85 | url: "https://pub.dartlang.org" 86 | source: hosted 87 | version: "1.1.2" 88 | file: 89 | dependency: transitive 90 | description: 91 | name: file 92 | url: "https://pub.dartlang.org" 93 | source: hosted 94 | version: "6.1.2" 95 | firebase_auth: 96 | dependency: "direct main" 97 | description: 98 | name: firebase_auth 99 | url: "https://pub.dartlang.org" 100 | source: hosted 101 | version: "3.0.2" 102 | firebase_auth_platform_interface: 103 | dependency: transitive 104 | description: 105 | name: firebase_auth_platform_interface 106 | url: "https://pub.dartlang.org" 107 | source: hosted 108 | version: "6.0.1" 109 | firebase_auth_web: 110 | dependency: transitive 111 | description: 112 | name: firebase_auth_web 113 | url: "https://pub.dartlang.org" 114 | source: hosted 115 | version: "3.0.1" 116 | firebase_core: 117 | dependency: transitive 118 | description: 119 | name: firebase_core 120 | url: "https://pub.dartlang.org" 121 | source: hosted 122 | version: "1.5.0" 123 | firebase_core_platform_interface: 124 | dependency: transitive 125 | description: 126 | name: firebase_core_platform_interface 127 | url: "https://pub.dartlang.org" 128 | source: hosted 129 | version: "4.0.1" 130 | firebase_core_web: 131 | dependency: transitive 132 | description: 133 | name: firebase_core_web 134 | url: "https://pub.dartlang.org" 135 | source: hosted 136 | version: "1.1.0" 137 | flutter: 138 | dependency: "direct main" 139 | description: flutter 140 | source: sdk 141 | version: "0.0.0" 142 | flutter_localizations: 143 | dependency: "direct main" 144 | description: flutter 145 | source: sdk 146 | version: "0.0.0" 147 | flutter_platform_widgets: 148 | dependency: "direct main" 149 | description: 150 | name: flutter_platform_widgets 151 | url: "https://pub.dartlang.org" 152 | source: hosted 153 | version: "1.9.5" 154 | flutter_test: 155 | dependency: "direct dev" 156 | description: flutter 157 | source: sdk 158 | version: "0.0.0" 159 | flutter_web_plugins: 160 | dependency: transitive 161 | description: flutter 162 | source: sdk 163 | version: "0.0.0" 164 | http_parser: 165 | dependency: transitive 166 | description: 167 | name: http_parser 168 | url: "https://pub.dartlang.org" 169 | source: hosted 170 | version: "4.0.0" 171 | intl: 172 | dependency: transitive 173 | description: 174 | name: intl 175 | url: "https://pub.dartlang.org" 176 | source: hosted 177 | version: "0.17.0" 178 | js: 179 | dependency: transitive 180 | description: 181 | name: js 182 | url: "https://pub.dartlang.org" 183 | source: hosted 184 | version: "0.6.3" 185 | matcher: 186 | dependency: transitive 187 | description: 188 | name: matcher 189 | url: "https://pub.dartlang.org" 190 | source: hosted 191 | version: "0.12.10" 192 | meta: 193 | dependency: transitive 194 | description: 195 | name: meta 196 | url: "https://pub.dartlang.org" 197 | source: hosted 198 | version: "1.3.0" 199 | nested: 200 | dependency: transitive 201 | description: 202 | name: nested 203 | url: "https://pub.dartlang.org" 204 | source: hosted 205 | version: "1.0.0" 206 | path: 207 | dependency: transitive 208 | description: 209 | name: path 210 | url: "https://pub.dartlang.org" 211 | source: hosted 212 | version: "1.8.0" 213 | path_provider_linux: 214 | dependency: transitive 215 | description: 216 | name: path_provider_linux 217 | url: "https://pub.dartlang.org" 218 | source: hosted 219 | version: "2.0.2" 220 | path_provider_platform_interface: 221 | dependency: transitive 222 | description: 223 | name: path_provider_platform_interface 224 | url: "https://pub.dartlang.org" 225 | source: hosted 226 | version: "2.0.1" 227 | path_provider_windows: 228 | dependency: transitive 229 | description: 230 | name: path_provider_windows 231 | url: "https://pub.dartlang.org" 232 | source: hosted 233 | version: "2.0.3" 234 | platform: 235 | dependency: transitive 236 | description: 237 | name: platform 238 | url: "https://pub.dartlang.org" 239 | source: hosted 240 | version: "3.0.2" 241 | plugin_platform_interface: 242 | dependency: transitive 243 | description: 244 | name: plugin_platform_interface 245 | url: "https://pub.dartlang.org" 246 | source: hosted 247 | version: "2.0.1" 248 | process: 249 | dependency: transitive 250 | description: 251 | name: process 252 | url: "https://pub.dartlang.org" 253 | source: hosted 254 | version: "4.2.3" 255 | provider: 256 | dependency: "direct main" 257 | description: 258 | name: provider 259 | url: "https://pub.dartlang.org" 260 | source: hosted 261 | version: "6.0.0" 262 | shared_preferences: 263 | dependency: "direct main" 264 | description: 265 | name: shared_preferences 266 | url: "https://pub.dartlang.org" 267 | source: hosted 268 | version: "2.0.6" 269 | shared_preferences_linux: 270 | dependency: transitive 271 | description: 272 | name: shared_preferences_linux 273 | url: "https://pub.dartlang.org" 274 | source: hosted 275 | version: "2.0.2" 276 | shared_preferences_macos: 277 | dependency: transitive 278 | description: 279 | name: shared_preferences_macos 280 | url: "https://pub.dartlang.org" 281 | source: hosted 282 | version: "2.0.2" 283 | shared_preferences_platform_interface: 284 | dependency: transitive 285 | description: 286 | name: shared_preferences_platform_interface 287 | url: "https://pub.dartlang.org" 288 | source: hosted 289 | version: "2.0.0" 290 | shared_preferences_web: 291 | dependency: transitive 292 | description: 293 | name: shared_preferences_web 294 | url: "https://pub.dartlang.org" 295 | source: hosted 296 | version: "2.0.2" 297 | shared_preferences_windows: 298 | dependency: transitive 299 | description: 300 | name: shared_preferences_windows 301 | url: "https://pub.dartlang.org" 302 | source: hosted 303 | version: "2.0.2" 304 | sky_engine: 305 | dependency: transitive 306 | description: flutter 307 | source: sdk 308 | version: "0.0.99" 309 | source_span: 310 | dependency: transitive 311 | description: 312 | name: source_span 313 | url: "https://pub.dartlang.org" 314 | source: hosted 315 | version: "1.8.1" 316 | stack_trace: 317 | dependency: transitive 318 | description: 319 | name: stack_trace 320 | url: "https://pub.dartlang.org" 321 | source: hosted 322 | version: "1.10.0" 323 | stream_channel: 324 | dependency: transitive 325 | description: 326 | name: stream_channel 327 | url: "https://pub.dartlang.org" 328 | source: hosted 329 | version: "2.1.0" 330 | string_scanner: 331 | dependency: transitive 332 | description: 333 | name: string_scanner 334 | url: "https://pub.dartlang.org" 335 | source: hosted 336 | version: "1.1.0" 337 | term_glyph: 338 | dependency: transitive 339 | description: 340 | name: term_glyph 341 | url: "https://pub.dartlang.org" 342 | source: hosted 343 | version: "1.2.0" 344 | test_api: 345 | dependency: transitive 346 | description: 347 | name: test_api 348 | url: "https://pub.dartlang.org" 349 | source: hosted 350 | version: "0.3.0" 351 | typed_data: 352 | dependency: transitive 353 | description: 354 | name: typed_data 355 | url: "https://pub.dartlang.org" 356 | source: hosted 357 | version: "1.3.0" 358 | vector_math: 359 | dependency: transitive 360 | description: 361 | name: vector_math 362 | url: "https://pub.dartlang.org" 363 | source: hosted 364 | version: "2.1.0" 365 | win32: 366 | dependency: transitive 367 | description: 368 | name: win32 369 | url: "https://pub.dartlang.org" 370 | source: hosted 371 | version: "2.2.5" 372 | xdg_directories: 373 | dependency: transitive 374 | description: 375 | name: xdg_directories 376 | url: "https://pub.dartlang.org" 377 | source: hosted 378 | version: "0.2.0" 379 | sdks: 380 | dart: ">=2.13.0 <3.0.0" 381 | flutter: ">=2.0.0" 382 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter Boilerplate Project using Provider with Firebase Authentication and Firestore 2 | 3 | This is the **boilerplate project** for anyone that want to create **Flutter mobile application using Provider, Shared Preferences and Firebase**. 4 | 5 | 6 | ## Motivation 7 | 8 | The project contains the minimal implementation required to create a new Flutter project using Provider for state management and Firebase as the backend connectivity for user authorisation control and Firestore for database. 9 | 10 | The project come with preloaded folder structure recommended for Provider and separation of business logic, user interface and services needed for any Flutter Firebase project. Apart from that, it come with commonly used files and widgets such as app theme, constants and common Firebase code. 11 | 12 | Instead of spending time in every new project to setup development structure, copy and paste, this will help in reducing the common code practise and structure, and quickly allow us to focus on the core part of the app, avoiding writing again and again the same code from scratch. 13 | 14 | 15 | ## Project Goals 16 | 17 | The main goal is to define a reference architecture that can be used as the foundation for Flutter app that require the usage of Firebase and eventually: 18 | 19 | - To reduce developer time in re-writing common code and project structure from scratch 20 | - To have a common folder and package structure that can be further extended should it require 21 | - Clearing define the app layer – UI layer, business logic layer and service layer 22 | 23 | The 3 main layers: 24 | - UI layer – all UI widgets files 25 | - Business logic layer – contains all app business and presentation logic 26 | - Service layer – contains services needed to interact with between the app and 3rd party services such as APIs 27 | 28 | This project bundled with a demo app as a practical example of this architecture. 29 | 30 | 31 | ## Demo App: Todo App 32 | 33 | ![](media/todo-app-screenshots.png) 34 | 35 | The demo app demonstrates a simple but comprehensive enough to capture the various challenges and use cases of state management across multiple screens and different modules. 36 | 37 | After signed into the app, users can view, create, edit and delete their todo notes. User can change the theme and data is stored in Firebase Firestore. 38 | 39 | 40 | ## How to Use 41 | 42 | ### Step 1: 43 | Download or clone this repo by using the following link: 44 | [https://github.com/KenAragorn/create_flutter_provider_app.git](https://github.com/KenAragorn/create_flutter_provider_app.git) 45 | 46 | ### Step 2: 47 | Go to your local extracted clone copy of the project, open the android/build.gradle file and change the applicationId to your own application ID. For example, com.your_com_name.project_name. 48 | ![](media/how-to-use-step2.png) 49 | 50 | ### Step 3: 51 | Go to the project root and execute the following command to get all dependencies packages: 52 | ```flutter pub get``` 53 | 54 | ### Step 4: 55 | Go to your [Firebase](https://console.firebase.google.com/) console. Create a new Firebase project. Once created without any issue, register your app by clicking the icon – for Android, click on the Android icon and for iOS, click on the iOS icon. Complete the rest of the step require. 56 | ![](media/how-to-use-step4a.png) 57 | 58 | ![](media/how-to-use-step4b.png) 59 | 60 | ### Step 5: 61 | Ensure the Android package name is the same value as your application ID setup in step 2. 62 | ![](media/how-to-use-step5.png) 63 | 64 | ### Step 6: 65 | Download the google-services.json file provided by Firebase to the android/app directory. Then, update the Gradle build files under android/build.gradle to register the app with Google services. 66 | ![](media/how-to-use-step6.png) 67 | 68 | ### Step 7: 69 | Update the android/app/build.gradle file by adding the following highlighted code: 70 | ![](media/how-to-use-step7.png) 71 | 72 | ### Step 8: 73 | Go back to your Firebase console to create the database Cloud Firestore. Choose the test mode option. You can change it later, just make sure to choose the nearer Cloud Firestore location nearer to your location. 74 | ![](media/how-to-use-step8.png) 75 | 76 | ### Step 9: 77 | Still within Fiebase console, go to Authentication and turn on the sign-in method. In our case, that would be email/password have to turn on by setting the status to enabled. Other sign-in method will be added soon. 78 | ![](media/how-to-use-step9.png) 79 | 80 | ### Step 10: 81 | Done. Try launch the app using emulator or your preferred physical test device. For first time user, you need to create a new account from the app. 82 | 83 | 84 | ## Third Party Libraries Dependencies: 85 | 86 | * [Provider](https://github.com/rrousselGit/provider) 87 | * [Firebase Auth](https://pub.dev/packages/firebase_auth) 88 | * [Cloud Firestore](https://pub.dev/packages/cloud_firestore) 89 | * [Shared Preferences](https://pub.dev/packages/shared_preferences) 90 | * [Flutter Platform Widgets](https://pub.dev/packages/flutter_platform_widgets) 91 | * [Flutter Localizations](https://flutter.dev/docs/development/accessibility-and-localization/internationalization) 92 | 93 | ## Project Key Features: 94 | 95 | * Splash Screen 96 | * New User Registration & Login - Firebase Auth - Email/Password only (more will be added soon) 97 | * Routing – Separation of routing files for ease of maintenance 98 | * Theme – Separation of theme files that support dark and light theme 99 | * Provider – State management 100 | * Caching – Using SharedPreferences to keep track theme selection 101 | * Internationalization – **Newly added - 11 April 2020** 102 | * Multiple Build Environment – Using multiple Flavors to connect 2 different Firebase environment - Dev and Production **Newly added - 23 April 2020** 103 | 104 | ## Folder Structure: 105 | ``` 106 | lib/ 107 | |- constants/ – contains all application level constants for theme, font types and app strings. 108 | |- caches/ – contains data layer such as shared preferences. 109 | |- models/ – contains all the plain data models. 110 | |- services/ – contains all services files that handle the API calling for CRUD related functionalities. 111 | |- providers/ – contains all Provider models for each of the widget views. 112 | |- ui/ the main folder that will contains all UI related breaking down further by different modules (such as authentication, home, etc) and sub section (reusable widgets and screens). 113 | |- routes.dart 114 | |- app_localizations.dart 115 | |- auth_widget_builder.dart 116 | |- flavor.dart - main class for handling the multiple build - dev and production 117 | |- my_app.dart 118 | |- main.dart - the main.dart file for dev environment (means, connecting to Firebase Cloud Database Dev) 119 | |- main_prod.dart - the main.dart for production environment (means, connecting to Firebase Cloud Database Production) 120 | ``` 121 | 122 | ### What inside constants? 123 | Basically, all needed constant files for the application to work will be here. 124 | ``` 125 | constants/ 126 | |- app_themes.dart – the theme file for the app 127 | |- app_font_family.dart – the app global supported font family 128 | ``` 129 | 130 | ### What inside cache? 131 | As of now, only shared preferences files. In future we may have others such as sqlite features and it is good to keep things separated. 132 | ``` 133 | caches/ 134 | |- sharedpref/shared_preference_helper.dart 135 | ``` 136 | 137 | ### What inside models? 138 | All plain data models file will be here. Depending on the number of the model files, if it is more than 10 files, we suggest separating the files by sub folder such as models/auth, models/todo etc. Else, keep it simple with every data model file is within this folder. 139 | ``` 140 | models/ 141 | |- user_model.dart 142 | |- todo_model.dart 143 | ``` 144 | 145 | ### What inside services? 146 | As we have backend firestore and firebase for application data such as user and todo data, we will need a dart classes that represent the CRUD services to handle any communication between the UI code and the backend. 147 | ``` 148 | services/ 149 | |- firestore_database.dart 150 | |- firestore_service.dart 151 | |- firestore_path.dart 152 | ``` 153 | 154 | ### What inside providers? 155 | Contains all provider models needed for the app. The idea is like those in models and services in which, different provider class will be created for different key modules. 156 | ``` 157 | providers/ 158 | |- auth_provider.dart 159 | |- theme_provider.dart 160 | ``` 161 | 162 | ### What inside UI? 163 | This directory contains all the UI files for the application. As most of the times, there will be more than 1 files representing a screen, thus is always good to separate it by sub-folder and grouping it based on their key features or modules. 164 | ``` 165 | ui/ 166 | |- auth 167 | |- register_screen.dart 168 | |- sign_in_screen.dart 169 | |- splash 170 | |- splash_screen.dart 171 | |- home 172 | |- home.dart 173 | |- todo 174 | |- todos_screen.dart 175 | |- create_edit_todo_screen.dart 176 | ``` 177 | 178 | ## Use Case: Firestore Service 179 | Any widgets module can subsribe to updates from Firebase Firestore through streams, as well as write operation such as creation and deletion of data using Future-based APIs. 180 | 181 | For convenient, all Firestore related CRUD services for the demo app is all in 1 place: 182 | ```dart 183 | class FirestoreDatabase { // implementation omitted for brevity 184 | Future setTodo(TodoModel todo); // create / update 185 | Future deleteTodo(TodoModel todo); // delete 186 | Stream> todosStream(); // read 187 | Stream todoStream({@required String todoId}); // read 188 | } 189 | ``` 190 | 191 | With this setup, creating a widget that shows a list of jobs becomes simple: 192 | ```dart 193 | @override 194 | Widget build(BuildContext context) { 195 | final database = Provider.of(context, listen: false); 196 | return StreamBuilder>( 197 | stream: database.todosStream(), 198 | builder: (context, snapshot) { 199 | // TODO: return widget based on snapshot 200 | }, 201 | ); 202 | } 203 | ``` 204 | Domain-level model classes are defined, along with `fromMap()` and `toMap()` methods for serialization. 205 | 206 | ### Note about stream-dependant services 207 | When using Firestore, is quite common to organise user data inside documents or collections that depend on the unique user `uid`. 208 | 209 | When reading or writing data, the app need to access to the user `uid`. This can change at runtime, as user can log out and sign back in with a different account, which eventually a different `uid`. 210 | 211 | This, the `FirestoreDatabase` takes the `uid` as a constructor parameter. So, the moment we have `FirebaseUser` upon user logged in, we can just pass the `uid` to the Firestore database when performing CRUD operations. 212 | 213 | To achieve this, `FirestoreDatabase` will be re-created everytime `onAuthStateChanged` changed. 214 | 215 | ## Use Case: Internationalization 216 | Added Internationalization features to the project. The example contains 2 languages namely English and Chinese. 217 | 218 | With this, the sample app now contains feature that allow user to change language from English to Chinese and vice-versa. 219 | 220 | This feature is added at Setting page - concept works the same as theme. 221 | 222 | There are 2 JSON files that contains key-value pairs of strings for English and Chinese. 223 | 224 | This 2 files is located at new folder called lang under the project root folder. 225 | 226 | ``` 227 | lang/ 228 | |- en.json 229 | |- zh.json 230 | ``` 231 | 232 | By using JSON, it will be easier to manage as you can give it to translator to translate the words without needing them to access the code. 233 | 234 | The pubspec.yaml is updated to include dependency for flutter_localizations. Also, added the asset reference for the new folder lang. 235 | 236 | Below are the steps require to add for additional languages. 237 | 1. Create a new JSON file for the new language file. For example, sk.json for Slovak. 238 | 2. Copy the content of en.json/zh.json to the new json file. 239 | 3. Pass the new JSON file to translator or do it yourself and update the word to the right word. 240 | 4. Update the main.dart to include the additional supported locales for the additional language. Inside main.dart: 241 | ``` 242 | supportedLocales: [ 243 | Locale('en', 'US'), 244 | Locale('zh', 'CN'), 245 | Locale('sk', 'SK') //example, if you add the Slovakian language 246 | ], 247 | ``` 248 | 5. Update the app_localizations.dart to include additional supported locales for the additional language: 249 | ``` 250 | @override 251 | bool isSupported(Locale locale) { 252 | // Include all of your supported language codes here 253 | return ['en', 'zh', 'sk'].contains(locale.languageCode); 254 | } 255 | ``` 256 | 6. **Take note**, if you prefer the app to be responsive to the phone actual locale setting and do not allow the app to contain any feature to change the language display, then you need to comment out the code at main.dart: 257 | ``` 258 | return MaterialApp( 259 | debugShowCheckedModeBanner: false, 260 | locale: languageProviderRef.appLocale, // <-- comment out this code 261 | ``` 262 | By comment out the code ``` locale: languageProviderRef.appLocale```, the app will response only to the system locale setting. For example: 263 | 264 | ![](media/language_control.gif) 265 | 266 | ## Use Case: Multiple Build Environment – Using multiple Flavors 267 | Added configurations and further enhanced the project structure with new codes to handle multiple build environment. 268 | 269 | It is always a good practise to build separate apps from the same source code and for different environment such as development, staging and production. 270 | 271 | In this latest update, we added 2 build environment namely development and production. 272 | 273 | Below are the steps for existing developers that used the older code and what need to be done to make your local project to have multiple build. 274 | 275 | 1. Create 2 Different Firebase Projects and Configuration 276 | ![](media/flavor-step-1.png) 277 | 278 | For this, as mentioned earlier, we will be defining 2 environments namely development and production, so we will need to create 2 Firebase projects. 279 | Then, for each Firebase projects, create a Cloud Firestore database. Do remember to have at least 1 sign-in method enabled for both projects – for this example, we are using Email/Password only. 280 | Next, we need to get the google-services.json for each Firebase project. 281 | 282 | To do that, lets assume we will be using the following applicationId for both environment: 283 | - Note App Prod – applicationId: com.example.create_flutter_provider_app.prod 284 | - Note App Dev - applicationId: com.example.create_flutter_provider_app.dev 285 | 286 | You can use your preference naming convention for the applicationId, as long it is understandable that 1 is for production environment and another is for development environment. 287 | With the applicationId defined, we will used it to add Firebase to our project. 288 | Go to each Firebase project, click on the small Android icon and the following screen will appear. Add the applicationId to the right Firebase project as specified above. 289 | ![](media/flavor-step-1b.png) 290 | 291 | Once registering the app, download the google-services.json for each Firebase projects. 292 | 293 | 2. Updating Project Config Files 294 | Add in the following configuration into the project android/app/build.gradle: 295 | ``` 296 | flavorDimensions "flavor-type" 297 | 298 | productFlavors { 299 | dev { 300 | dimension "flavor-type" 301 | applicationId "com.example.create_flutter_provider_app.dev" 302 | versionCode 1 303 | versionName "1.0" 304 | } 305 | prod{ 306 | dimension "flavor-type" 307 | applicationId "com.example.create_flutter_provider_app.prod" 308 | versionCode 1 309 | versionName "1.0" 310 | } 311 | } 312 | ``` 313 | An example: 314 | 315 | ![](media/flavor-step-2.png) 316 | 317 | 318 | 3. Create 2 folders Representing Development and Production 319 | 320 | ![](media/flavor-step-3.png) 321 | 322 | The folder name must match the named specified in productFlavors (android/app/build.gradle). 323 | The previous downloaded google-services.json files for both Firebase projects need to be moved to the right folder. 324 | For the file downloaded from Note App Prod, place it under the folder prod and for the same file name downloaded from Note App Dev, place it under the dev folder. 325 | For the sub folder called ‘res’, ignore it for now. We will go through that shortly. 326 | 327 | 4. Having different app name and app icon for Dev and Prod 328 | We want to have a different app name for our project. This is helpful as with Flavor setup, we can install both app in the same devices for testing. 329 | Having app name such as NoteApp-Dev or Note-Dev or Note-Prod helps us to identify which app is for which environment – development or production. 330 | The same concept and benefits if we have different app icon for each apps that are connecting to different environment. 331 | For app name, create file called strings.xml and with the following content: 332 | ``` 333 | 334 | 335 | Note-Dev 336 | 337 | 338 | ``` 339 | Change the app_name to fit your Dev app. Put this file under the dev/res/values: 340 | 341 | ![](media/flavor-step-4.png) 342 | 343 | Do the same for the prod environment but name your app_name to be Note-Prod or something that represent it is the app for Production. 344 | ``` 345 | 346 | 347 | Note-Prod 348 | 349 | 350 | ``` 351 | The final step for the app name is to change the Android manifest file to refer to the String name app_name. 352 | To do that, open the file AndroidManifest.xml located at android/app/src/main folder. Update the android:label as follow: 353 | ``` 354 | android:label="@string/app_name" 355 | 356 | ``` 357 | Next, for the images, copied all 5 sub folders that start with the name mipmap-xxx from main/res/, and paste it to the res folder for both dev and prod. 358 | At the end, you should have the following view: 359 | 360 | ![](media/flavor-step-4b.png) 361 | 362 | If you notice, each sub-folder with the name minimap contains images and that image is the app icon image. Change it to your needs. 363 | 364 | 5. New Dart files and Code Updates 365 | Create a flavour.dart file. This will just contain enum value that represent both dev and prod environment that the main class can refer and decide. 366 | The code: 367 | ``` 368 | enum Flavor {dev, prod} 369 | ``` 370 | Since we are using the Flutter template shared above, we will need to create additional main.dart that represent the production build and using the original main.dart to be representing as development build. 371 | Our goal is to able to build and run different flavor as follow: 372 | ``` 373 | flutter run --flavor dev -t lib/main.dart 374 | flutter run --flavor prod -t lib/main_prod.dart 375 | ``` 376 | First, update the main.dart as follow: 377 | ``` 378 | void main() { 379 | WidgetsFlutterBinding.ensureInitialized(); 380 | SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]) 381 | .then((_) async { 382 | runApp( 383 | MultiProvider( 384 | providers: [ 385 | Provider.value(value: Flavor.dev), 386 | ChangeNotifierProvider( 387 | create: (context) => ThemeProvider(), 388 | ), 389 | ChangeNotifierProvider( 390 | create: (context) => AuthProvider(), 391 | ), 392 | ChangeNotifierProvider( 393 | create: (context) => LanguageProvider(), 394 | ), 395 | ], 396 | child: MyApp( 397 | databaseBuilder: (_, uid) => FirestoreDatabase(uid: uid), 398 | ), 399 | ), 400 | ); 401 | }); 402 | } 403 | 404 | ``` 405 | As mentioned before, we will make the main.dart as the representative of building a development build. 406 | As this project is using Provider, we will pre-set the main.dart to use the dev flavor. 407 | Duplicate this file and renamed it as main_prod.dart. Update the Flavor.dev to Flavor.prod. 408 | Then, apart from the previous app name and app icon changes, we felt it is also necessary to display some info at the first screen namely the sign-in screen. 409 | Thus, since we are using Provider to set the initial value of the both main.dart and main_prod.dart, we then can access this initial default value at the sign-in screen as follow: 410 | ``` 411 | Text( 412 | Provider.of(context).toString(), 413 | ), 414 | ``` 415 | So, what will happen if we were to run the following code: 416 | ``` 417 | flutter run --flavor dev -t lib/main.dart 418 | ``` 419 | Flutter will try to build and run the app using the flavor dev and using the main.dart file. 420 | This will result the following display of sign-in screen: 421 | 422 | ![](media/flavor-step-5.png) 423 | 424 | Notice the bottom part, it contains the value of the enum value dev. 425 | 426 | The command: ```flutter run --flavor dev -t lib/main.dart``` is basically telling Flutter to build the app for testing using the flavour dev specified in the build.gradle. 427 | Also, the second part is basically telling it to run it using main.dart file. And since our main.dart has the initial flavor value pre-set as dev, it will be display as Flavor.dev. 428 | If were to run the command: ```flutter run --flavor prod -t lib/main_prod.dart```, will result the following: 429 | 430 | ![](media/flavor-step-5b.png) 431 | 432 | Notice the bottom of the screen, it is displaying Flavor.prod. 433 | 434 | ## Future Roadmap 435 | * Additional Sign-in method - Google 436 | * Animation 437 | * ~~Separation of different build flavors~~ 438 | * ~~Internationalization~~ 439 | 440 | ## Conclusion 441 | Again, take note, this is an example from my few mobile app projects, and instead of repeating the same process over and over again, I decided to spend some time to create this architecture that is easier to onboard for any new Flutter project that used Provider and Firebase. 442 | If you liked my work, don’t forget to ⭐ star the repo to show your support. 443 | 444 | ## [License: MIT](LICENSE.md) 445 | 446 | 447 | 448 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 13 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 14 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 15 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; 16 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 17 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 18 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 19 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXCopyFilesBuildPhase section */ 23 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 24 | isa = PBXCopyFilesBuildPhase; 25 | buildActionMask = 2147483647; 26 | dstPath = ""; 27 | dstSubfolderSpec = 10; 28 | files = ( 29 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, 30 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, 31 | ); 32 | name = "Embed Frameworks"; 33 | runOnlyForDeploymentPostprocessing = 0; 34 | }; 35 | /* End PBXCopyFilesBuildPhase section */ 36 | 37 | /* Begin PBXFileReference section */ 38 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 39 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 40 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 41 | 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 42 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 43 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 44 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 45 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 46 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 47 | 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 48 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 49 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 50 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 51 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 52 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 53 | /* End PBXFileReference section */ 54 | 55 | /* Begin PBXFrameworksBuildPhase section */ 56 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 57 | isa = PBXFrameworksBuildPhase; 58 | buildActionMask = 2147483647; 59 | files = ( 60 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, 61 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 62 | ); 63 | runOnlyForDeploymentPostprocessing = 0; 64 | }; 65 | /* End PBXFrameworksBuildPhase section */ 66 | 67 | /* Begin PBXGroup section */ 68 | 9740EEB11CF90186004384FC /* Flutter */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | 3B80C3931E831B6300D905FE /* App.framework */, 72 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 73 | 9740EEBA1CF902C7004384FC /* Flutter.framework */, 74 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 75 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 76 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 77 | ); 78 | name = Flutter; 79 | sourceTree = ""; 80 | }; 81 | 97C146E51CF9000F007C117D = { 82 | isa = PBXGroup; 83 | children = ( 84 | 9740EEB11CF90186004384FC /* Flutter */, 85 | 97C146F01CF9000F007C117D /* Runner */, 86 | 97C146EF1CF9000F007C117D /* Products */, 87 | ); 88 | sourceTree = ""; 89 | }; 90 | 97C146EF1CF9000F007C117D /* Products */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 97C146EE1CF9000F007C117D /* Runner.app */, 94 | ); 95 | name = Products; 96 | sourceTree = ""; 97 | }; 98 | 97C146F01CF9000F007C117D /* Runner */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 102 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 103 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 104 | 97C147021CF9000F007C117D /* Info.plist */, 105 | 97C146F11CF9000F007C117D /* Supporting Files */, 106 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 107 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 108 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 109 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 110 | ); 111 | path = Runner; 112 | sourceTree = ""; 113 | }; 114 | 97C146F11CF9000F007C117D /* Supporting Files */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | ); 118 | name = "Supporting Files"; 119 | sourceTree = ""; 120 | }; 121 | /* End PBXGroup section */ 122 | 123 | /* Begin PBXNativeTarget section */ 124 | 97C146ED1CF9000F007C117D /* Runner */ = { 125 | isa = PBXNativeTarget; 126 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 127 | buildPhases = ( 128 | 9740EEB61CF901F6004384FC /* Run Script */, 129 | 97C146EA1CF9000F007C117D /* Sources */, 130 | 97C146EB1CF9000F007C117D /* Frameworks */, 131 | 97C146EC1CF9000F007C117D /* Resources */, 132 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 133 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 134 | ); 135 | buildRules = ( 136 | ); 137 | dependencies = ( 138 | ); 139 | name = Runner; 140 | productName = Runner; 141 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 142 | productType = "com.apple.product-type.application"; 143 | }; 144 | /* End PBXNativeTarget section */ 145 | 146 | /* Begin PBXProject section */ 147 | 97C146E61CF9000F007C117D /* Project object */ = { 148 | isa = PBXProject; 149 | attributes = { 150 | LastUpgradeCheck = 1020; 151 | ORGANIZATIONNAME = "The Chromium Authors"; 152 | TargetAttributes = { 153 | 97C146ED1CF9000F007C117D = { 154 | CreatedOnToolsVersion = 7.3.1; 155 | LastSwiftMigration = 1100; 156 | }; 157 | }; 158 | }; 159 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 160 | compatibilityVersion = "Xcode 3.2"; 161 | developmentRegion = en; 162 | hasScannedForEncodings = 0; 163 | knownRegions = ( 164 | en, 165 | Base, 166 | ); 167 | mainGroup = 97C146E51CF9000F007C117D; 168 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 169 | projectDirPath = ""; 170 | projectRoot = ""; 171 | targets = ( 172 | 97C146ED1CF9000F007C117D /* Runner */, 173 | ); 174 | }; 175 | /* End PBXProject section */ 176 | 177 | /* Begin PBXResourcesBuildPhase section */ 178 | 97C146EC1CF9000F007C117D /* Resources */ = { 179 | isa = PBXResourcesBuildPhase; 180 | buildActionMask = 2147483647; 181 | files = ( 182 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 183 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 184 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 185 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 186 | ); 187 | runOnlyForDeploymentPostprocessing = 0; 188 | }; 189 | /* End PBXResourcesBuildPhase section */ 190 | 191 | /* Begin PBXShellScriptBuildPhase section */ 192 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 193 | isa = PBXShellScriptBuildPhase; 194 | buildActionMask = 2147483647; 195 | files = ( 196 | ); 197 | inputPaths = ( 198 | ); 199 | name = "Thin Binary"; 200 | outputPaths = ( 201 | ); 202 | runOnlyForDeploymentPostprocessing = 0; 203 | shellPath = /bin/sh; 204 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; 205 | }; 206 | 9740EEB61CF901F6004384FC /* Run Script */ = { 207 | isa = PBXShellScriptBuildPhase; 208 | buildActionMask = 2147483647; 209 | files = ( 210 | ); 211 | inputPaths = ( 212 | ); 213 | name = "Run Script"; 214 | outputPaths = ( 215 | ); 216 | runOnlyForDeploymentPostprocessing = 0; 217 | shellPath = /bin/sh; 218 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 219 | }; 220 | /* End PBXShellScriptBuildPhase section */ 221 | 222 | /* Begin PBXSourcesBuildPhase section */ 223 | 97C146EA1CF9000F007C117D /* Sources */ = { 224 | isa = PBXSourcesBuildPhase; 225 | buildActionMask = 2147483647; 226 | files = ( 227 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 228 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 229 | ); 230 | runOnlyForDeploymentPostprocessing = 0; 231 | }; 232 | /* End PBXSourcesBuildPhase section */ 233 | 234 | /* Begin PBXVariantGroup section */ 235 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 236 | isa = PBXVariantGroup; 237 | children = ( 238 | 97C146FB1CF9000F007C117D /* Base */, 239 | ); 240 | name = Main.storyboard; 241 | sourceTree = ""; 242 | }; 243 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 244 | isa = PBXVariantGroup; 245 | children = ( 246 | 97C147001CF9000F007C117D /* Base */, 247 | ); 248 | name = LaunchScreen.storyboard; 249 | sourceTree = ""; 250 | }; 251 | /* End PBXVariantGroup section */ 252 | 253 | /* Begin XCBuildConfiguration section */ 254 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 255 | isa = XCBuildConfiguration; 256 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 257 | buildSettings = { 258 | ALWAYS_SEARCH_USER_PATHS = NO; 259 | CLANG_ANALYZER_NONNULL = YES; 260 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 261 | CLANG_CXX_LIBRARY = "libc++"; 262 | CLANG_ENABLE_MODULES = YES; 263 | CLANG_ENABLE_OBJC_ARC = YES; 264 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 265 | CLANG_WARN_BOOL_CONVERSION = YES; 266 | CLANG_WARN_COMMA = YES; 267 | CLANG_WARN_CONSTANT_CONVERSION = YES; 268 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 269 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 270 | CLANG_WARN_EMPTY_BODY = YES; 271 | CLANG_WARN_ENUM_CONVERSION = YES; 272 | CLANG_WARN_INFINITE_RECURSION = YES; 273 | CLANG_WARN_INT_CONVERSION = YES; 274 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 275 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 276 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 277 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 278 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 279 | CLANG_WARN_STRICT_PROTOTYPES = YES; 280 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 281 | CLANG_WARN_UNREACHABLE_CODE = YES; 282 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 283 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 284 | COPY_PHASE_STRIP = NO; 285 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 286 | ENABLE_NS_ASSERTIONS = NO; 287 | ENABLE_STRICT_OBJC_MSGSEND = YES; 288 | GCC_C_LANGUAGE_STANDARD = gnu99; 289 | GCC_NO_COMMON_BLOCKS = YES; 290 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 291 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 292 | GCC_WARN_UNDECLARED_SELECTOR = YES; 293 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 294 | GCC_WARN_UNUSED_FUNCTION = YES; 295 | GCC_WARN_UNUSED_VARIABLE = YES; 296 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 297 | MTL_ENABLE_DEBUG_INFO = NO; 298 | SDKROOT = iphoneos; 299 | SUPPORTED_PLATFORMS = iphoneos; 300 | TARGETED_DEVICE_FAMILY = "1,2"; 301 | VALIDATE_PRODUCT = YES; 302 | }; 303 | name = Profile; 304 | }; 305 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 306 | isa = XCBuildConfiguration; 307 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 308 | buildSettings = { 309 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 310 | CLANG_ENABLE_MODULES = YES; 311 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 312 | ENABLE_BITCODE = NO; 313 | FRAMEWORK_SEARCH_PATHS = ( 314 | "$(inherited)", 315 | "$(PROJECT_DIR)/Flutter", 316 | ); 317 | INFOPLIST_FILE = Runner/Info.plist; 318 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 319 | LIBRARY_SEARCH_PATHS = ( 320 | "$(inherited)", 321 | "$(PROJECT_DIR)/Flutter", 322 | ); 323 | PRODUCT_BUNDLE_IDENTIFIER = com.venturearkstudio.noteapp; 324 | PRODUCT_NAME = "$(TARGET_NAME)"; 325 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 326 | SWIFT_VERSION = 5.0; 327 | VERSIONING_SYSTEM = "apple-generic"; 328 | }; 329 | name = Profile; 330 | }; 331 | 97C147031CF9000F007C117D /* Debug */ = { 332 | isa = XCBuildConfiguration; 333 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 334 | buildSettings = { 335 | ALWAYS_SEARCH_USER_PATHS = NO; 336 | CLANG_ANALYZER_NONNULL = YES; 337 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 338 | CLANG_CXX_LIBRARY = "libc++"; 339 | CLANG_ENABLE_MODULES = YES; 340 | CLANG_ENABLE_OBJC_ARC = YES; 341 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 342 | CLANG_WARN_BOOL_CONVERSION = YES; 343 | CLANG_WARN_COMMA = YES; 344 | CLANG_WARN_CONSTANT_CONVERSION = YES; 345 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 346 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 347 | CLANG_WARN_EMPTY_BODY = YES; 348 | CLANG_WARN_ENUM_CONVERSION = YES; 349 | CLANG_WARN_INFINITE_RECURSION = YES; 350 | CLANG_WARN_INT_CONVERSION = YES; 351 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 352 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 353 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 354 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 355 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 356 | CLANG_WARN_STRICT_PROTOTYPES = YES; 357 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 358 | CLANG_WARN_UNREACHABLE_CODE = YES; 359 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 360 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 361 | COPY_PHASE_STRIP = NO; 362 | DEBUG_INFORMATION_FORMAT = dwarf; 363 | ENABLE_STRICT_OBJC_MSGSEND = YES; 364 | ENABLE_TESTABILITY = YES; 365 | GCC_C_LANGUAGE_STANDARD = gnu99; 366 | GCC_DYNAMIC_NO_PIC = NO; 367 | GCC_NO_COMMON_BLOCKS = YES; 368 | GCC_OPTIMIZATION_LEVEL = 0; 369 | GCC_PREPROCESSOR_DEFINITIONS = ( 370 | "DEBUG=1", 371 | "$(inherited)", 372 | ); 373 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 374 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 375 | GCC_WARN_UNDECLARED_SELECTOR = YES; 376 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 377 | GCC_WARN_UNUSED_FUNCTION = YES; 378 | GCC_WARN_UNUSED_VARIABLE = YES; 379 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 380 | MTL_ENABLE_DEBUG_INFO = YES; 381 | ONLY_ACTIVE_ARCH = YES; 382 | SDKROOT = iphoneos; 383 | TARGETED_DEVICE_FAMILY = "1,2"; 384 | }; 385 | name = Debug; 386 | }; 387 | 97C147041CF9000F007C117D /* Release */ = { 388 | isa = XCBuildConfiguration; 389 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 390 | buildSettings = { 391 | ALWAYS_SEARCH_USER_PATHS = NO; 392 | CLANG_ANALYZER_NONNULL = YES; 393 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 394 | CLANG_CXX_LIBRARY = "libc++"; 395 | CLANG_ENABLE_MODULES = YES; 396 | CLANG_ENABLE_OBJC_ARC = YES; 397 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 398 | CLANG_WARN_BOOL_CONVERSION = YES; 399 | CLANG_WARN_COMMA = YES; 400 | CLANG_WARN_CONSTANT_CONVERSION = YES; 401 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 402 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 403 | CLANG_WARN_EMPTY_BODY = YES; 404 | CLANG_WARN_ENUM_CONVERSION = YES; 405 | CLANG_WARN_INFINITE_RECURSION = YES; 406 | CLANG_WARN_INT_CONVERSION = YES; 407 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 408 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 409 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 410 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 411 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 412 | CLANG_WARN_STRICT_PROTOTYPES = YES; 413 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 414 | CLANG_WARN_UNREACHABLE_CODE = YES; 415 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 416 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 417 | COPY_PHASE_STRIP = NO; 418 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 419 | ENABLE_NS_ASSERTIONS = NO; 420 | ENABLE_STRICT_OBJC_MSGSEND = YES; 421 | GCC_C_LANGUAGE_STANDARD = gnu99; 422 | GCC_NO_COMMON_BLOCKS = YES; 423 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 424 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 425 | GCC_WARN_UNDECLARED_SELECTOR = YES; 426 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 427 | GCC_WARN_UNUSED_FUNCTION = YES; 428 | GCC_WARN_UNUSED_VARIABLE = YES; 429 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 430 | MTL_ENABLE_DEBUG_INFO = NO; 431 | SDKROOT = iphoneos; 432 | SUPPORTED_PLATFORMS = iphoneos; 433 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 434 | TARGETED_DEVICE_FAMILY = "1,2"; 435 | VALIDATE_PRODUCT = YES; 436 | }; 437 | name = Release; 438 | }; 439 | 97C147061CF9000F007C117D /* Debug */ = { 440 | isa = XCBuildConfiguration; 441 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 442 | buildSettings = { 443 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 444 | CLANG_ENABLE_MODULES = YES; 445 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 446 | ENABLE_BITCODE = NO; 447 | FRAMEWORK_SEARCH_PATHS = ( 448 | "$(inherited)", 449 | "$(PROJECT_DIR)/Flutter", 450 | ); 451 | INFOPLIST_FILE = Runner/Info.plist; 452 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 453 | LIBRARY_SEARCH_PATHS = ( 454 | "$(inherited)", 455 | "$(PROJECT_DIR)/Flutter", 456 | ); 457 | PRODUCT_BUNDLE_IDENTIFIER = com.venturearkstudio.noteapp; 458 | PRODUCT_NAME = "$(TARGET_NAME)"; 459 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 460 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 461 | SWIFT_VERSION = 5.0; 462 | VERSIONING_SYSTEM = "apple-generic"; 463 | }; 464 | name = Debug; 465 | }; 466 | 97C147071CF9000F007C117D /* Release */ = { 467 | isa = XCBuildConfiguration; 468 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 469 | buildSettings = { 470 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 471 | CLANG_ENABLE_MODULES = YES; 472 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 473 | ENABLE_BITCODE = NO; 474 | FRAMEWORK_SEARCH_PATHS = ( 475 | "$(inherited)", 476 | "$(PROJECT_DIR)/Flutter", 477 | ); 478 | INFOPLIST_FILE = Runner/Info.plist; 479 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 480 | LIBRARY_SEARCH_PATHS = ( 481 | "$(inherited)", 482 | "$(PROJECT_DIR)/Flutter", 483 | ); 484 | PRODUCT_BUNDLE_IDENTIFIER = com.venturearkstudio.noteapp; 485 | PRODUCT_NAME = "$(TARGET_NAME)"; 486 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 487 | SWIFT_VERSION = 5.0; 488 | VERSIONING_SYSTEM = "apple-generic"; 489 | }; 490 | name = Release; 491 | }; 492 | /* End XCBuildConfiguration section */ 493 | 494 | /* Begin XCConfigurationList section */ 495 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 496 | isa = XCConfigurationList; 497 | buildConfigurations = ( 498 | 97C147031CF9000F007C117D /* Debug */, 499 | 97C147041CF9000F007C117D /* Release */, 500 | 249021D3217E4FDB00AE95B9 /* Profile */, 501 | ); 502 | defaultConfigurationIsVisible = 0; 503 | defaultConfigurationName = Release; 504 | }; 505 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 506 | isa = XCConfigurationList; 507 | buildConfigurations = ( 508 | 97C147061CF9000F007C117D /* Debug */, 509 | 97C147071CF9000F007C117D /* Release */, 510 | 249021D4217E4FDB00AE95B9 /* Profile */, 511 | ); 512 | defaultConfigurationIsVisible = 0; 513 | defaultConfigurationName = Release; 514 | }; 515 | /* End XCConfigurationList section */ 516 | }; 517 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 518 | } 519 | --------------------------------------------------------------------------------