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
+