diff --git a/README.md b/README.md index ad5889bd..514b204e 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,9 @@ To run the samples, please use the `beta` channel and Dart 1 for now. We'll upda * [Redux Example](example/redux) - Uses the [Redux](https://pub.dartlang.org/packages/redux) library to manage app state and update Widgets * [built_redux Example](example/built_redux) - Uses the [built_redux](https://pub.dartlang.org/packages/built_redux) library to enforce immutability and manage app state * [scoped_model Example](example/scoped_model) - Uses the [scoped_model](https://pub.dartlang.org/packages/scoped_model) library to hold app state and notify Widgets of Updates - + * [Firestore Redux Example](example/firestore_redux) - Uses the [Redux](https://pub.dartlang.org/packages/redux) library to manage app state and update Widgets and + adds [Cloud_Firestore](https://firebase.google.com/docs/firestore/) as the Todos database. + ### Supporting Code * [integration_tests](example/integration_tests) - Demonstrates how to write selenium-style integration (aka end to end) tests using the Page Object Model. This test suite is run against all samples. diff --git a/example/firestore_redux/.gitignore b/example/firestore_redux/.gitignore new file mode 100644 index 00000000..29e0692d --- /dev/null +++ b/example/firestore_redux/.gitignore @@ -0,0 +1,11 @@ +.DS_Store +.atom/ +.idea +.packages +.pub/ +build/ +ios/.generated/ +packages +pubspec.lock +.flutter-plugins +Podfile.lock diff --git a/example/firestore_redux/README.md b/example/firestore_redux/README.md new file mode 100644 index 00000000..33fb9a97 --- /dev/null +++ b/example/firestore_redux/README.md @@ -0,0 +1,123 @@ +# firestore redux sample + + + + +This repo started with [flutter_architecture_redux sample](https://github.com/brianegan/flutter_architecture_samples/blob/master/example/redux/README.md), +and added [Cloud_Firestore](https://firebase.google.com/docs/firestore/) as the backend database. Cloud Firestore +provides realtime connection between the database and authenticated devices, as well as automatic offline +persistence for Android and iOS. Firebase authentication is included for anonymous authentication of users. + +# Set-up + +The steps below were developed from [MemeChat repo](https://github.com/efortuna/memechat/blob/master/README.md). +There is a very useful [video tutorial](https://www.youtube.com/watch?v=w2TcYP8qiRI) associated with the MemeChat +repo from 2017 Google I/O that covers some basics related to connecting to Firebase. +In the present case, Firestore is being used but set up is similar. + +1) Set up a Firestore instance at [Firebase Console](https://console.firebase.google.com/). + +2) Enable anonymous authentication by going to 'Authentication' in left hand menu, selecting +'Sign-in Method', and enabling Anonymous at the bottom of the page. + +3) For Android: + + - Create an app within your Firebase instance for Android, with package name com.yourcompany.fireredux. + - Follow instructions to download google-services.json, and place it into fire_redux/android/app/. + - Set the defaultConfig.applicationID in `android/app/build.gradle` to match + android_client_info.package_name in `google-services.json`, e.g. `com.yourcompany.firereduxandroid`. + This is the name of your Android app in Firebase. + Package names must match between files `android/app/src/main/AndroidManifest.xml` and + `android/app/src/main/java/yourcompany/redux/MainActivity.java`, e.g. `com.yourcompany.fireredux`. + - Run the following command to get your SHA-1 key: + + `keytool -exportcert -list -v \ + -alias androiddebugkey -keystore ~/.android/debug.keystore` + - In the Firebase console, in the settings of your Android app, add your SHA-1 key by clicking "Add Fingerprint". + - To connect to Firestore be sure your project is using Gradle 4.1 and Android Studio Gradle plugin 3.0.1. + If not, then follow these + [upgrades steps](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1). + You will need to edit these files: `android/gradle/wrapper/gradle-wrapper.properties`, + `android/build.gradle`, and `android/app/build.gradle`. + +4) For iOS: + + - Create an app within your Firebase instance for iOS, with package name com.yourcompany.fireredux. + - Follow instructions to download GoogleService-Info.plist, and place it into fire_redux/ios/Runner. + - Open fire_redux/ios/Runner/Info.plist. Locate the CFBundleURLSchemes key. + The second item in the array value of this key is specific to the Firebase instance. + Replace it with the value for REVERSED_CLIENT_ID from GoogleService-Info.plist. It will look like this: + ```$xslt + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + com.yourcompany.firereduxios + com.googleusercontent.apps.631911544122-jtjdk7lmrqoiup15hofsceegpfn0dhj6 + + + + ``` + - To successfully run on iOS, it may be necessary to manually copy GoogleService-Info.plist + to your Xcode project. After you attempt a run/build of your project on iOS open Xcode by + clicking on fire_redux/ios/Runner.xcworkspace. When your project is open in Xcode, then copy + GoogleService-Info.plist to Runner/Runner folder. Then your project should run on iOS. + + +# Summary of changes made to the original redux sample repo. + +a) Added `firebase_auth` and `cloud_firestore` to pubspec.yaml. + +b) Created new file `firestore_services.dart`. Class `FirestoreServices` in this file +provides complete interface to Firestore and is called only from `main.dart`. +This file acts as the data layer for the app and updates the redux store automatically +whenever there is a change to the Firestore todos data. The app UI is connected to the redux +store per the original redux sample repo. +Changes were made to actions, reducers, and middleware files from original repo +based on this new data source. + +c) Only one widget was modified from the original repo. This was in `extra_actions_container.dart`. +```apple js +store.dispatch(new ToggleAllAction( + allCompleteSelector(todosSelector(store.state)))); +``` +Here an argument was added to ToggleAllAction to ensure all Todos are toggled correctly to +complete or active. Otherwise issues arose due to the propagation times from the app to +Firestore and back again. + +For integration testing, the following files were created to avoid using Firestore and instead +access the original repo's local data source: `package:todos_repository/src/web_client.dart`. + +`test/firestore_services_mock.dart` - the mocked data service. + +`test/main_fire_4test.dart` - this is a copy of main.dart and replaces firestore_services with +firestore_services_mock. Any changes made to a new project main.dart should be replicated here +for testing purposes. + +Additionally, `test_driver/todo_app.dart` was modified to import `test/main_fire_4test.dart` +rather than main.dart. + + + +1) `flutter test` will run all unit tests. + + a) `flutter test test/selectors_test.dart` for selectors unit testing. + + b) `flutter test test/reducer_test.dart` for reducers unit testing. + + c) `flutter test test/middleware_test.dart` for middleware unit testing. + +2) `flutter drive --target=test_driver/todo_app.dart` to run integrations test. +Integrations tests are unchanged from the original redux repo. + + +Please see original repo +[flutter_architecture_redux sample](https://github.com/brianegan/flutter_architecture_samples/blob/master/example/redux/README.md) +for all details related to [redux](https://pub.dartlang.org/packages/redux) +and [flutter_redux](https://pub.dartlang.org/packages/flutter_redux). + +Special thanks to [brianegan](https://github.com/brianegan) for providing such a wonderful repo: +[Flutter architecture samples](https://github.com/brianegan/flutter_architecture_samples/blob/master/README.md)! diff --git a/example/firestore_redux/android.iml b/example/firestore_redux/android.iml new file mode 100644 index 00000000..462b903e --- /dev/null +++ b/example/firestore_redux/android.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/example/firestore_redux/android/.gitignore b/example/firestore_redux/android/.gitignore new file mode 100644 index 00000000..1658458c --- /dev/null +++ b/example/firestore_redux/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +GeneratedPluginRegistrant.java diff --git a/example/firestore_redux/android/app/build.gradle b/example/firestore_redux/android/app/build.gradle new file mode 100644 index 00000000..e87babf3 --- /dev/null +++ b/example/firestore_redux/android/app/build.gradle @@ -0,0 +1,57 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withInputStream { stream -> + localProperties.load(stream) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 26 + buildToolsVersion '26.0.3' + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.yourcompany.firereduxandroid" + minSdkVersion 16 + targetSdkVersion 26 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + profile { + matchingFallbacks = ['debug', 'release'] + } + } +} + +flutter { + source '../..' +} + +dependencies { + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.1' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' +} + +apply plugin: 'com.google.gms.google-services' diff --git a/example/firestore_redux/android/app/src/main/AndroidManifest.xml b/example/firestore_redux/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..9ed1c988 --- /dev/null +++ b/example/firestore_redux/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + diff --git a/example/firestore_redux/android/app/src/main/java/com/yourcompany/redux/MainActivity.java b/example/firestore_redux/android/app/src/main/java/com/yourcompany/redux/MainActivity.java new file mode 100644 index 00000000..ea4bc553 --- /dev/null +++ b/example/firestore_redux/android/app/src/main/java/com/yourcompany/redux/MainActivity.java @@ -0,0 +1,14 @@ +package com.yourcompany.fireredux; + +import android.os.Bundle; + +import io.flutter.app.FlutterActivity; +import io.flutter.plugins.GeneratedPluginRegistrant; + +public class MainActivity extends FlutterActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + GeneratedPluginRegistrant.registerWith(this); + } +} diff --git a/example/firestore_redux/android/app/src/main/res/drawable/launch_background.xml b/example/firestore_redux/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/example/firestore_redux/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/firestore_redux/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/firestore_redux/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..a3f285f9 Binary files /dev/null and b/example/firestore_redux/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/example/firestore_redux/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/firestore_redux/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..5e6f3ac6 Binary files /dev/null and b/example/firestore_redux/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/example/firestore_redux/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/firestore_redux/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..144d60be Binary files /dev/null and b/example/firestore_redux/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/example/firestore_redux/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/firestore_redux/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..deafae2d Binary files /dev/null and b/example/firestore_redux/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/example/firestore_redux/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/firestore_redux/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..d5614ac8 Binary files /dev/null and b/example/firestore_redux/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/example/firestore_redux/android/app/src/main/res/values/styles.xml b/example/firestore_redux/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..00fa4417 --- /dev/null +++ b/example/firestore_redux/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/example/firestore_redux/android/build.gradle b/example/firestore_redux/android/build.gradle new file mode 100644 index 00000000..6d0f68dd --- /dev/null +++ b/example/firestore_redux/android/build.gradle @@ -0,0 +1,28 @@ +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.0.1' + classpath 'com.google.gms:google-services:3.1.0' + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/example/firestore_redux/android/gradle.properties b/example/firestore_redux/android/gradle.properties new file mode 100644 index 00000000..8bd86f68 --- /dev/null +++ b/example/firestore_redux/android/gradle.properties @@ -0,0 +1 @@ +org.gradle.jvmargs=-Xmx1536M diff --git a/example/firestore_redux/android/gradle/wrapper/gradle-wrapper.jar b/example/firestore_redux/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..13372aef Binary files /dev/null and b/example/firestore_redux/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/example/firestore_redux/android/gradle/wrapper/gradle-wrapper.properties b/example/firestore_redux/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..aa901e1e --- /dev/null +++ b/example/firestore_redux/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip diff --git a/example/firestore_redux/android/gradlew b/example/firestore_redux/android/gradlew new file mode 100755 index 00000000..9d82f789 --- /dev/null +++ b/example/firestore_redux/android/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/example/firestore_redux/android/gradlew.bat b/example/firestore_redux/android/gradlew.bat new file mode 100644 index 00000000..8a0b282a --- /dev/null +++ b/example/firestore_redux/android/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/example/firestore_redux/android/settings.gradle b/example/firestore_redux/android/settings.gradle new file mode 100644 index 00000000..115da6cb --- /dev/null +++ b/example/firestore_redux/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withInputStream { stream -> plugins.load(stream) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/example/firestore_redux/ios/.gitignore b/example/firestore_redux/ios/.gitignore new file mode 100644 index 00000000..38864eed --- /dev/null +++ b/example/firestore_redux/ios/.gitignore @@ -0,0 +1,41 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/app.flx +/Flutter/app.zip +/Flutter/App.framework +/Flutter/Flutter.framework +/Flutter/Generated.xcconfig +/ServiceDefinitions.json + +Pods/ diff --git a/example/firestore_redux/ios/Flutter/AppFrameworkInfo.plist b/example/firestore_redux/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 00000000..6c2de808 --- /dev/null +++ b/example/firestore_redux/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + UIRequiredDeviceCapabilities + + arm64 + + MinimumOSVersion + 8.0 + + diff --git a/example/firestore_redux/ios/Flutter/Debug.xcconfig b/example/firestore_redux/ios/Flutter/Debug.xcconfig new file mode 100644 index 00000000..e8efba11 --- /dev/null +++ b/example/firestore_redux/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/example/firestore_redux/ios/Flutter/Release.xcconfig b/example/firestore_redux/ios/Flutter/Release.xcconfig new file mode 100644 index 00000000..399e9340 --- /dev/null +++ b/example/firestore_redux/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/example/firestore_redux/ios/Podfile b/example/firestore_redux/ios/Podfile new file mode 100644 index 00000000..90b5f651 --- /dev/null +++ b/example/firestore_redux/ios/Podfile @@ -0,0 +1,36 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +if ENV['FLUTTER_FRAMEWORK_DIR'] == nil + abort('Please set FLUTTER_FRAMEWORK_DIR to the directory containing Flutter.framework') +end + +target 'Runner' do + # Pods for Runner + + # Flutter Pods + pod 'Flutter', :path => ENV['FLUTTER_FRAMEWORK_DIR'] + + if File.exists? '../.flutter-plugins' + flutter_root = File.expand_path('..') + File.foreach('../.flutter-plugins') { |line| + plugin = line.split(pattern='=') + if plugin.length == 2 + name = plugin[0].strip() + path = plugin[1].strip() + resolved_path = File.expand_path("#{path}/ios", flutter_root) + pod name, :path => resolved_path + else + puts "Invalid plugin specification: #{line}" + end + } + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['ENABLE_BITCODE'] = 'NO' + end + end +end diff --git a/example/firestore_redux/ios/Runner.xcodeproj/project.pbxproj b/example/firestore_redux/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..d40698dd --- /dev/null +++ b/example/firestore_redux/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,511 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 541940462056FAF2006CCFFF /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 541940452056FAF2006CCFFF /* GoogleService-Info.plist */; }; + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; + 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + D81914C92D0720DE3FC43A9C /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DC22E18A4ED9886E684E65DA /* libPods-Runner.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; + 541940452056FAF2006CCFFF /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DC22E18A4ED9886E684E65DA /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, + D81914C92D0720DE3FC43A9C /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B80C3931E831B6300D905FE /* App.framework */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 2D5378251FAA1A9400D5DBA9 /* flutter_assets */, + 9740EEBA1CF902C7004384FC /* Flutter.framework */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + C3A726D2907594DDF217E434 /* Pods */, + A194E5440BF656DB194168F6 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 541940452056FAF2006CCFFF /* GoogleService-Info.plist */, + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + A194E5440BF656DB194168F6 /* Frameworks */ = { + isa = PBXGroup; + children = ( + DC22E18A4ED9886E684E65DA /* libPods-Runner.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + C3A726D2907594DDF217E434 /* Pods */ = { + isa = PBXGroup; + children = ( + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + B6B1E6D8275F3BC613C14695 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 5B0AD370B717FEB7EAD50A04 /* [CP] Embed Pods Frameworks */, + 12391B6FDF5B43A54346CE38 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0830; + ORGANIZATIONNAME = "The Chromium Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = 9ZCVNY23KU; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 541940462056FAF2006CCFFF /* GoogleService-Info.plist in Resources */, + 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */, + 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 12391B6FDF5B43A54346CE38 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/gRPC/gRPCCertificates.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/gRPCCertificates.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + }; + 5B0AD370B717FEB7EAD50A04 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${PODS_ROOT}/../../../../../flutter/bin/cache/artifacts/engine/ios/Flutter.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + B6B1E6D8275F3BC613C14695 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = 9ZCVNY23KU; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.golftocs.firereduxios; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = 9ZCVNY23KU; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.golftocs.firereduxios; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/example/firestore_redux/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/firestore_redux/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..1d526a16 --- /dev/null +++ b/example/firestore_redux/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/firestore_redux/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/firestore_redux/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..1c958078 --- /dev/null +++ b/example/firestore_redux/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/firestore_redux/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/firestore_redux/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/example/firestore_redux/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/example/firestore_redux/ios/Runner/AppDelegate.h b/example/firestore_redux/ios/Runner/AppDelegate.h new file mode 100644 index 00000000..cf210d21 --- /dev/null +++ b/example/firestore_redux/ios/Runner/AppDelegate.h @@ -0,0 +1,6 @@ +#import +#import + +@interface AppDelegate : FlutterAppDelegate + +@end diff --git a/example/firestore_redux/ios/Runner/AppDelegate.m b/example/firestore_redux/ios/Runner/AppDelegate.m new file mode 100644 index 00000000..112becd1 --- /dev/null +++ b/example/firestore_redux/ios/Runner/AppDelegate.m @@ -0,0 +1,12 @@ +#include "AppDelegate.h" +#include "GeneratedPluginRegistrant.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GeneratedPluginRegistrant registerWithRegistry:self]; + // Override point for customization after application launch. + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d22f10b2 --- /dev/null +++ b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,116 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 00000000..980e5ad6 Binary files /dev/null and b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 00000000..fd870289 Binary files /dev/null and b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 00000000..75e84cd1 Binary files /dev/null and b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 00000000..03ab8a84 Binary files /dev/null and b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 00000000..a03431cb Binary files /dev/null and b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 00000000..f47613ee Binary files /dev/null and b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 00000000..7f2230a9 Binary files /dev/null and b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 00000000..42315c6d Binary files /dev/null and b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 00000000..f9882cc0 Binary files /dev/null and b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@1x.png b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@1x.png new file mode 100644 index 00000000..8c552e23 Binary files /dev/null and b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@1x.png differ diff --git a/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 00000000..45537513 Binary files /dev/null and b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 00000000..6360ea17 Binary files /dev/null and b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 00000000..152d5e12 Binary files /dev/null and b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 00000000..310b0b8f Binary files /dev/null and b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@3x.png b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@3x.png new file mode 100644 index 00000000..40ac4ea7 Binary files /dev/null and b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@3x.png differ diff --git a/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@1x.png b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@1x.png new file mode 100644 index 00000000..dfc408df Binary files /dev/null and b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@1x.png differ diff --git a/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 00000000..092b7bfe Binary files /dev/null and b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@3x.png b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@3x.png new file mode 100644 index 00000000..521c3e2a Binary files /dev/null and b/example/firestore_redux/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@3x.png differ diff --git a/example/firestore_redux/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/example/firestore_redux/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 00000000..0bedcf2f --- /dev/null +++ b/example/firestore_redux/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/firestore_redux/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/example/firestore_redux/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/example/firestore_redux/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/example/firestore_redux/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/firestore_redux/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/example/firestore_redux/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/example/firestore_redux/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/firestore_redux/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/example/firestore_redux/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/example/firestore_redux/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/firestore_redux/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 00000000..89c2725b --- /dev/null +++ b/example/firestore_redux/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +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. \ No newline at end of file diff --git a/example/firestore_redux/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/firestore_redux/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..f2e259c7 --- /dev/null +++ b/example/firestore_redux/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/firestore_redux/ios/Runner/Base.lproj/Main.storyboard b/example/firestore_redux/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 00000000..863684b8 --- /dev/null +++ b/example/firestore_redux/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/firestore_redux/ios/Runner/Info.plist b/example/firestore_redux/ios/Runner/Info.plist new file mode 100644 index 00000000..72ca3ffd --- /dev/null +++ b/example/firestore_redux/ios/Runner/Info.plist @@ -0,0 +1,63 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + fire redux + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + redux + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + com.yourcompany.firereduxios + com.googleusercontent.apps.631911544122-jtjdk7lmrqoiup15hofsceegpfn0dhj6 + + + + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/example/firestore_redux/ios/Runner/main.m b/example/firestore_redux/ios/Runner/main.m new file mode 100644 index 00000000..0ccc4500 --- /dev/null +++ b/example/firestore_redux/ios/Runner/main.m @@ -0,0 +1,9 @@ +#import +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/example/firestore_redux/lib/actions/actions.dart b/example/firestore_redux/lib/actions/actions.dart new file mode 100644 index 00000000..d7d93017 --- /dev/null +++ b/example/firestore_redux/lib/actions/actions.dart @@ -0,0 +1,85 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:fire_redux_sample/models/models.dart'; + +class ClearCompletedAction {} + +class ToggleAllAction { + final bool toggleAllTodosToActive; + + ToggleAllAction(this.toggleAllTodosToActive); + + @override + String toString() { + return 'ToggleAllAction{toggleAllTodosToActive: $toggleAllTodosToActive}'; + } +} + +class LoadTodosAction { + final List todos; + + LoadTodosAction(this.todos); + + @override + String toString() { + return 'LoadTodosAction{todos: $todos}'; + } +} + +class UpdateTodoAction { + final String id; + final Todo updatedTodo; + + UpdateTodoAction(this.id, this.updatedTodo); + + @override + String toString() { + return 'UpdateTodoAction{id: $id, updatedTodo: $updatedTodo}'; + } +} + +class DeleteTodoAction { + final String id; + + DeleteTodoAction(this.id); + + @override + String toString() { + return 'DeleteTodoAction{id: $id}'; + } +} + +class AddTodoAction { + final Todo todo; + + AddTodoAction(this.todo); + + @override + String toString() { + return 'AddTodoAction{todo: $todo}'; + } +} + +class UpdateFilterAction { + final VisibilityFilter newFilter; + + UpdateFilterAction(this.newFilter); + + @override + String toString() { + return 'UpdateFilterAction{newFilter: $newFilter}'; + } +} + +class UpdateTabAction { + final AppTab newTab; + + UpdateTabAction(this.newTab); + + @override + String toString() { + return 'UpdateTabAction{newTab: $newTab}'; + } +} diff --git a/example/firestore_redux/lib/containers/active_tab.dart b/example/firestore_redux/lib/containers/active_tab.dart new file mode 100644 index 00000000..50bb72cc --- /dev/null +++ b/example/firestore_redux/lib/containers/active_tab.dart @@ -0,0 +1,24 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux/redux.dart'; +import 'package:fire_redux_sample/models/models.dart'; + +class ActiveTab extends StatelessWidget { + final ViewModelBuilder builder; + + ActiveTab({Key key, @required this.builder}) : super(key: key); + + @override + Widget build(BuildContext context) { + return new StoreConnector( + distinct: true, + converter: (Store store) => store.state.activeTab, + builder: builder, + ); + } +} diff --git a/example/firestore_redux/lib/containers/add_todo.dart b/example/firestore_redux/lib/containers/add_todo.dart new file mode 100644 index 00000000..23a6fe1d --- /dev/null +++ b/example/firestore_redux/lib/containers/add_todo.dart @@ -0,0 +1,36 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_architecture_samples/flutter_architecture_samples.dart'; +import 'package:fire_redux_sample/actions/actions.dart'; +import 'package:fire_redux_sample/models/models.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux/redux.dart'; +import 'package:fire_redux_sample/presentation/add_edit_screen.dart'; + +class AddTodo extends StatelessWidget { + AddTodo({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return new StoreConnector( + converter: (Store store) { + return (task, note) { + store.dispatch(new AddTodoAction(new Todo( + task, + note: note, + ))); + }; + }, + builder: (BuildContext context, OnSaveCallback onSave) { + return new AddEditScreen( + key: ArchSampleKeys.addTodoScreen, + onSave: onSave, + isEditing: false, + ); + }, + ); + } +} diff --git a/example/firestore_redux/lib/containers/app_loading.dart b/example/firestore_redux/lib/containers/app_loading.dart new file mode 100644 index 00000000..e2b52635 --- /dev/null +++ b/example/firestore_redux/lib/containers/app_loading.dart @@ -0,0 +1,25 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux/redux.dart'; +import 'package:fire_redux_sample/models/models.dart'; +import 'package:fire_redux_sample/selectors/selectors.dart'; + +class AppLoading extends StatelessWidget { + final Function(BuildContext context, bool isLoading) builder; + + AppLoading({Key key, @required this.builder}) : super(key: key); + + @override + Widget build(BuildContext context) { + return new StoreConnector( + distinct: true, + converter: (Store store) => isLoadingSelector(store.state), + builder: builder, + ); + } +} diff --git a/example/firestore_redux/lib/containers/edit_todo.dart b/example/firestore_redux/lib/containers/edit_todo.dart new file mode 100644 index 00000000..ec2cca7f --- /dev/null +++ b/example/firestore_redux/lib/containers/edit_todo.dart @@ -0,0 +1,42 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:flutter/widgets.dart'; +import 'package:flutter_architecture_samples/flutter_architecture_samples.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux/redux.dart'; +import 'package:fire_redux_sample/actions/actions.dart'; +import 'package:fire_redux_sample/models/models.dart'; +import 'package:fire_redux_sample/presentation/add_edit_screen.dart'; + +class EditTodo extends StatelessWidget { + final Todo todo; + + EditTodo({this.todo, Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return new StoreConnector( + converter: (Store store) { + return (task, note) { + store.dispatch(new UpdateTodoAction( + todo.id, + todo.copyWith( + task: task, + note: note, + ), + )); + }; + }, + builder: (BuildContext context, OnSaveCallback onSave) { + return new AddEditScreen( + key: ArchSampleKeys.editTodoScreen, + onSave: onSave, + isEditing: true, + todo: todo, + ); + }, + ); + } +} diff --git a/example/firestore_redux/lib/containers/extra_actions_container.dart b/example/firestore_redux/lib/containers/extra_actions_container.dart new file mode 100644 index 00000000..53f977bc --- /dev/null +++ b/example/firestore_redux/lib/containers/extra_actions_container.dart @@ -0,0 +1,64 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux/redux.dart'; +import 'package:fire_redux_sample/models/models.dart'; +import 'package:fire_redux_sample/selectors/selectors.dart'; +import 'package:fire_redux_sample/actions/actions.dart'; +import 'package:fire_redux_sample/presentation/extra_actions_button.dart'; + +class ExtraActionsContainer extends StatelessWidget { + ExtraActionsContainer({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return new StoreConnector( + distinct: true, + converter: _ViewModel.fromStore, + builder: (context, vm) { + return new ExtraActionsButton( + allComplete: vm.allComplete, + onSelected: vm.onActionSelected, + ); + }, + ); + } +} + +class _ViewModel { + final Function(ExtraAction) onActionSelected; + final bool allComplete; + + _ViewModel({ + @required this.onActionSelected, + @required this.allComplete, + }); + + static _ViewModel fromStore(Store store) { + return new _ViewModel( + onActionSelected: (action) { + if (action == ExtraAction.clearCompleted) { + store.dispatch(new ClearCompletedAction()); + } else if (action == ExtraAction.toggleAllComplete) { + store.dispatch(new ToggleAllAction( + allCompleteSelector(todosSelector(store.state)))); + } + }, + allComplete: allCompleteSelector(todosSelector(store.state)), + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is _ViewModel && + runtimeType == other.runtimeType && + allComplete == other.allComplete; + + @override + int get hashCode => allComplete.hashCode; +} diff --git a/example/firestore_redux/lib/containers/filter_selector.dart b/example/firestore_redux/lib/containers/filter_selector.dart new file mode 100644 index 00000000..70562cd2 --- /dev/null +++ b/example/firestore_redux/lib/containers/filter_selector.dart @@ -0,0 +1,61 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux/redux.dart'; +import 'package:fire_redux_sample/models/models.dart'; +import 'package:fire_redux_sample/actions/actions.dart'; +import 'package:fire_redux_sample/presentation/filter_button.dart'; + +class FilterSelector extends StatelessWidget { + final bool visible; + + FilterSelector({Key key, @required this.visible}) : super(key: key); + + @override + Widget build(BuildContext context) { + return new StoreConnector( + distinct: true, + converter: _ViewModel.fromStore, + builder: (context, vm) { + return new FilterButton( + visible: visible, + activeFilter: vm.activeFilter, + onSelected: vm.onFilterSelected, + ); + }, + ); + } +} + +class _ViewModel { + final Function(VisibilityFilter) onFilterSelected; + final VisibilityFilter activeFilter; + + _ViewModel({ + @required this.onFilterSelected, + @required this.activeFilter, + }); + + static _ViewModel fromStore(Store store) { + return new _ViewModel( + onFilterSelected: (filter) { + store.dispatch(new UpdateFilterAction(filter)); + }, + activeFilter: store.state.activeFilter, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is _ViewModel && + runtimeType == other.runtimeType && + activeFilter == other.activeFilter; + + @override + int get hashCode => activeFilter.hashCode; +} diff --git a/example/firestore_redux/lib/containers/filtered_todos.dart b/example/firestore_redux/lib/containers/filtered_todos.dart new file mode 100644 index 00000000..b768046e --- /dev/null +++ b/example/firestore_redux/lib/containers/filtered_todos.dart @@ -0,0 +1,69 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux/redux.dart'; +import 'package:fire_redux_sample/models/models.dart'; +import 'package:fire_redux_sample/actions/actions.dart'; +import 'package:fire_redux_sample/selectors/selectors.dart'; +import 'package:fire_redux_sample/presentation/todo_list.dart'; + +class FilteredTodos extends StatelessWidget { + FilteredTodos({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return new StoreConnector( + converter: _ViewModel.fromStore, + builder: (context, vm) { + return new TodoList( + todos: vm.todos, + onCheckboxChanged: vm.onCheckboxChanged, + onRemove: vm.onRemove, + onUndoRemove: vm.onUndoRemove, + ); + }, + ); + } +} + +class _ViewModel { + final List todos; + final bool loading; + final Function(Todo, bool) onCheckboxChanged; + final Function(Todo) onRemove; + final Function(Todo) onUndoRemove; + + _ViewModel({ + @required this.todos, + @required this.loading, + @required this.onCheckboxChanged, + @required this.onRemove, + @required this.onUndoRemove, + }); + + static _ViewModel fromStore(Store store) { + return new _ViewModel( + todos: filteredTodosSelector( + todosSelector(store.state), + activeFilterSelector(store.state), + ), + loading: store.state.isLoading, + onCheckboxChanged: (todo, complete) { + store.dispatch(new UpdateTodoAction( + todo.id, + todo.copyWith(complete: !todo.complete), + )); + }, + onRemove: (todo) { + store.dispatch(new DeleteTodoAction(todo.id)); + }, + onUndoRemove: (todo) { + store.dispatch(new AddTodoAction(todo)); + }, + ); + } +} diff --git a/example/firestore_redux/lib/containers/stats.dart b/example/firestore_redux/lib/containers/stats.dart new file mode 100644 index 00000000..b92b3fd9 --- /dev/null +++ b/example/firestore_redux/lib/containers/stats.dart @@ -0,0 +1,42 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux/redux.dart'; +import 'package:fire_redux_sample/models/models.dart'; +import 'package:fire_redux_sample/selectors/selectors.dart'; +import 'package:fire_redux_sample/presentation/stats_counter.dart'; + +class Stats extends StatelessWidget { + Stats({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return new StoreConnector( + converter: _ViewModel.fromStore, + builder: (context, vm) { + return new StatsCounter( + numActive: vm.numActive, + numCompleted: vm.numCompleted, + ); + }, + ); + } +} + +class _ViewModel { + final int numCompleted; + final int numActive; + + _ViewModel({@required this.numCompleted, @required this.numActive}); + + static _ViewModel fromStore(Store store) { + return new _ViewModel( + numActive: numActiveSelector(todosSelector(store.state)), + numCompleted: numCompletedSelector(todosSelector(store.state)), + ); + } +} diff --git a/example/firestore_redux/lib/containers/tab_selector.dart b/example/firestore_redux/lib/containers/tab_selector.dart new file mode 100644 index 00000000..a4879127 --- /dev/null +++ b/example/firestore_redux/lib/containers/tab_selector.dart @@ -0,0 +1,73 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_architecture_samples/flutter_architecture_samples.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux/redux.dart'; +import 'package:fire_redux_sample/models/models.dart'; +import 'package:fire_redux_sample/actions/actions.dart'; + +class TabSelector extends StatelessWidget { + TabSelector({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return new StoreConnector( + distinct: true, + converter: _ViewModel.fromStore, + builder: (context, vm) { + return new BottomNavigationBar( + key: ArchSampleKeys.tabs, + currentIndex: AppTab.values.indexOf(vm.activeTab), + onTap: vm.onTabSelected, + items: AppTab.values.map((tab) { + return new BottomNavigationBarItem( + icon: new Icon( + tab == AppTab.todos ? Icons.list : Icons.show_chart, + key: tab == AppTab.todos + ? ArchSampleKeys.todoTab + : ArchSampleKeys.statsTab, + ), + title: new Text(tab == AppTab.stats + ? ArchSampleLocalizations.of(context).stats + : ArchSampleLocalizations.of(context).todos), + ); + }).toList(), + ); + }, + ); + } +} + +class _ViewModel { + final AppTab activeTab; + final Function(int) onTabSelected; + + _ViewModel({ + @required this.activeTab, + @required this.onTabSelected, + }); + + static _ViewModel fromStore(Store store) { + return new _ViewModel( + activeTab: store.state.activeTab, + onTabSelected: (index) { + store.dispatch(new UpdateTabAction((AppTab.values[index]))); + }, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is _ViewModel && + runtimeType == other.runtimeType && + activeTab == other.activeTab; + + @override + int get hashCode => activeTab.hashCode; +} diff --git a/example/firestore_redux/lib/containers/todo_details.dart b/example/firestore_redux/lib/containers/todo_details.dart new file mode 100644 index 00000000..f5a5122a --- /dev/null +++ b/example/firestore_redux/lib/containers/todo_details.dart @@ -0,0 +1,62 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux/redux.dart'; +import 'package:fire_redux_sample/actions/actions.dart'; +import 'package:fire_redux_sample/models/models.dart'; +import 'package:fire_redux_sample/selectors/selectors.dart'; +import 'package:fire_redux_sample/presentation/details_screen.dart'; + +class TodoDetails extends StatelessWidget { + final String id; + + TodoDetails({Key key, @required this.id}) : super(key: key); + + @override + Widget build(BuildContext context) { + return new StoreConnector( + ignoreChange: (state) => todoSelector(state.todos, id).isNotPresent, + converter: (Store store) { + return new _ViewModel.from(store, id); + }, + builder: (context, vm) { + return new DetailsScreen( + todo: vm.todo, + onDelete: vm.onDelete, + toggleCompleted: vm.toggleCompleted, + ); + }, + ); + } +} + +class _ViewModel { + final Todo todo; + final Function onDelete; + final Function(bool) toggleCompleted; + + _ViewModel({ + @required this.todo, + @required this.onDelete, + @required this.toggleCompleted, + }); + + factory _ViewModel.from(Store store, String id) { + final todo = todoSelector(todosSelector(store.state), id).value; + + return new _ViewModel( + todo: todo, + onDelete: () => store.dispatch(new DeleteTodoAction(todo.id)), + toggleCompleted: (isComplete) { + store.dispatch(new UpdateTodoAction( + todo.id, + todo.copyWith(complete: isComplete), + )); + }, + ); + } +} diff --git a/example/firestore_redux/lib/firestore_services.dart b/example/firestore_redux/lib/firestore_services.dart new file mode 100644 index 00000000..3cdab38b --- /dev/null +++ b/example/firestore_redux/lib/firestore_services.dart @@ -0,0 +1,76 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:fire_redux_sample/models/models.dart'; +import 'package:fire_redux_sample/actions/actions.dart'; +import 'package:fire_redux_sample/selectors/selectors.dart'; + +class FirestoreServices { + void anonymousLogin(store) { + FirebaseAuth.instance.onAuthStateChanged.listen((user) { + if (user != null) { + var isAnonymous = user.isAnonymous; + var uid = user.uid; + print( + 'In FirestoreServices, isAnonymous = $isAnonymous and uid = $uid'); + todosListener(store); + } + else { + FirebaseAuth.instance.signInAnonymously().then((user) { + todosListener(store); + }); + } + }); + } + + void todosListener(store) { + Firestore.instance.collection('todos').snapshots.listen((snapshot) { + var todosList = []; + snapshot.documents.forEach((doc) { + todosList.add(_convertSnapshotToTodo(doc)); + }); + store.dispatch(new LoadTodosAction(todosList)); + }); + } + + Todo _convertSnapshotToTodo(DocumentSnapshot document) { + return new Todo( + document['task'], + complete: document['complete'] ?? false, + id: document.documentID, + note: document['note'] ?? '', + ); + } + + void addNewTodo(store, todo) { + Firestore.instance.collection('todos').document(todo.id).setData( + {'complete': todo.complete, 'task': todo.task, 'note': todo.note}); + } + + void deleteTodo(store, List idList) { + // Code below updates the store synchronously prior to calling delete on + // Firestore. Without this an error will be thrown when using the + // dismissable widget action to delete a todo. + // Feels a little hacky, but it seems to work. + List todos = todosSelector(store.state); + List newTodos = []; + for (Todo todo in todos) { + newTodos.add(todo.copyWith()); + } + for (var id in idList) { + Todo todo = todoSelector(newTodos, id).value; + newTodos.remove(todo); + } + store.dispatch(new LoadTodosAction(newTodos)); + + Firestore.instance.collection('todos').document(idList[0]).delete(); + } + + void updateTodo(store, todo) { + Firestore.instance.collection('todos').document(todo.id).updateData( + {'complete': todo.complete, 'task': todo.task, 'note': todo.note}); + } +} diff --git a/example/firestore_redux/lib/localization.dart b/example/firestore_redux/lib/localization.dart new file mode 100644 index 00000000..7c23e862 --- /dev/null +++ b/example/firestore_redux/lib/localization.dart @@ -0,0 +1,32 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/material.dart'; + +class ReduxLocalizations { + static ReduxLocalizations of(BuildContext context) { + return Localizations.of( + context, + ReduxLocalizations, + ); + } + + String get appTitle => "Redux Example"; +} + +class ReduxLocalizationsDelegate + extends LocalizationsDelegate { + @override + Future load(Locale locale) => + new Future(() => new ReduxLocalizations()); + + @override + bool shouldReload(ReduxLocalizationsDelegate old) => false; + + @override + bool isSupported(Locale locale) => + locale.languageCode.toLowerCase().contains("en"); +} diff --git a/example/firestore_redux/lib/main.dart b/example/firestore_redux/lib/main.dart new file mode 100644 index 00000000..f7c38d5c --- /dev/null +++ b/example/firestore_redux/lib/main.dart @@ -0,0 +1,60 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_architecture_samples/flutter_architecture_samples.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux/redux.dart'; +import 'package:fire_redux_sample/containers/add_todo.dart'; +import 'package:fire_redux_sample/localization.dart'; +import 'package:fire_redux_sample/middleware/store_todos_middleware.dart'; +import 'package:fire_redux_sample/models/models.dart'; +import 'package:fire_redux_sample/presentation/home_screen.dart'; +import 'package:fire_redux_sample/reducers/app_state_reducer.dart'; +import 'package:fire_redux_sample/firestore_services.dart'; + +void main() { + runApp(new ReduxApp()); +} + +class ReduxApp extends StatelessWidget { + static dynamic firestoreServices = new FirestoreServices(); + + static Store store = new Store( + appReducer, + initialState: new AppState.loading(), + middleware: createStoreTodosMiddleware(firestoreServices), + ); + + ReduxApp() { + firestoreServices.anonymousLogin(store); + } + + @override + Widget build(BuildContext context) { + return new StoreProvider( + store: store, + child: new MaterialApp( + title: new ReduxLocalizations().appTitle, + theme: ArchSampleTheme.theme, + localizationsDelegates: [ + new ArchSampleLocalizationsDelegate(), + new ReduxLocalizationsDelegate(), + ], + routes: { + ArchSampleRoutes.home: (context) { + return new StoreBuilder( + builder: (context, store) { + return new HomeScreen(); + }, + ); + }, + ArchSampleRoutes.addTodo: (context) { + return new AddTodo(); + }, + }, + ), + ); + } +} diff --git a/example/firestore_redux/lib/middleware/store_todos_middleware.dart b/example/firestore_redux/lib/middleware/store_todos_middleware.dart new file mode 100644 index 00000000..3cf5e227 --- /dev/null +++ b/example/firestore_redux/lib/middleware/store_todos_middleware.dart @@ -0,0 +1,70 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:redux/redux.dart'; +import 'package:fire_redux_sample/actions/actions.dart'; +import 'package:fire_redux_sample/models/models.dart'; +import 'package:fire_redux_sample/selectors/selectors.dart'; + +List> createStoreTodosMiddleware(firestoreServices) { + return combineTypedMiddleware([ + new MiddlewareBinding( + _firestoreSaveNewTodo(firestoreServices)), + new MiddlewareBinding( + _firestoreDeleteTodo(firestoreServices)), + new MiddlewareBinding( + _firestoreUpdateTodo(firestoreServices)), + new MiddlewareBinding( + _firestoreToggleAll(firestoreServices)), + new MiddlewareBinding( + _firestoreClearCompleted(firestoreServices)), + ]); +} + +Middleware _firestoreSaveNewTodo(firestoreServices) { + return (Store store, action, NextDispatcher next) { + firestoreServices.addNewTodo(store, action.todo); + next(action); + }; +} + +Middleware _firestoreDeleteTodo(firestoreServices) { + return (Store store, action, NextDispatcher next) { + firestoreServices.deleteTodo(store, [action.id]); + next(action); + }; +} + +Middleware _firestoreUpdateTodo(firestoreServices) { + return (Store store, action, NextDispatcher next) { + firestoreServices.updateTodo(store, action.updatedTodo); + next(action); + }; +} + +Middleware _firestoreToggleAll(firestoreServices) { + return (Store store, action, NextDispatcher next) { + for (var todo in todosSelector(store.state)) { + if (action.toggleAllTodosToActive) { + if (todo.complete) + firestoreServices.updateTodo(store, todo.copyWith(complete: false)); + } else { + if (!todo.complete) + firestoreServices.updateTodo(store, todo.copyWith(complete: true)); + } + } + next(action); + }; +} + +Middleware _firestoreClearCompleted(firestoreServices) { + return (Store store, action, NextDispatcher next) { + List indexesToDelete = []; + for (var todo in todosSelector(store.state)) { + if (todo.complete) indexesToDelete.add(todo.id); + } + firestoreServices.deleteTodo(store, indexesToDelete); + next(action); + }; +} diff --git a/example/firestore_redux/lib/models/app_state.dart b/example/firestore_redux/lib/models/app_state.dart new file mode 100644 index 00000000..4f53dd23 --- /dev/null +++ b/example/firestore_redux/lib/models/app_state.dart @@ -0,0 +1,58 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:meta/meta.dart'; +import 'package:fire_redux_sample/models/models.dart'; + +@immutable +class AppState { + final bool isLoading; + final List todos; + final AppTab activeTab; + final VisibilityFilter activeFilter; + + AppState( + {this.isLoading = false, + this.todos = const [], + this.activeTab = AppTab.todos, + this.activeFilter = VisibilityFilter.all}); + + factory AppState.loading() => new AppState(isLoading: true); + + AppState copyWith({ + bool isLoading, + List todos, + AppTab activeTab, + VisibilityFilter activeFilter, + }) { + return new AppState( + isLoading: isLoading ?? this.isLoading, + todos: todos ?? this.todos, + activeTab: activeTab ?? this.activeTab, + activeFilter: activeFilter ?? this.activeFilter, + ); + } + + @override + int get hashCode => + isLoading.hashCode ^ + todos.hashCode ^ + activeTab.hashCode ^ + activeFilter.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AppState && + runtimeType == other.runtimeType && + isLoading == other.isLoading && + todos == other.todos && + activeTab == other.activeTab && + activeFilter == other.activeFilter; + + @override + String toString() { + return 'AppState{isLoading: $isLoading, todos: $todos, activeTab: $activeTab, activeFilter: $activeFilter}'; + } +} diff --git a/example/firestore_redux/lib/models/app_tab.dart b/example/firestore_redux/lib/models/app_tab.dart new file mode 100644 index 00000000..096b6f56 --- /dev/null +++ b/example/firestore_redux/lib/models/app_tab.dart @@ -0,0 +1,5 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +enum AppTab { todos, stats } diff --git a/example/firestore_redux/lib/models/extra_action.dart b/example/firestore_redux/lib/models/extra_action.dart new file mode 100644 index 00000000..236a7d0b --- /dev/null +++ b/example/firestore_redux/lib/models/extra_action.dart @@ -0,0 +1,5 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +enum ExtraAction { toggleAllComplete, clearCompleted } diff --git a/example/firestore_redux/lib/models/models.dart b/example/firestore_redux/lib/models/models.dart new file mode 100644 index 00000000..fa3d6aa1 --- /dev/null +++ b/example/firestore_redux/lib/models/models.dart @@ -0,0 +1,9 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +export 'app_tab.dart'; +export 'extra_action.dart'; +export 'visibility_filter.dart'; +export 'app_state.dart'; +export 'todo.dart'; diff --git a/example/firestore_redux/lib/models/todo.dart b/example/firestore_redux/lib/models/todo.dart new file mode 100644 index 00000000..e7984448 --- /dev/null +++ b/example/firestore_redux/lib/models/todo.dart @@ -0,0 +1,60 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:flutter_architecture_samples/uuid.dart'; +import 'package:meta/meta.dart'; +import 'package:todos_repository/todos_repository.dart'; + +@immutable +class Todo { + final bool complete; + final String id; + final String note; + final String task; + + Todo(this.task, {this.complete = false, String note = '', String id}) + : this.note = note ?? '', + this.id = id ?? new Uuid().generateV4(); + + Todo copyWith({bool complete, String id, String note, String task}) { + return new Todo( + task ?? this.task, + complete: complete ?? this.complete, + id: id ?? this.id, + note: note ?? this.note, + ); + } + + @override + int get hashCode => + complete.hashCode ^ task.hashCode ^ note.hashCode ^ id.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Todo && + runtimeType == other.runtimeType && + complete == other.complete && + task == other.task && + note == other.note && + id == other.id; + + @override + String toString() { + return 'Todo{complete: $complete, task: $task, note: $note, id: $id}'; + } + + TodoEntity toEntity() { + return new TodoEntity(task, id, note, complete); + } + + static Todo fromEntity(TodoEntity entity) { + return new Todo( + entity.task, + complete: entity.complete ?? false, + note: entity.note, + id: entity.id ?? new Uuid().generateV4(), + ); + } +} diff --git a/example/firestore_redux/lib/models/visibility_filter.dart b/example/firestore_redux/lib/models/visibility_filter.dart new file mode 100644 index 00000000..11f11982 --- /dev/null +++ b/example/firestore_redux/lib/models/visibility_filter.dart @@ -0,0 +1,5 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +enum VisibilityFilter { all, active, completed } diff --git a/example/firestore_redux/lib/presentation/add_edit_screen.dart b/example/firestore_redux/lib/presentation/add_edit_screen.dart new file mode 100644 index 00000000..1481a987 --- /dev/null +++ b/example/firestore_redux/lib/presentation/add_edit_screen.dart @@ -0,0 +1,87 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_architecture_samples/flutter_architecture_samples.dart'; +import 'package:fire_redux_sample/models/models.dart'; + +typedef OnSaveCallback = Function(String task, String note); + +class AddEditScreen extends StatelessWidget { + static final GlobalKey _formKey = new GlobalKey(); + static final GlobalKey> _taskKey = + new GlobalKey>(); + static final GlobalKey> _noteKey = + new GlobalKey>(); + + final bool isEditing; + final Function(String, String) onSave; + final Todo todo; + + AddEditScreen( + {Key key, @required this.onSave, @required this.isEditing, this.todo}) + : super(key: key ?? ArchSampleKeys.addTodoScreen); + + @override + Widget build(BuildContext context) { + final localizations = ArchSampleLocalizations.of(context); + final textTheme = Theme.of(context).textTheme; + + return new Scaffold( + appBar: new AppBar( + title: new Text( + isEditing ? localizations.editTodo : localizations.addTodo, + ), + ), + body: new Padding( + padding: new EdgeInsets.all(16.0), + child: new Form( + key: _formKey, + child: new ListView( + children: [ + new TextFormField( + initialValue: isEditing ? todo.task : '', + key: _taskKey, + autofocus: !isEditing, + style: textTheme.headline, + decoration: new InputDecoration( + hintText: localizations.newTodoHint, + ), + validator: (val) { + return val.trim().isEmpty + ? localizations.emptyTodoError + : null; + }, + ), + new TextFormField( + initialValue: isEditing ? todo.note : '', + key: _noteKey, + maxLines: 10, + style: textTheme.subhead, + decoration: new InputDecoration( + hintText: localizations.notesHint, + ), + ) + ], + ), + ), + ), + floatingActionButton: new FloatingActionButton( + tooltip: isEditing ? localizations.saveChanges : localizations.addTodo, + child: new Icon(isEditing ? Icons.check : Icons.add), + onPressed: () { + if (_formKey.currentState.validate()) { + onSave( + _taskKey.currentState.value, + _noteKey.currentState.value, + ); + + Navigator.pop(context); + } + }, + ), + ); + } +} diff --git a/example/firestore_redux/lib/presentation/details_screen.dart b/example/firestore_redux/lib/presentation/details_screen.dart new file mode 100644 index 00000000..31f8cbf6 --- /dev/null +++ b/example/firestore_redux/lib/presentation/details_screen.dart @@ -0,0 +1,107 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_architecture_samples/flutter_architecture_samples.dart'; +import 'package:fire_redux_sample/containers/edit_todo.dart'; +import 'package:fire_redux_sample/models/models.dart'; + +class DetailsScreen extends StatelessWidget { + final Todo todo; + final Function onDelete; + final Function(bool) toggleCompleted; + + DetailsScreen({ + Key key, + @required this.todo, + @required this.onDelete, + @required this.toggleCompleted, + }) + : super(key: key); + + @override + Widget build(BuildContext context) { + final localizations = ArchSampleLocalizations.of(context); + + return new Scaffold( + appBar: new AppBar( + title: new Text(localizations.todoDetails), + actions: [ + new IconButton( + tooltip: localizations.deleteTodo, + key: ArchSampleKeys.deleteTodoButton, + icon: new Icon(Icons.delete), + onPressed: () { + onDelete(); + Navigator.pop(context, todo); + }, + ) + ], + ), + body: new Padding( + padding: new EdgeInsets.all(16.0), + child: new ListView( + children: [ + new Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + new Padding( + padding: new EdgeInsets.only(right: 8.0), + child: new Checkbox( + value: todo.complete, + onChanged: toggleCompleted, + ), + ), + new Expanded( + child: new Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + new Hero( + tag: todo.task + '__heroTag', + child: new Container( + width: MediaQuery.of(context).size.width, + padding: new EdgeInsets.only( + top: 8.0, + bottom: 16.0, + ), + child: new Text( + todo.task, + key: ArchSampleKeys.detailsTodoItemTask, + style: Theme.of(context).textTheme.headline, + ), + ), + ), + new Text( + todo.note, + key: ArchSampleKeys.detailsTodoItemNote, + style: Theme.of(context).textTheme.subhead, + ), + ], + ), + ), + ], + ), + ], + ), + ), + floatingActionButton: new FloatingActionButton( + key: ArchSampleKeys.editTodoFab, + tooltip: localizations.editTodo, + child: new Icon(Icons.edit), + onPressed: () { + Navigator.of(context).push( + new MaterialPageRoute( + builder: (context) { + return new EditTodo( + todo: todo, + ); + }, + ), + ); + }, + ), + ); + } +} diff --git a/example/firestore_redux/lib/presentation/extra_actions_button.dart b/example/firestore_redux/lib/presentation/extra_actions_button.dart new file mode 100644 index 00000000..1c101cb9 --- /dev/null +++ b/example/firestore_redux/lib/presentation/extra_actions_button.dart @@ -0,0 +1,41 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_architecture_samples/flutter_architecture_samples.dart'; +import 'package:fire_redux_sample/models/models.dart'; + +class ExtraActionsButton extends StatelessWidget { + final PopupMenuItemSelected onSelected; + final bool allComplete; + + ExtraActionsButton({ + this.onSelected, + this.allComplete = false, + Key key, + }) + : super(key: ArchSampleKeys.extraActionsButton); + + @override + Widget build(BuildContext context) { + return new PopupMenuButton( + onSelected: onSelected, + itemBuilder: (BuildContext context) => >[ + new PopupMenuItem( + key: ArchSampleKeys.toggleAll, + value: ExtraAction.toggleAllComplete, + child: new Text(allComplete + ? ArchSampleLocalizations.of(context).markAllIncomplete + : ArchSampleLocalizations.of(context).markAllComplete), + ), + new PopupMenuItem( + key: ArchSampleKeys.clearCompleted, + value: ExtraAction.clearCompleted, + child: + new Text(ArchSampleLocalizations.of(context).clearCompleted), + ), + ], + ); + } +} diff --git a/example/firestore_redux/lib/presentation/filter_button.dart b/example/firestore_redux/lib/presentation/filter_button.dart new file mode 100644 index 00000000..a30b0346 --- /dev/null +++ b/example/firestore_redux/lib/presentation/filter_button.dart @@ -0,0 +1,70 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_architecture_samples/flutter_architecture_samples.dart'; +import 'package:fire_redux_sample/models/models.dart'; + +class FilterButton extends StatelessWidget { + final PopupMenuItemSelected onSelected; + final VisibilityFilter activeFilter; + final bool visible; + + FilterButton({this.onSelected, this.activeFilter, this.visible, Key key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final defaultStyle = Theme.of(context).textTheme.body1; + final activeStyle = Theme + .of(context) + .textTheme + .body1 + .copyWith(color: Theme.of(context).accentColor); + + return new AnimatedOpacity( + opacity: visible ? 1.0 : 0.0, + duration: new Duration(milliseconds: 150), + child: new PopupMenuButton( + key: ArchSampleKeys.filterButton, + tooltip: ArchSampleLocalizations.of(context).filterTodos, + onSelected: onSelected, + itemBuilder: (BuildContext context) => + >[ + new PopupMenuItem( + key: ArchSampleKeys.allFilter, + value: VisibilityFilter.all, + child: new Text( + ArchSampleLocalizations.of(context).showAll, + style: activeFilter == VisibilityFilter.all + ? activeStyle + : defaultStyle, + ), + ), + new PopupMenuItem( + key: ArchSampleKeys.activeFilter, + value: VisibilityFilter.active, + child: new Text( + ArchSampleLocalizations.of(context).showActive, + style: activeFilter == VisibilityFilter.active + ? activeStyle + : defaultStyle, + ), + ), + new PopupMenuItem( + key: ArchSampleKeys.completedFilter, + value: VisibilityFilter.completed, + child: new Text( + ArchSampleLocalizations.of(context).showCompleted, + style: activeFilter == VisibilityFilter.completed + ? activeStyle + : defaultStyle, + ), + ), + ], + icon: new Icon(Icons.filter_list), + ), + ); + } +} diff --git a/example/firestore_redux/lib/presentation/home_screen.dart b/example/firestore_redux/lib/presentation/home_screen.dart new file mode 100644 index 00000000..6c3816ea --- /dev/null +++ b/example/firestore_redux/lib/presentation/home_screen.dart @@ -0,0 +1,45 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_architecture_samples/flutter_architecture_samples.dart'; +import 'package:fire_redux_sample/containers/extra_actions_container.dart'; +import 'package:fire_redux_sample/containers/filter_selector.dart'; +import 'package:fire_redux_sample/containers/active_tab.dart'; +import 'package:fire_redux_sample/containers/filtered_todos.dart'; +import 'package:fire_redux_sample/containers/stats.dart'; +import 'package:fire_redux_sample/containers/tab_selector.dart'; +import 'package:fire_redux_sample/models/models.dart'; +import 'package:fire_redux_sample/localization.dart'; + +class HomeScreen extends StatelessWidget { + HomeScreen() : super(key: ArchSampleKeys.homeScreen); + + @override + Widget build(BuildContext context) { + return new ActiveTab( + builder: (BuildContext context, AppTab activeTab) { + return new Scaffold( + appBar: new AppBar( + title: new Text(ReduxLocalizations.of(context).appTitle), + actions: [ + new FilterSelector(visible: activeTab == AppTab.todos), + new ExtraActionsContainer(), + ], + ), + body: activeTab == AppTab.todos ? new FilteredTodos() : new Stats(), + floatingActionButton: new FloatingActionButton( + key: ArchSampleKeys.addTodoFab, + onPressed: () { + Navigator.pushNamed(context, ArchSampleRoutes.addTodo); + }, + child: new Icon(Icons.add), + tooltip: ArchSampleLocalizations.of(context).addTodo, + ), + bottomNavigationBar: new TabSelector(), + ); + }, + ); + } +} diff --git a/example/firestore_redux/lib/presentation/loading_indicator.dart b/example/firestore_redux/lib/presentation/loading_indicator.dart new file mode 100644 index 00000000..cf9f2ed7 --- /dev/null +++ b/example/firestore_redux/lib/presentation/loading_indicator.dart @@ -0,0 +1,16 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:flutter/material.dart'; + +class LoadingIndicator extends StatelessWidget { + LoadingIndicator({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return new Center( + child: new CircularProgressIndicator(), + ); + } +} diff --git a/example/firestore_redux/lib/presentation/stats_counter.dart b/example/firestore_redux/lib/presentation/stats_counter.dart new file mode 100644 index 00000000..155c61eb --- /dev/null +++ b/example/firestore_redux/lib/presentation/stats_counter.dart @@ -0,0 +1,69 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_architecture_samples/flutter_architecture_samples.dart'; +import 'package:fire_redux_sample/containers/app_loading.dart'; +import 'package:fire_redux_sample/presentation/loading_indicator.dart'; + +class StatsCounter extends StatelessWidget { + final int numActive; + final int numCompleted; + + StatsCounter({ + @required this.numActive, + @required this.numCompleted, + }); + + @override + Widget build(BuildContext context) { + return new AppLoading(builder: (context, loading) { + return loading + ? new LoadingIndicator(key: new Key('__statsLoading__')) + : _buildStats(context); + }); + } + + Widget _buildStats(BuildContext context) { + return new Center( + child: new Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + new Padding( + padding: new EdgeInsets.only(bottom: 8.0), + child: new Text( + ArchSampleLocalizations.of(context).completedTodos, + style: Theme.of(context).textTheme.title, + ), + ), + new Padding( + padding: new EdgeInsets.only(bottom: 24.0), + child: new Text( + '$numCompleted', + key: ArchSampleKeys.statsNumCompleted, + style: Theme.of(context).textTheme.subhead, + ), + ), + new Padding( + padding: new EdgeInsets.only(bottom: 8.0), + child: new Text( + ArchSampleLocalizations.of(context).activeTodos, + style: Theme.of(context).textTheme.title, + ), + ), + new Padding( + padding: new EdgeInsets.only(bottom: 24.0), + child: new Text( + "$numActive", + key: ArchSampleKeys.statsNumActive, + style: Theme.of(context).textTheme.subhead, + ), + ) + ], + ), + ); + } +} diff --git a/example/firestore_redux/lib/presentation/todo_item.dart b/example/firestore_redux/lib/presentation/todo_item.dart new file mode 100644 index 00000000..fa26b283 --- /dev/null +++ b/example/firestore_redux/lib/presentation/todo_item.dart @@ -0,0 +1,56 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_architecture_samples/flutter_architecture_samples.dart'; +import 'package:fire_redux_sample/models/models.dart'; + +class TodoItem extends StatelessWidget { + final DismissDirectionCallback onDismissed; + final GestureTapCallback onTap; + final ValueChanged onCheckboxChanged; + final Todo todo; + + TodoItem({ + @required this.onDismissed, + @required this.onTap, + @required this.onCheckboxChanged, + @required this.todo, + }); + + @override + Widget build(BuildContext context) { + return new Dismissible( + key: ArchSampleKeys.todoItem(todo.id), + onDismissed: onDismissed, + child: new ListTile( + onTap: onTap, + leading: new Checkbox( + key: ArchSampleKeys.todoItemCheckbox(todo.id), + value: todo.complete, + onChanged: onCheckboxChanged, + ), + title: new Hero( + tag: todo.task + '__heroTag', + child: new Container( + width: MediaQuery.of(context).size.width, + child: new Text( + todo.task, + key: ArchSampleKeys.todoItemTask(todo.id), + style: Theme.of(context).textTheme.title, + ), + ), + ), + subtitle: new Text( + todo.note, + key: ArchSampleKeys.todoItemNote(todo.id), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.subhead, + ), + ), + ); + } +} diff --git a/example/firestore_redux/lib/presentation/todo_list.dart b/example/firestore_redux/lib/presentation/todo_list.dart new file mode 100644 index 00000000..d4999a9f --- /dev/null +++ b/example/firestore_redux/lib/presentation/todo_list.dart @@ -0,0 +1,102 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_architecture_samples/flutter_architecture_samples.dart'; +import 'package:fire_redux_sample/containers/app_loading.dart'; +import 'package:fire_redux_sample/containers/todo_details.dart'; +import 'package:fire_redux_sample/models/models.dart'; +import 'package:fire_redux_sample/presentation/loading_indicator.dart'; +import 'package:fire_redux_sample/presentation/todo_item.dart'; + +class TodoList extends StatelessWidget { + final List todos; + final Function(Todo, bool) onCheckboxChanged; + final Function(Todo) onRemove; + final Function(Todo) onUndoRemove; + + TodoList({ + Key key, + @required this.todos, + @required this.onCheckboxChanged, + @required this.onRemove, + @required this.onUndoRemove, + }) + : super(key: key); + + @override + Widget build(BuildContext context) { + return new AppLoading(builder: (context, loading) { + return loading + ? new LoadingIndicator(key: ArchSampleKeys.todosLoading) + : _buildListView(); + }); + } + + ListView _buildListView() { + return new ListView.builder( + key: ArchSampleKeys.todoList, + itemCount: todos.length, + itemBuilder: (BuildContext context, int index) { + final todo = todos[index]; + + return new TodoItem( + todo: todo, + onDismissed: (direction) { + _removeTodo(context, todo); + }, + onTap: () => _onTodoTap(context, todo), + onCheckboxChanged: (complete) { + onCheckboxChanged(todo, complete); + }, + ); + }, + ); + } + + void _removeTodo(BuildContext context, Todo todo) { + onRemove(todo); + + Scaffold.of(context).showSnackBar(new SnackBar( + duration: new Duration(seconds: 2), + backgroundColor: Theme.of(context).backgroundColor, + content: new Text( + ArchSampleLocalizations.of(context).todoDeleted(todo.task), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + action: new SnackBarAction( + label: ArchSampleLocalizations.of(context).undo, + onPressed: () => onUndoRemove(todo), + ))); + } + + void _onTodoTap(BuildContext context, Todo todo) { + Navigator + .of(context) + .push(new MaterialPageRoute( + builder: (_) => new TodoDetails(id: todo.id), + )) + .then((removedTodo) { + if (removedTodo != null) { + Scaffold.of(context).showSnackBar(new SnackBar( + key: ArchSampleKeys.snackbar, + duration: new Duration(seconds: 2), + backgroundColor: Theme.of(context).backgroundColor, + content: new Text( + ArchSampleLocalizations.of(context).todoDeleted(todo.task), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + action: new SnackBarAction( + label: ArchSampleLocalizations.of(context).undo, + onPressed: () { + onUndoRemove(todo); + }, + ))); + } + }); + } +} diff --git a/example/firestore_redux/lib/presentation/typedefs.dart b/example/firestore_redux/lib/presentation/typedefs.dart new file mode 100644 index 00000000..90fcaa0a --- /dev/null +++ b/example/firestore_redux/lib/presentation/typedefs.dart @@ -0,0 +1,11 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:fire_redux_sample/models/models.dart'; + +typedef TodoAdder(Todo todo); + +typedef TodoRemover(String id); + +typedef TodoUpdater(String id, Todo todo); diff --git a/example/firestore_redux/lib/reducers/app_state_reducer.dart b/example/firestore_redux/lib/reducers/app_state_reducer.dart new file mode 100644 index 00000000..0f8c1eda --- /dev/null +++ b/example/firestore_redux/lib/reducers/app_state_reducer.dart @@ -0,0 +1,19 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:fire_redux_sample/models/models.dart'; +import 'package:fire_redux_sample/reducers/loading_reducer.dart'; +import 'package:fire_redux_sample/reducers/todos_reducer.dart'; +import 'package:fire_redux_sample/reducers/visibility_reducer.dart'; +import 'package:fire_redux_sample/reducers/tabs_reducer.dart'; + +// We create the State reducer by combining many smaller reducers into one! +AppState appReducer(AppState state, action) { + return new AppState( + isLoading: loadingReducer(state.isLoading, action), + todos: todosReducer(state.todos, action), + activeFilter: visibilityReducer(state.activeFilter, action), + activeTab: tabsReducer(state.activeTab, action), + ); +} diff --git a/example/firestore_redux/lib/reducers/loading_reducer.dart b/example/firestore_redux/lib/reducers/loading_reducer.dart new file mode 100644 index 00000000..2acc982c --- /dev/null +++ b/example/firestore_redux/lib/reducers/loading_reducer.dart @@ -0,0 +1,14 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:redux/redux.dart'; +import 'package:fire_redux_sample/actions/actions.dart'; + +final loadingReducer = combineTypedReducers([ + new ReducerBinding(_setLoaded), +]); + +bool _setLoaded(bool state, action) { + return false; +} diff --git a/example/firestore_redux/lib/reducers/tabs_reducer.dart b/example/firestore_redux/lib/reducers/tabs_reducer.dart new file mode 100644 index 00000000..41f2d157 --- /dev/null +++ b/example/firestore_redux/lib/reducers/tabs_reducer.dart @@ -0,0 +1,15 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:redux/redux.dart'; +import 'package:fire_redux_sample/actions/actions.dart'; +import 'package:fire_redux_sample/models/models.dart'; + +final tabsReducer = combineTypedReducers([ + new ReducerBinding(_activeTabReducer), +]); + +AppTab _activeTabReducer(AppTab activeTab, UpdateTabAction action) { + return action.newTab; +} diff --git a/example/firestore_redux/lib/reducers/todos_reducer.dart b/example/firestore_redux/lib/reducers/todos_reducer.dart new file mode 100644 index 00000000..c53d9e85 --- /dev/null +++ b/example/firestore_redux/lib/reducers/todos_reducer.dart @@ -0,0 +1,15 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:redux/redux.dart'; +import 'package:fire_redux_sample/actions/actions.dart'; +import 'package:fire_redux_sample/models/models.dart'; + +final todosReducer = combineTypedReducers>([ + new ReducerBinding, LoadTodosAction>(_setLoadedTodos), +]); + +List _setLoadedTodos(List todos, LoadTodosAction action) { + return action.todos; +} diff --git a/example/firestore_redux/lib/reducers/visibility_reducer.dart b/example/firestore_redux/lib/reducers/visibility_reducer.dart new file mode 100644 index 00000000..b8f25e38 --- /dev/null +++ b/example/firestore_redux/lib/reducers/visibility_reducer.dart @@ -0,0 +1,17 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:redux/redux.dart'; +import 'package:fire_redux_sample/actions/actions.dart'; +import 'package:fire_redux_sample/models/models.dart'; + +final visibilityReducer = combineTypedReducers([ + new ReducerBinding( + _activeFilterReducer), +]); + +VisibilityFilter _activeFilterReducer( + VisibilityFilter activeFilter, UpdateFilterAction action) { + return action.newFilter; +} diff --git a/example/firestore_redux/lib/selectors/selectors.dart b/example/firestore_redux/lib/selectors/selectors.dart new file mode 100644 index 00000000..51555c58 --- /dev/null +++ b/example/firestore_redux/lib/selectors/selectors.dart @@ -0,0 +1,46 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:fire_redux_sample/models/models.dart'; +import 'package:flutter_architecture_samples/optional.dart'; + +List todosSelector(AppState state) => state.todos; + +VisibilityFilter activeFilterSelector(AppState state) => state.activeFilter; + +AppTab activeTabSelector(AppState state) => state.activeTab; + +bool isLoadingSelector(AppState state) => state.isLoading; + +bool allCompleteSelector(List todos) => + todos.every((todo) => todo.complete); + +int numActiveSelector(List todos) => + todos.fold(0, (sum, todo) => !todo.complete ? ++sum : sum); + +int numCompletedSelector(List todos) => + todos.fold(0, (sum, todo) => todo.complete ? ++sum : sum); + +List filteredTodosSelector( + List todos, + VisibilityFilter activeFilter, +) { + return todos.where((todo) { + if (activeFilter == VisibilityFilter.all) { + return true; + } else if (activeFilter == VisibilityFilter.active) { + return !todo.complete; + } else if (activeFilter == VisibilityFilter.completed) { + return todo.complete; + } + }).toList(); +} + +Optional todoSelector(List todos, String id) { + try { + return new Optional.of(todos.firstWhere((todo) => todo.id == id)); + } catch (e) { + return new Optional.absent(); + } +} diff --git a/example/firestore_redux/pubspec.yaml b/example/firestore_redux/pubspec.yaml new file mode 100644 index 00000000..466ae490 --- /dev/null +++ b/example/firestore_redux/pubspec.yaml @@ -0,0 +1,67 @@ +name: fire_redux_sample +description: redux_sample using Firestore + +dependencies: + meta: '>=1.1.0 <2.0.0' + redux: ^2.0.2 + flutter_redux: '0.3.2' + flutter: + sdk: flutter + firebase_auth: 0.4.7 + cloud_firestore: 0.2.12 + flutter_architecture_samples: + path: ../../ + todos_repository: + path: ../todos_repository + +dev_dependencies: + test: ^0.12.0 + mockito: ">=2.2.0 <3.0.0" + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_tests: + path: ../integration_tests + +# For information on the generic Dart part of this file, see the +# following page: https://www.dartlang.org/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.io/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.io/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.io/custom-fonts/#from-packages diff --git a/example/firestore_redux/test/firestore_services_mock.dart b/example/firestore_redux/test/firestore_services_mock.dart new file mode 100644 index 00000000..7d973255 --- /dev/null +++ b/example/firestore_redux/test/firestore_services_mock.dart @@ -0,0 +1,65 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'dart:async'; +import 'package:fire_redux_sample/models/models.dart'; +import 'package:fire_redux_sample/actions/actions.dart'; +import 'package:todos_repository/src/web_client.dart'; +import 'package:fire_redux_sample/selectors/selectors.dart'; + +class FirestoreServices { + void anonymousLogin(store, [delayAuth = const Duration(milliseconds: 1000)]) { + new Future.delayed(delayAuth, () { + todosListener(store); + }); + } + + void todosListener(store, {webClient = const WebClient()}) { + var todosList = []; + webClient.fetchTodos().then((listTodoEntity) { + todosList = listTodoEntity.map(Todo.fromEntity).toList(); + store.dispatch(new LoadTodosAction(todosList)); + }); + } + + void addNewTodo(store, newTodo) { + List todos = todosSelector(store.state); + List newTodos = []; + for (Todo todo in todos) { + newTodos.add(todo.copyWith()); + } + newTodos.add(newTodo); + store.dispatch(new LoadTodosAction(newTodos)); + } + + void deleteTodo(store, List idList) { + List todos = todosSelector(store.state); + List newTodos = []; + for (Todo todo in todos) { + newTodos.add(todo.copyWith()); + } + for (var id in idList) { + Todo todo = todoSelector(newTodos, id).value; + newTodos.remove(todo); + } + store.dispatch(new LoadTodosAction(newTodos)); + } + + void updateTodo(store, todo) { + List todos = todosSelector(store.state); + List newTodos = []; + for (Todo todo in todos) { + newTodos.add(todo.copyWith()); + } + int todoIndex = 0; + for (var searchTodo in newTodos) { + if (searchTodo.id == todo.id) { + break; + } + todoIndex++; + } + newTodos.replaceRange(todoIndex, todoIndex + 1, [todo]); + store.dispatch(new LoadTodosAction(newTodos)); + } +} diff --git a/example/firestore_redux/test/main_fire_4test.dart b/example/firestore_redux/test/main_fire_4test.dart new file mode 100644 index 00000000..bca44fb9 --- /dev/null +++ b/example/firestore_redux/test/main_fire_4test.dart @@ -0,0 +1,64 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_architecture_samples/flutter_architecture_samples.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux/redux.dart'; +import 'package:fire_redux_sample/containers/add_todo.dart'; +import 'package:fire_redux_sample/localization.dart'; +import 'package:fire_redux_sample/models/models.dart'; +import 'package:fire_redux_sample/reducers/app_state_reducer.dart'; +import 'package:fire_redux_sample/middleware/store_todos_middleware.dart'; +import 'package:fire_redux_sample/presentation/home_screen.dart'; +import 'firestore_services_mock.dart'; + +// This is a copy of main.dart only replacing +// import 'firestore_services_mock.dart'; for +// import 'firestore_services.dart'; + +void main() { + runApp(new ReduxApp()); +} + +class ReduxApp extends StatelessWidget { + static dynamic firestoreServices = new FirestoreServices(); + + static Store store = new Store( + appReducer, + initialState: new AppState.loading(), + middleware: createStoreTodosMiddleware(firestoreServices), + ); + + ReduxApp() { + firestoreServices.anonymousLogin(store); + } + + @override + Widget build(BuildContext context) { + return new StoreProvider( + store: store, + child: new MaterialApp( + title: new ReduxLocalizations().appTitle, + theme: ArchSampleTheme.theme, + localizationsDelegates: [ + new ArchSampleLocalizationsDelegate(), + new ReduxLocalizationsDelegate(), + ], + routes: { + ArchSampleRoutes.home: (context) { + return new StoreBuilder( + builder: (context, store) { + return new HomeScreen(); + }, + ); + }, + ArchSampleRoutes.addTodo: (context) { + return new AddTodo(); + }, + }, + ), + ); + } +} diff --git a/example/firestore_redux/test/middleware_test.dart b/example/firestore_redux/test/middleware_test.dart new file mode 100644 index 00000000..0f9b0b33 --- /dev/null +++ b/example/firestore_redux/test/middleware_test.dart @@ -0,0 +1,148 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:redux/redux.dart'; +import 'package:fire_redux_sample/models/models.dart'; +import 'package:fire_redux_sample/actions/actions.dart'; +import 'package:fire_redux_sample/reducers/app_state_reducer.dart'; +import 'package:fire_redux_sample/middleware/store_todos_middleware.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fire_redux_sample/selectors/selectors.dart'; +import 'firestore_services_mock.dart'; + +main() { + group('verify all actions with Middleware', () { + test('AddTodoAction: should have 3 todos, 2 active and 1 completed', () { + final firestoreServices = new FirestoreServices(); + final store = new Store( + appReducer, + initialState: new AppState.loading(), + middleware: createStoreTodosMiddleware(firestoreServices), + ); + final todo1 = new Todo("Hallo", id: '1'); + final todo2 = new Todo("Bye", complete: true); + final todo3 = new Todo("Uncertain"); + + store.dispatch(new AddTodoAction(todo1)); + store.dispatch(new AddTodoAction(todo2)); + store.dispatch(new AddTodoAction(todo3)); + expect(todosSelector(store.state).length, 3); + expect(numActiveSelector(todosSelector(store.state)), 2); + expect(numCompletedSelector(todosSelector(store.state)), 1); + }); + + test( + 'ClearCompletedAction: ' + 'should have 2 todos, 2 active and 0 completed', () { + final firestoreServices = new FirestoreServices(); + final store = new Store( + appReducer, + initialState: new AppState.loading(), + middleware: createStoreTodosMiddleware(firestoreServices), + ); + final todo1 = new Todo("Hallo", id: '1'); + final todo2 = new Todo("Bye", complete: true); + final todo3 = new Todo("Uncertain"); + + store.dispatch(new AddTodoAction(todo1)); + store.dispatch(new AddTodoAction(todo2)); + store.dispatch(new AddTodoAction(todo3)); + store.dispatch(new ClearCompletedAction()); + expect(todosSelector(store.state).length, 2); + expect(numActiveSelector(todosSelector(store.state)), 2); + expect(numCompletedSelector(todosSelector(store.state)), 0); + }); + + test( + 'ToggleAllAction(false): ' + 'should have 3 todos, 0 active and 3 completed', () { + final firestoreServices = new FirestoreServices(); + final store = new Store( + appReducer, + initialState: new AppState.loading(), + middleware: createStoreTodosMiddleware(firestoreServices), + ); + final todo1 = new Todo("Hallo", id: '1'); + final todo2 = new Todo("Bye", complete: true); + final todo3 = new Todo("Uncertain"); + + store.dispatch(new AddTodoAction(todo1)); + store.dispatch(new AddTodoAction(todo2)); + store.dispatch(new AddTodoAction(todo3)); + store.dispatch(new ToggleAllAction(false)); + expect(todosSelector(store.state).length, 3); + expect(numActiveSelector(todosSelector(store.state)), 0); + expect(numCompletedSelector(todosSelector(store.state)), 3); + }); + + test( + 'ToggleAllAction(true): ' + 'should have 3 todos, 3 active and 0 completed', () { + final firestoreServices = new FirestoreServices(); + final store = new Store( + appReducer, + initialState: new AppState.loading(), + middleware: createStoreTodosMiddleware(firestoreServices), + ); + final todo1 = new Todo("Hallo", id: '1'); + final todo2 = new Todo("Bye", complete: true); + final todo3 = new Todo("Uncertain"); + + store.dispatch(new AddTodoAction(todo1)); + store.dispatch(new AddTodoAction(todo2)); + store.dispatch(new AddTodoAction(todo3)); + store.dispatch(new ToggleAllAction(true)); + expect(todosSelector(store.state).length, 3); + expect(numActiveSelector(todosSelector(store.state)), 3); + expect(numCompletedSelector(todosSelector(store.state)), 0); + }); + + test( + 'UpdateTodoAction: ' + 'should have 3 todos, 1 active and 2 completed', () { + final firestoreServices = new FirestoreServices(); + final store = new Store( + appReducer, + initialState: new AppState.loading(), + middleware: createStoreTodosMiddleware(firestoreServices), + ); + final todo1 = new Todo("Hallo", id: '1'); + final todo2 = new Todo("Bye", complete: true); + final todo3 = new Todo("Uncertain"); + + store.dispatch(new AddTodoAction(todo1)); + store.dispatch(new AddTodoAction(todo2)); + store.dispatch(new AddTodoAction(todo3)); + + store.dispatch(new UpdateTodoAction( + '1', new Todo("Hallo & Welcome", complete: true, id: '1'))); + expect(todosSelector(store.state).length, 3); + expect(numActiveSelector(todosSelector(store.state)), 1); + expect(numCompletedSelector(todosSelector(store.state)), 2); + }); + + test( + 'DeleteTodoAction: ' + 'should have 2 todos, 1 active and 1 completed', () { + final firestoreServices = new FirestoreServices(); + final store = new Store( + appReducer, + initialState: new AppState.loading(), + middleware: createStoreTodosMiddleware(firestoreServices), + ); + final todo1 = new Todo("Hallo", id: '1'); + final todo2 = new Todo("Bye", complete: true); + final todo3 = new Todo("Uncertain"); + + store.dispatch(new AddTodoAction(todo1)); + store.dispatch(new AddTodoAction(todo2)); + store.dispatch(new AddTodoAction(todo3)); + + store.dispatch(new DeleteTodoAction('1')); + expect(todosSelector(store.state).length, 2); + expect(numActiveSelector(todosSelector(store.state)), 1); + expect(numCompletedSelector(todosSelector(store.state)), 1); + }); + }); +} diff --git a/example/firestore_redux/test/reducer_test.dart b/example/firestore_redux/test/reducer_test.dart new file mode 100644 index 00000000..abedd31e --- /dev/null +++ b/example/firestore_redux/test/reducer_test.dart @@ -0,0 +1,57 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:redux/redux.dart'; +import 'package:fire_redux_sample/models/models.dart'; +import 'package:fire_redux_sample/actions/actions.dart'; +import 'package:fire_redux_sample/reducers/app_state_reducer.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fire_redux_sample/selectors/selectors.dart'; + +main() { + group('State Reducer', () { + test('should load todos into store', () { + final todo1 = new Todo('a'); + final todo2 = new Todo('b'); + final todo3 = new Todo('c', complete: true); + final todos = [ + todo1, + todo2, + todo3, + ]; + final store = new Store( + appReducer, + initialState: new AppState.loading(), + ); + + expect(todosSelector(store.state), []); + + store.dispatch(new LoadTodosAction(todos)); + + expect(todosSelector(store.state), todos); + }); + + test('should update the VisibilityFilter', () { + final store = new Store( + appReducer, + initialState: new AppState(activeFilter: VisibilityFilter.active), + ); + + store.dispatch(new UpdateFilterAction(VisibilityFilter.completed)); + + expect(store.state.activeFilter, VisibilityFilter.completed); + }); + + test('should update the AppTab', () { + final store = new Store( + appReducer, + initialState: new AppState(activeTab: AppTab.todos), + ); + + store.dispatch(new UpdateTabAction(AppTab.stats)); + + expect(store.state.activeTab, AppTab.stats); + }); + }); +} diff --git a/example/firestore_redux/test/selectors_test.dart b/example/firestore_redux/test/selectors_test.dart new file mode 100644 index 00000000..cd12e3ad --- /dev/null +++ b/example/firestore_redux/test/selectors_test.dart @@ -0,0 +1,98 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:quiver/core.dart'; +import 'package:fire_redux_sample/models/models.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fire_redux_sample/selectors/selectors.dart'; + +main() { + group('Selectors', () { + test('should calculate the number of active todos', () { + final todos = [ + new Todo('a'), + new Todo('b'), + new Todo('c', complete: true), + ]; + + expect(numActiveSelector(todos), 2); + }); + + test('should calculate the number of completed todos', () { + final todos = [ + new Todo('a'), + new Todo('b'), + new Todo('c', complete: true), + ]; + + expect(numCompletedSelector(todos), 1); + }); + + test('should return all todos if the VisibilityFilter is all', () { + final todos = [ + new Todo('a'), + new Todo('b'), + new Todo('c', complete: true), + ]; + + expect(filteredTodosSelector(todos, VisibilityFilter.all), todos); + }); + + test('should return active todos if the VisibilityFilter is active', () { + final todo1 = new Todo('a'); + final todo2 = new Todo('b'); + final todo3 = new Todo('c', complete: true); + final todos = [ + todo1, + todo2, + todo3, + ]; + + expect(filteredTodosSelector(todos, VisibilityFilter.active), [ + todo1, + todo2, + ]); + }); + + test('should return completed todos if the VisibilityFilter is completed', + () { + final todo1 = new Todo('a'); + final todo2 = new Todo('b'); + final todo3 = new Todo('c', complete: true); + final todos = [ + todo1, + todo2, + todo3, + ]; + + expect(filteredTodosSelector(todos, VisibilityFilter.completed), [todo3]); + }); + + test('should return an Optional todo based on id', () { + final todo1 = new Todo('a', id: "1"); + final todo2 = new Todo('b'); + final todo3 = new Todo('c', complete: true); + final todos = [ + todo1, + todo2, + todo3, + ]; + + expect(todoSelector(todos, "1"), new Optional.of(todo1)); + }); + + test('should return an absent Optional if the id is not found', () { + final todo1 = new Todo('a', id: "1"); + final todo2 = new Todo('b'); + final todo3 = new Todo('c', complete: true); + final todos = [ + todo1, + todo2, + todo3, + ]; + + expect(todoSelector(todos, "2"), new Optional.absent()); + }); + }); +} diff --git a/example/firestore_redux/test_driver/todo_app.dart b/example/firestore_redux/test_driver/todo_app.dart new file mode 100644 index 00000000..da9d98b7 --- /dev/null +++ b/example/firestore_redux/test_driver/todo_app.dart @@ -0,0 +1,12 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:flutter_driver/driver_extension.dart'; +import '../test/main_fire_4test.dart' as app; + +void main() { + enableFlutterDriverExtension(); + + app.main(); +} diff --git a/example/firestore_redux/test_driver/todo_app_test.dart b/example/firestore_redux/test_driver/todo_app_test.dart new file mode 100644 index 00000000..14ddb566 --- /dev/null +++ b/example/firestore_redux/test_driver/todo_app_test.dart @@ -0,0 +1,9 @@ +// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be found +// in the LICENSE file. + +import 'package:integration_tests/integration_tests.dart' as integrationTests; + +main() { + integrationTests.main(); +} diff --git a/index.html b/index.html index 38eb8d7b..8f2e8c39 100644 --- a/index.html +++ b/index.html @@ -191,6 +191,9 @@

Samples

  • scoped_model
  • +
  • + Redux and Firestore +