├── .gitignore ├── .metadata ├── README.md ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── hillelcoren │ │ │ └── flutterreduxstarter │ │ │ └── MainActivity.java │ │ └── res │ │ ├── drawable │ │ └── launch_background.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle ├── flutter_redux_starter.iml ├── flutter_redux_starter_android.iml ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ └── contents.xcworkspacedata └── Runner │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── main.m ├── lib ├── constants.dart ├── data │ ├── file_storage.dart │ ├── models │ │ ├── models.dart │ │ ├── models.g.dart │ │ ├── serializers.dart │ │ └── serializers.g.dart │ ├── repositories │ │ ├── auth_repository.dart │ │ └── persistence_repository.dart │ └── web_client.dart ├── keys.dart ├── main.dart ├── redux │ ├── app │ │ ├── app_actions.dart │ │ ├── app_middleware.dart │ │ ├── app_reducer.dart │ │ ├── app_state.dart │ │ ├── app_state.g.dart │ │ ├── data_reducer.dart │ │ ├── data_state.dart │ │ └── data_state.g.dart │ ├── auth │ │ ├── auth_actions.dart │ │ ├── auth_middleware.dart │ │ ├── auth_reducer.dart │ │ ├── auth_state.dart │ │ └── auth_state.g.dart │ └── ui │ │ ├── entity_ui_state.dart │ │ ├── list_ui_state.dart │ │ ├── list_ui_state.g.dart │ │ ├── ui_actions.dart │ │ ├── ui_reducer.dart │ │ ├── ui_state.dart │ │ └── ui_state.g.dart └── ui │ ├── app │ ├── actions_menu_button.dart │ ├── app_bottom_bar.dart │ ├── app_drawer.dart │ ├── app_drawer_vm.dart │ ├── app_search.dart │ ├── app_search_button.dart │ ├── dismissible_entity.dart │ ├── form_card.dart │ ├── icon_message.dart │ ├── init.dart │ └── save_icon_button.dart │ ├── auth │ ├── login.dart │ └── login_vm.dart │ └── home │ └── home_screen.dart ├── pubspec.lock ├── pubspec.yaml ├── starter.sh ├── stubs ├── data │ ├── models │ │ └── stub_model │ └── repositories │ │ └── stub_repository ├── redux │ └── stub │ │ ├── stub_actions │ │ ├── stub_middleware │ │ ├── stub_reducer │ │ ├── stub_selectors │ │ └── stub_state └── ui │ └── stub │ ├── edit │ ├── stub_edit │ └── stub_edit_vm │ ├── stub_item │ ├── stub_list │ ├── stub_list_vm │ ├── stub_screen │ └── view │ ├── stub_view │ └── stub_view_vm └── test ├── login_test.dart └── widget_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | .vscode/ 7 | .idea/ 8 | 9 | build/ 10 | 11 | .flutter-plugins 12 | .env.dart -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: c7ea3ca377e909469c68f2ab878a5bc53d3cf66b 8 | channel: dev 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter Redux Starter/Code Generator 2 | 3 |

4 | Flutter Redux Starter 5 |

6 | 7 | ## Videos 8 | 9 | - [Short video](https://www.youtube.com/watch?v=pMSokKmwp1U) ~ 1 minute 10 | - [Long video](https://www.youtube.com/watch?v=RgV5xesTgDA) ~ 10 minutes 11 | 12 | We're using this approach to develop the [Flutter app](https://github.com/invoiceninja/flutter-mobile/) for [Invoice Ninja](https://www.invoiceninja.com). 13 | 14 | ## Usage 15 | 16 | #### Step 1: Clone the GitHub repo 17 | 18 | `git clone git@github.com:hillelcoren/flutter-redux-starter.git && cd ` 19 | 20 | #### Step 2: Initialize the project 21 | 22 | `./starter.sh init ` 23 | 24 | #### Step 3: Create the module 25 | 26 | `./starter.sh make ` 27 | 28 | For example: 29 | 30 | ``` 31 | git clone git@github.com:hillelcoren/flutter-redux-starter.git hacker_news && cd hacker_news 32 | ./starter.sh init hacker_news articles api.hackerwebapp.com 33 | ./starter.sh make articles article title,url 34 | # Change the route on line 20 of lib/data/repositories/article_repository.dart from /articles to /news 35 | flutter run 36 | ``` 37 | 38 | Note: on macOS sed leaves behind backup files ending with `-e`, you can use this command to delete the files: 39 | 40 | `find . -name "*-e" -type f -delete` 41 | 42 | ## Features 43 | 44 | - Supports large Redux stores by persisting parts separately 45 | - App state (including navigation) is persisted on form changes 46 | - Automatically implements support for sorting and searching 47 | - The account email is also backed up in shared preferences 48 | - All state and models classes are created using built_values 49 | 50 | ## Included Packages 51 | 52 | - [flutter_redux](https://pub.dartlang.org/packages/flutter_redux) - Consume a Redux Store to build Flutter Widgets 53 | - [redux_logging](https://pub.dartlang.org/packages/redux_logging) - Print the latest state\action changes 54 | - [path_provider](https://pub.dartlang.org/packages/path_provider) - Find commonly used locations on the filesystem 55 | - [shared_preferences ](https://pub.dartlang.org/packages/shared_preferences) - Provides a persistent store for simple data 56 | - [build_runner](https://pub.dartlang.org/packages/build_runner) - A concrete way of generating files using Dart code 57 | - [built_value](https://pub.dartlang.org/packages/built_value) - Built Values for Dart 58 | - [built_collection](https://pub.dartlang.org/packages/built_collection) - Built Collections for Dart 59 | - [memoize](https://pub.dartlang.org/packages/memoize) - Cache results of function calls 60 | 61 | ## Application Architecture 62 | 63 | The architecture is based off these two projects: 64 | 65 | - [Redux Sample](https://github.com/brianegan/flutter_architecture_samples/tree/master/example/redux) - [Brian Egan](https://twitter.com/brianegan) 66 | - [inKino](https://github.com/roughike/inKino) - [Iiro Krankka](https://twitter.com/koorankka) 67 | 68 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.class 3 | .gradle 4 | /local.properties 5 | /.idea/workspace.xml 6 | /.idea/libraries 7 | .DS_Store 8 | /build 9 | /captures 10 | GeneratedPluginRegistrant.java 11 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | apply plugin: 'com.android.application' 15 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 16 | 17 | android { 18 | compileSdkVersion 27 19 | 20 | lintOptions { 21 | disable 'InvalidPackage' 22 | } 23 | 24 | defaultConfig { 25 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 26 | applicationId "com.hillelcoren.flutterreduxstarter" 27 | minSdkVersion 16 28 | targetSdkVersion 27 29 | versionCode 1 30 | versionName "1.0" 31 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 32 | } 33 | 34 | buildTypes { 35 | release { 36 | // TODO: Add your own signing config for the release build. 37 | // Signing with the debug keys for now, so `flutter run --release` works. 38 | signingConfig signingConfigs.debug 39 | } 40 | } 41 | } 42 | 43 | flutter { 44 | source '../..' 45 | } 46 | 47 | dependencies { 48 | testImplementation 'junit:junit:4.12' 49 | androidTestImplementation 'com.android.support.test:runner:1.0.1' 50 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' 51 | } 52 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 15 | 19 | 26 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hillelcoren/flutterreduxstarter/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.hillelcoren.flutterreduxstarter; 2 | 3 | import android.os.Bundle; 4 | import io.flutter.app.FlutterActivity; 5 | import io.flutter.plugins.GeneratedPluginRegistrant; 6 | 7 | public class MainActivity extends FlutterActivity { 8 | @Override 9 | protected void onCreate(Bundle savedInstanceState) { 10 | super.onCreate(savedInstanceState); 11 | GeneratedPluginRegistrant.registerWith(this); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillelcoren/flutter-redux-starter/a55685f6afc87454796f7600a511851e0584f478/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillelcoren/flutter-redux-starter/a55685f6afc87454796f7600a511851e0584f478/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillelcoren/flutter-redux-starter/a55685f6afc87454796f7600a511851e0584f478/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillelcoren/flutter-redux-starter/a55685f6afc87454796f7600a511851e0584f478/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillelcoren/flutter-redux-starter/a55685f6afc87454796f7600a511851e0584f478/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.2.1' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillelcoren/flutter-redux-starter/a55685f6afc87454796f7600a511851e0584f478/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip 7 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /flutter_redux_starter.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /flutter_redux_starter_android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/app.flx 37 | /Flutter/app.zip 38 | /Flutter/flutter_assets/ 39 | /Flutter/App.framework 40 | /Flutter/Flutter.framework 41 | /Flutter/Generated.xcconfig 42 | /ServiceDefinitions.json 43 | 44 | Pods/ 45 | .symlinks/ 46 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application 7 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 8 | [GeneratedPluginRegistrant registerWithRegistry:self]; 9 | // Override point for customization after application launch. 10 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillelcoren/flutter-redux-starter/a55685f6afc87454796f7600a511851e0584f478/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillelcoren/flutter-redux-starter/a55685f6afc87454796f7600a511851e0584f478/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillelcoren/flutter-redux-starter/a55685f6afc87454796f7600a511851e0584f478/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillelcoren/flutter-redux-starter/a55685f6afc87454796f7600a511851e0584f478/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillelcoren/flutter-redux-starter/a55685f6afc87454796f7600a511851e0584f478/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillelcoren/flutter-redux-starter/a55685f6afc87454796f7600a511851e0584f478/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillelcoren/flutter-redux-starter/a55685f6afc87454796f7600a511851e0584f478/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillelcoren/flutter-redux-starter/a55685f6afc87454796f7600a511851e0584f478/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillelcoren/flutter-redux-starter/a55685f6afc87454796f7600a511851e0584f478/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillelcoren/flutter-redux-starter/a55685f6afc87454796f7600a511851e0584f478/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillelcoren/flutter-redux-starter/a55685f6afc87454796f7600a511851e0584f478/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillelcoren/flutter-redux-starter/a55685f6afc87454796f7600a511851e0584f478/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillelcoren/flutter-redux-starter/a55685f6afc87454796f7600a511851e0584f478/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillelcoren/flutter-redux-starter/a55685f6afc87454796f7600a511851e0584f478/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillelcoren/flutter-redux-starter/a55685f6afc87454796f7600a511851e0584f478/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillelcoren/flutter-redux-starter/a55685f6afc87454796f7600a511851e0584f478/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillelcoren/flutter-redux-starter/a55685f6afc87454796f7600a511851e0584f478/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillelcoren/flutter-redux-starter/a55685f6afc87454796f7600a511851e0584f478/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | flutter_redux_starter 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/constants.dart: -------------------------------------------------------------------------------- 1 | const String kApiUrl = 'https://__API_URL__'; 2 | 3 | const int kMillisecondsToRefreshData = 1000 * 60 * 15; // 15 minutes -------------------------------------------------------------------------------- /lib/data/file_storage.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | class FileStorage { 5 | final String tag; 6 | final Future Function() getDirectory; 7 | 8 | const FileStorage( 9 | this.tag, 10 | this.getDirectory, 11 | ); 12 | 13 | Future _getLocalFile() async { 14 | final dir = await getDirectory(); 15 | 16 | return File('${dir.path}/$tag.json'); 17 | } 18 | 19 | Future load() async { 20 | final file = await _getLocalFile(); 21 | final contents = await file.readAsString(); 22 | 23 | return contents; 24 | } 25 | 26 | Future save(String data) async { 27 | final file = await _getLocalFile(); 28 | 29 | return file.writeAsString(data); 30 | } 31 | 32 | Future delete() async { 33 | final file = await _getLocalFile(); 34 | 35 | return file.delete(); 36 | } 37 | 38 | Future exisits() async { 39 | final file = await _getLocalFile(); 40 | 41 | return file.exists(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/data/models/models.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_collection/built_collection.dart'; 2 | import 'package:built_value/built_value.dart'; 3 | import 'package:built_value/serializer.dart'; 4 | 5 | part 'models.g.dart'; 6 | 7 | class EntityType extends EnumClass { 8 | static Serializer get serializer => _$entityTypeSerializer; 9 | 10 | // STARTER: types - do not remove comment 11 | 12 | const EntityType._(String name) : super(name); 13 | 14 | String get plural { 15 | // TODO improve implementation 16 | return this.toString() + 's'; 17 | } 18 | 19 | static BuiltSet get values => _$typeValues; 20 | static EntityType valueOf(String name) => _$typeValueOf(name); 21 | } 22 | 23 | class EntityAction extends EnumClass { 24 | static Serializer get serializer => _$entityActionSerializer; 25 | 26 | static const EntityAction delete = _$delete; 27 | 28 | const EntityAction._(String name) : super(name); 29 | 30 | static BuiltSet get values => _$values; 31 | static EntityAction valueOf(String name) => _$valueOf(name); 32 | } 33 | 34 | abstract class BaseEntity { 35 | 36 | int get id; 37 | 38 | String get listDisplayName { 39 | return 'Error: not set'; 40 | } 41 | 42 | bool matchesSearch(String search) { 43 | return true; 44 | } 45 | 46 | String matchesSearchField(String search) { 47 | return null; 48 | } 49 | 50 | String matchesSearchValue(String search) { 51 | return null; 52 | } 53 | 54 | bool get isNew { 55 | return this.id == null || this.id <= 0; 56 | } 57 | 58 | bool get isActive { 59 | return true; 60 | } 61 | 62 | bool get isDeleted { 63 | return false; 64 | } 65 | 66 | } 67 | 68 | 69 | abstract class LoginResponse implements Built { 70 | 71 | BuiltList get data; 72 | 73 | LoginResponse._(); 74 | factory LoginResponse([updates(LoginResponseBuilder b)]) = _$LoginResponse; 75 | static Serializer get serializer => _$loginResponseSerializer; 76 | } 77 | -------------------------------------------------------------------------------- /lib/data/models/models.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'models.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: always_put_control_body_on_new_line 10 | // ignore_for_file: annotate_overrides 11 | // ignore_for_file: avoid_annotating_with_dynamic 12 | // ignore_for_file: avoid_returning_this 13 | // ignore_for_file: omit_local_variable_types 14 | // ignore_for_file: prefer_expression_function_bodies 15 | // ignore_for_file: sort_constructors_first 16 | 17 | EntityType _$typeValueOf(String name) { 18 | switch (name) { 19 | default: 20 | throw new ArgumentError(name); 21 | } 22 | } 23 | 24 | final BuiltSet _$typeValues = 25 | new BuiltSet(const []); 26 | 27 | const EntityAction _$delete = const EntityAction._('delete'); 28 | 29 | EntityAction _$valueOf(String name) { 30 | switch (name) { 31 | case 'delete': 32 | return _$delete; 33 | default: 34 | throw new ArgumentError(name); 35 | } 36 | } 37 | 38 | final BuiltSet _$values = 39 | new BuiltSet(const [ 40 | _$delete, 41 | ]); 42 | 43 | Serializer _$entityTypeSerializer = new _$EntityTypeSerializer(); 44 | Serializer _$entityActionSerializer = 45 | new _$EntityActionSerializer(); 46 | Serializer _$loginResponseSerializer = 47 | new _$LoginResponseSerializer(); 48 | 49 | class _$EntityTypeSerializer implements PrimitiveSerializer { 50 | @override 51 | final Iterable types = const [EntityType]; 52 | @override 53 | final String wireName = 'EntityType'; 54 | 55 | @override 56 | Object serialize(Serializers serializers, EntityType object, 57 | {FullType specifiedType: FullType.unspecified}) => 58 | object.name; 59 | 60 | @override 61 | EntityType deserialize(Serializers serializers, Object serialized, 62 | {FullType specifiedType: FullType.unspecified}) => 63 | EntityType.valueOf(serialized as String); 64 | } 65 | 66 | class _$EntityActionSerializer implements PrimitiveSerializer { 67 | @override 68 | final Iterable types = const [EntityAction]; 69 | @override 70 | final String wireName = 'EntityAction'; 71 | 72 | @override 73 | Object serialize(Serializers serializers, EntityAction object, 74 | {FullType specifiedType: FullType.unspecified}) => 75 | object.name; 76 | 77 | @override 78 | EntityAction deserialize(Serializers serializers, Object serialized, 79 | {FullType specifiedType: FullType.unspecified}) => 80 | EntityAction.valueOf(serialized as String); 81 | } 82 | 83 | class _$LoginResponseSerializer implements StructuredSerializer { 84 | @override 85 | final Iterable types = const [LoginResponse, _$LoginResponse]; 86 | @override 87 | final String wireName = 'LoginResponse'; 88 | 89 | @override 90 | Iterable serialize(Serializers serializers, LoginResponse object, 91 | {FullType specifiedType: FullType.unspecified}) { 92 | final result = [ 93 | 'data', 94 | serializers.serialize(object.data, 95 | specifiedType: const FullType(BuiltList)), 96 | ]; 97 | 98 | return result; 99 | } 100 | 101 | @override 102 | LoginResponse deserialize(Serializers serializers, Iterable serialized, 103 | {FullType specifiedType: FullType.unspecified}) { 104 | final result = new LoginResponseBuilder(); 105 | 106 | final iterator = serialized.iterator; 107 | while (iterator.moveNext()) { 108 | final key = iterator.current as String; 109 | iterator.moveNext(); 110 | final dynamic value = iterator.current; 111 | switch (key) { 112 | case 'data': 113 | result.data = serializers.deserialize(value, 114 | specifiedType: const FullType(BuiltList)) as BuiltList; 115 | break; 116 | } 117 | } 118 | 119 | return result.build(); 120 | } 121 | } 122 | 123 | class _$LoginResponse extends LoginResponse { 124 | @override 125 | final BuiltList data; 126 | 127 | factory _$LoginResponse([void updates(LoginResponseBuilder b)]) => 128 | (new LoginResponseBuilder()..update(updates)).build(); 129 | 130 | _$LoginResponse._({this.data}) : super._() { 131 | if (data == null) 132 | throw new BuiltValueNullFieldError('LoginResponse', 'data'); 133 | } 134 | 135 | @override 136 | LoginResponse rebuild(void updates(LoginResponseBuilder b)) => 137 | (toBuilder()..update(updates)).build(); 138 | 139 | @override 140 | LoginResponseBuilder toBuilder() => new LoginResponseBuilder()..replace(this); 141 | 142 | @override 143 | bool operator ==(dynamic other) { 144 | if (identical(other, this)) return true; 145 | if (other is! LoginResponse) return false; 146 | return data == other.data; 147 | } 148 | 149 | @override 150 | int get hashCode { 151 | return $jf($jc(0, data.hashCode)); 152 | } 153 | 154 | @override 155 | String toString() { 156 | return (newBuiltValueToStringHelper('LoginResponse')..add('data', data)) 157 | .toString(); 158 | } 159 | } 160 | 161 | class LoginResponseBuilder 162 | implements Builder { 163 | _$LoginResponse _$v; 164 | 165 | BuiltList _data; 166 | BuiltList get data => _$this._data; 167 | set data(BuiltList data) => _$this._data = data; 168 | 169 | LoginResponseBuilder(); 170 | 171 | LoginResponseBuilder get _$this { 172 | if (_$v != null) { 173 | _data = _$v.data; 174 | _$v = null; 175 | } 176 | return this; 177 | } 178 | 179 | @override 180 | void replace(LoginResponse other) { 181 | if (other == null) throw new ArgumentError.notNull('other'); 182 | _$v = other as _$LoginResponse; 183 | } 184 | 185 | @override 186 | void update(void updates(LoginResponseBuilder b)) { 187 | if (updates != null) updates(this); 188 | } 189 | 190 | @override 191 | _$LoginResponse build() { 192 | final _$result = _$v ?? new _$LoginResponse._(data: data); 193 | replace(_$result); 194 | return _$result; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /lib/data/models/serializers.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/serializer.dart'; 2 | import 'package:built_value/standard_json_plugin.dart'; 3 | import 'package:flutter_redux_starter/data/models/models.dart'; 4 | import 'package:flutter_redux_starter/redux/app/app_state.dart'; 5 | import 'package:flutter_redux_starter/redux/app/data_state.dart'; 6 | import 'package:flutter_redux_starter/redux/auth/auth_state.dart'; 7 | import 'package:flutter_redux_starter/redux/ui/ui_state.dart'; 8 | import 'package:built_collection/built_collection.dart'; 9 | import 'package:flutter_redux_starter/redux/ui/list_ui_state.dart'; 10 | // STARTER: import - do not remove comment 11 | 12 | part 'serializers.g.dart'; 13 | 14 | @SerializersFor(const [ 15 | AppState, 16 | LoginResponse, 17 | // STARTER: serializers - do not remove comment 18 | ]) 19 | final Serializers serializers = 20 | (_$serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build(); -------------------------------------------------------------------------------- /lib/data/models/serializers.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'serializers.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: always_put_control_body_on_new_line 10 | // ignore_for_file: annotate_overrides 11 | // ignore_for_file: avoid_annotating_with_dynamic 12 | // ignore_for_file: avoid_returning_this 13 | // ignore_for_file: omit_local_variable_types 14 | // ignore_for_file: prefer_expression_function_bodies 15 | // ignore_for_file: sort_constructors_first 16 | 17 | Serializers _$serializers = (new Serializers().toBuilder() 18 | ..add(AppState.serializer) 19 | ..add(AuthState.serializer) 20 | ..add(DataState.serializer) 21 | ..add(LoginResponse.serializer) 22 | ..add(UIState.serializer)) 23 | .build(); 24 | -------------------------------------------------------------------------------- /lib/data/repositories/auth_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:core'; 3 | import 'dart:convert'; 4 | import 'package:built_collection/built_collection.dart'; 5 | import 'package:flutter_redux_starter/data/models/models.dart'; 6 | import 'package:flutter_redux_starter/data/models/serializers.dart'; 7 | import 'package:flutter_redux_starter/data/web_client.dart'; 8 | 9 | class AuthRepository { 10 | final WebClient webClient; 11 | 12 | const AuthRepository({ 13 | this.webClient = const WebClient(), 14 | }); 15 | 16 | Future> login(String email, String password) async { 17 | 18 | final credentials = { 19 | 'email': email, 20 | 'password': password, 21 | }; 22 | 23 | var url = 'https://example.com/login'; 24 | 25 | final response = await webClient.post(url, json.encode(credentials)); 26 | 27 | LoginResponse loginResponse = serializers.deserializeWith( 28 | LoginResponse.serializer, response); 29 | 30 | return loginResponse.data.toBuiltList(); 31 | } 32 | } -------------------------------------------------------------------------------- /lib/data/repositories/persistence_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:core'; 4 | import 'dart:io'; 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:flutter_redux_starter/data/file_storage.dart'; 7 | import 'package:flutter_redux_starter/data/models/serializers.dart'; 8 | import 'package:flutter_redux_starter/redux/app/data_state.dart'; 9 | import 'package:flutter_redux_starter/redux/auth/auth_state.dart'; 10 | import 'package:flutter_redux_starter/redux/ui/ui_state.dart'; 11 | import 'package:meta/meta.dart'; 12 | 13 | class PersistenceRepository { 14 | final FileStorage fileStorage; 15 | 16 | const PersistenceRepository({ 17 | @required this.fileStorage, 18 | }); 19 | 20 | Future saveAuthState(AuthState state) async { 21 | var data = serializers.serializeWith(AuthState.serializer, state); 22 | return await fileStorage.save(json.encode(data)); 23 | } 24 | 25 | Future loadAuthState() async { 26 | String data = await fileStorage.load(); 27 | return serializers.deserializeWith(AuthState.serializer, json.decode(data)); 28 | } 29 | 30 | Future saveUIState(UIState state) async { 31 | var data = serializers.serializeWith(UIState.serializer, state); 32 | return await fileStorage.save(json.encode(data)); 33 | } 34 | 35 | Future loadUIState() async { 36 | String data = await fileStorage.load(); 37 | return serializers.deserializeWith(UIState.serializer, json.decode(data)); 38 | } 39 | 40 | Future saveDataState(DataState state) async { 41 | var data = serializers.serializeWith(DataState.serializer, state); 42 | return await fileStorage.save(json.encode(data)); 43 | } 44 | 45 | Future loadDataState() async { 46 | String data = await fileStorage.load(); 47 | return serializers.deserializeWith(DataState.serializer, json.decode(data)); 48 | } 49 | 50 | Future delete() async { 51 | return await fileStorage.exisits().then((exists) => exists ? fileStorage.delete() : null); 52 | } 53 | 54 | Future exists() async { 55 | return await fileStorage.exisits(); 56 | } 57 | } -------------------------------------------------------------------------------- /lib/data/web_client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:core'; 3 | import 'dart:convert'; 4 | import 'package:http/http.dart' as http; 5 | 6 | class WebClient { 7 | const WebClient(); 8 | 9 | Future get(String url) async { 10 | final http.Response response = await http.Client().get( 11 | url, 12 | ); 13 | 14 | if (response.statusCode >= 400) { 15 | throw ('An error occurred: ' + response.body); 16 | } 17 | 18 | return json.decode(response.body); 19 | } 20 | 21 | Future post(String url, dynamic data) async { 22 | final http.Response response = await http.Client().post( 23 | url, 24 | body: data, 25 | headers: { 26 | 'Content-Type': 'application/json', 27 | }, 28 | ); 29 | 30 | if (response.statusCode >= 400) { 31 | throw ('An error occurred: ' + response.body); 32 | } 33 | 34 | try { 35 | final jsonResponse = json.decode(response.body); 36 | return jsonResponse; 37 | } catch (exception) { 38 | print(response.body); 39 | throw ('An error occurred'); 40 | } 41 | } 42 | 43 | Future put(String url, dynamic data) async { 44 | final http.Response response = await http.Client().put( 45 | url, 46 | body: data, 47 | headers: { 48 | 'Content-Type': 'application/json', 49 | }, 50 | ); 51 | 52 | if (response.statusCode >= 400) { 53 | throw ('An error occurred: ' + response.body); 54 | } 55 | 56 | try { 57 | final jsonResponse = json.decode(response.body); 58 | return jsonResponse; 59 | } catch (exception) { 60 | print(response.body); 61 | throw ('An error occurred'); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/keys.dart: -------------------------------------------------------------------------------- 1 | // Use strings keys to support integrations tests otherwise you'll see this error 2 | // "The built-in library 'dart:ui' is not available on the stand-alone VM." 3 | // http://cogitas.net/write-integration-test-flutter/ 4 | 5 | class LoginKeys { 6 | static final String emailKey = '__login__email__'; 7 | static final String passwordKey = '__login__password__'; 8 | } -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:redux/redux.dart'; 2 | import 'package:redux_logging/redux_logging.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_redux/flutter_redux.dart'; 5 | import 'package:flutter_redux_starter/redux/app/app_middleware.dart'; 6 | import 'package:flutter_redux_starter/redux/app/app_state.dart'; 7 | import 'package:flutter_redux_starter/redux/app/app_reducer.dart'; 8 | import 'package:flutter_redux_starter/redux/auth/auth_middleware.dart'; 9 | import 'package:flutter_redux_starter/ui/app/init.dart'; 10 | import 'package:flutter_redux_starter/ui/auth/login_vm.dart'; 11 | import 'package:flutter_redux_starter/ui/home/home_screen.dart'; 12 | // STARTER: import - do not remove comment 13 | 14 | void main() { 15 | final store = Store(appReducer, 16 | initialState: AppState(), 17 | middleware: [] 18 | ..addAll(createStoreAuthMiddleware()) 19 | ..addAll(createStorePersistenceMiddleware()) 20 | // STARTER: middleware - do not remove comment 21 | ..addAll([ 22 | LoggingMiddleware.printer(), 23 | ])); 24 | 25 | runApp(SampleReduxApp(store: store)); 26 | } 27 | 28 | class SampleReduxApp extends StatefulWidget { 29 | final Store store; 30 | 31 | SampleReduxApp({Key key, this.store}) : super(key: key); 32 | 33 | @override 34 | _SampleReduxAppState createState() => _SampleReduxAppState(); 35 | } 36 | 37 | class _SampleReduxAppState extends State { 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | return StoreProvider( 42 | store: widget.store, 43 | child: MaterialApp( 44 | title: 'Sample App', 45 | routes: { 46 | InitScreen.route: (context) => InitScreen(), 47 | LoginScreen.route: (context) => LoginScreen(), 48 | HomeScreen.route: (context) => HomeScreen(), 49 | // STARTER: routes - do not remove comment 50 | }, 51 | ), 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/redux/app/app_actions.dart: -------------------------------------------------------------------------------- 1 | class PersistData {} 2 | class PersistUI {} 3 | 4 | class StartLoading {} 5 | class StopLoading {} -------------------------------------------------------------------------------- /lib/redux/app/app_middleware.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_redux_starter/data/file_storage.dart'; 3 | import 'package:flutter_redux_starter/data/models/models.dart'; 4 | import 'package:flutter_redux_starter/data/repositories/persistence_repository.dart'; 5 | import 'package:flutter_redux_starter/redux/app/app_actions.dart'; 6 | import 'package:flutter_redux_starter/redux/app/app_state.dart'; 7 | import 'package:flutter_redux_starter/redux/app/data_state.dart'; 8 | import 'package:flutter_redux_starter/redux/auth/auth_actions.dart'; 9 | import 'package:flutter_redux_starter/redux/auth/auth_state.dart'; 10 | import 'package:flutter_redux_starter/redux/ui/ui_state.dart'; 11 | import 'package:flutter_redux_starter/ui/auth/login_vm.dart'; 12 | import 'package:flutter_redux_starter/ui/home/home_screen.dart'; 13 | import 'package:redux/redux.dart'; 14 | import 'package:path_provider/path_provider.dart'; 15 | 16 | List> createStorePersistenceMiddleware([ 17 | PersistenceRepository authRepository = const PersistenceRepository( 18 | fileStorage: const FileStorage( 19 | 'auth_state', 20 | getApplicationDocumentsDirectory, 21 | ), 22 | ), 23 | PersistenceRepository uiRepository = const PersistenceRepository( 24 | fileStorage: const FileStorage( 25 | 'ui_state', 26 | getApplicationDocumentsDirectory, 27 | ), 28 | ), 29 | PersistenceRepository dataRepository = const PersistenceRepository( 30 | fileStorage: const FileStorage( 31 | 'data_state', 32 | getApplicationDocumentsDirectory, 33 | ), 34 | ), 35 | ]) { 36 | final loadState = _createLoadState( 37 | authRepository, 38 | uiRepository, 39 | dataRepository); 40 | final userLoggedIn = _createUserLoggedIn( 41 | authRepository, 42 | uiRepository); 43 | final uiChange = _createUIChange(uiRepository); 44 | final dataChange = _createDataChange(dataRepository); 45 | final deleteState = _createDeleteState( 46 | authRepository, 47 | uiRepository); 48 | 49 | return [ 50 | TypedMiddleware(deleteState), 51 | TypedMiddleware(loadState), 52 | TypedMiddleware(userLoggedIn), 53 | TypedMiddleware(uiChange), 54 | TypedMiddleware(dataChange), 55 | ]; 56 | } 57 | 58 | Middleware _createLoadState( 59 | PersistenceRepository authRepository, 60 | PersistenceRepository uiRepository, 61 | PersistenceRepository dataRepository, 62 | ) { 63 | AuthState authState; 64 | UIState uiState; 65 | DataState dataState; 66 | 67 | return (Store store, action, NextDispatcher next) { 68 | 69 | // TODO passing back future/single catchError 70 | authRepository.exists().then((exists) { 71 | if (exists) { 72 | authRepository.loadAuthState().then((state) { 73 | authState = state; 74 | uiRepository.loadUIState().then((state) { 75 | uiState = state; 76 | dataRepository.loadDataState().then((state) { 77 | dataState = state; 78 | AppState appState = AppState().rebuild((b) => b 79 | ..authState.replace(authState) 80 | ..uiState.replace(uiState) 81 | ..dataState.replace(dataState)); 82 | store.dispatch(LoadStateSuccess(appState)); 83 | if (uiState.currentRoute != LoginScreen.route) { 84 | NavigatorState navigator = Navigator.of(action.context); 85 | bool isFirst = true; 86 | _getRoutes(appState).forEach((route) { 87 | if (isFirst) { 88 | navigator.pushReplacementNamed(route); 89 | } else { 90 | navigator.pushNamed(route); 91 | } 92 | isFirst = false; 93 | }); 94 | } 95 | }).catchError((error) => _handleError(store, error, action.context)); 96 | }).catchError((error) => _handleError(store, error, action.context)); 97 | }).catchError((error) => _handleError(store, error, action.context)); 98 | } else { 99 | store.dispatch(UserLoginSuccess()); 100 | Navigator.of(action.context).pushReplacementNamed(HomeScreen.route); 101 | 102 | //store.dispatch(UserLogout()); 103 | //store.dispatch(LoadUserLogin(action.context)); 104 | } 105 | }); 106 | 107 | next(action); 108 | }; 109 | } 110 | 111 | List _getRoutes(AppState state) { 112 | List routes = []; 113 | var route = ''; 114 | EntityType entityType = null; 115 | 116 | state.uiState.currentRoute 117 | .split('/') 118 | .where((part) => part.isNotEmpty) 119 | .forEach((part) { 120 | if (part == 'edit') { 121 | // Only restore new unsaved entities to prevent conflicts 122 | bool isNew = state.getUIState(entityType).isSelectedNew; 123 | if (isNew) { 124 | route += '/edit'; 125 | } 126 | } else { 127 | if (entityType == null) { 128 | entityType = EntityType.valueOf(part); 129 | } 130 | 131 | route += '/' + part; 132 | } 133 | 134 | routes.add(route); 135 | }); 136 | 137 | return routes; 138 | } 139 | 140 | _handleError(store, error, context) { 141 | print(error); 142 | 143 | store.dispatch(UserLoginSuccess()); 144 | Navigator.of(context).pushReplacementNamed(HomeScreen.route); 145 | 146 | //store.dispatch(UserLogout()); 147 | //store.dispatch(LoadUserLogin(context)); 148 | } 149 | 150 | Middleware _createUserLoggedIn( 151 | PersistenceRepository authRepository, 152 | PersistenceRepository uiRepository, 153 | ) { 154 | return (Store store, action, NextDispatcher next) { 155 | next(action); 156 | 157 | var state = store.state; 158 | 159 | authRepository.saveAuthState(state.authState); 160 | uiRepository.saveUIState(state.uiState); 161 | }; 162 | } 163 | 164 | Middleware _createUIChange(PersistenceRepository uiRepository) { 165 | return (Store store, action, NextDispatcher next) { 166 | next(action); 167 | 168 | uiRepository.saveUIState(store.state.uiState); 169 | }; 170 | } 171 | 172 | Middleware _createDataChange(PersistenceRepository dataRepository) { 173 | return (Store store, action, NextDispatcher next) { 174 | next(action); 175 | 176 | dataRepository.saveDataState(store.state.dataState); 177 | }; 178 | } 179 | 180 | Middleware _createDeleteState( 181 | PersistenceRepository authRepository, 182 | PersistenceRepository uiRepository, 183 | ) { 184 | return (Store store, action, NextDispatcher next) { 185 | authRepository.delete(); 186 | uiRepository.delete(); 187 | 188 | next(action); 189 | }; 190 | } 191 | -------------------------------------------------------------------------------- /lib/redux/app/app_reducer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_redux_starter/redux/app/app_actions.dart'; 2 | import 'package:flutter_redux_starter/redux/app/app_state.dart'; 3 | import 'package:flutter_redux_starter/redux/app/data_reducer.dart'; 4 | import 'package:flutter_redux_starter/redux/auth/auth_actions.dart'; 5 | import 'package:flutter_redux_starter/redux/auth/auth_reducer.dart'; 6 | import 'package:flutter_redux_starter/redux/ui/ui_reducer.dart'; 7 | import 'package:redux/redux.dart'; 8 | 9 | AppState appReducer(AppState state, action) { 10 | if (action is UserLogout) { 11 | return AppState().rebuild((b) => b.authState.replace(state.authState)); 12 | } else if (action is LoadStateSuccess) { 13 | return action.state.rebuild((b) => b.isLoading = false); 14 | } 15 | 16 | return state.rebuild((b) => b 17 | ..isLoading = loadingReducer(state.isLoading, action) 18 | ..uiState.replace(uiReducer(state.uiState, action)) 19 | ..authState.replace(authReducer(state.authState, action)) 20 | ..dataState.replace(dataReducer(state.dataState, action)) 21 | ); 22 | } 23 | 24 | final loadingReducer = combineReducers([ 25 | TypedReducer(_setLoading), 26 | TypedReducer(_setLoaded), 27 | ]); 28 | 29 | bool _setLoading(bool state, action) { 30 | return true; 31 | } 32 | 33 | bool _setLoaded(bool state, action) { 34 | return false; 35 | } 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/redux/app/app_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:flutter_redux_starter/data/models/models.dart'; 4 | import 'package:flutter_redux_starter/redux/app/data_state.dart'; 5 | import 'package:flutter_redux_starter/redux/auth/auth_state.dart'; 6 | import 'package:flutter_redux_starter/redux/ui/entity_ui_state.dart'; 7 | import 'package:flutter_redux_starter/redux/ui/list_ui_state.dart'; 8 | import 'package:flutter_redux_starter/redux/ui/ui_state.dart'; 9 | // STARTER: import - do not remove comment 10 | 11 | part 'app_state.g.dart'; 12 | 13 | abstract class AppState implements Built { 14 | bool get isLoading; 15 | AuthState get authState; 16 | UIState get uiState; 17 | DataState get dataState; 18 | 19 | factory AppState() { 20 | return _$AppState._( 21 | isLoading: false, 22 | authState: AuthState(), 23 | uiState: UIState(), 24 | dataState: DataState(), 25 | ); 26 | } 27 | 28 | AppState._(); 29 | static Serializer get serializer => _$appStateSerializer; 30 | 31 | EntityUIState getUIState(EntityType type) { 32 | switch (type) { 33 | // STARTER: states switch - do not remove comment 34 | default: 35 | return null; 36 | } 37 | } 38 | 39 | ListUIState getListState(EntityType type) { 40 | return getUIState(type).listUIState; 41 | } 42 | 43 | // STARTER: state getters - do not remove comment 44 | 45 | /* 46 | @override 47 | String toString() { 48 | return 'Is Loading: ${this.isLoading}'; 49 | } 50 | */ 51 | } -------------------------------------------------------------------------------- /lib/redux/app/app_state.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'app_state.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: always_put_control_body_on_new_line 10 | // ignore_for_file: annotate_overrides 11 | // ignore_for_file: avoid_annotating_with_dynamic 12 | // ignore_for_file: avoid_returning_this 13 | // ignore_for_file: omit_local_variable_types 14 | // ignore_for_file: prefer_expression_function_bodies 15 | // ignore_for_file: sort_constructors_first 16 | 17 | Serializer _$appStateSerializer = new _$AppStateSerializer(); 18 | 19 | class _$AppStateSerializer implements StructuredSerializer { 20 | @override 21 | final Iterable types = const [AppState, _$AppState]; 22 | @override 23 | final String wireName = 'AppState'; 24 | 25 | @override 26 | Iterable serialize(Serializers serializers, AppState object, 27 | {FullType specifiedType: FullType.unspecified}) { 28 | final result = [ 29 | 'isLoading', 30 | serializers.serialize(object.isLoading, 31 | specifiedType: const FullType(bool)), 32 | 'authState', 33 | serializers.serialize(object.authState, 34 | specifiedType: const FullType(AuthState)), 35 | 'uiState', 36 | serializers.serialize(object.uiState, 37 | specifiedType: const FullType(UIState)), 38 | 'dataState', 39 | serializers.serialize(object.dataState, 40 | specifiedType: const FullType(DataState)), 41 | ]; 42 | 43 | return result; 44 | } 45 | 46 | @override 47 | AppState deserialize(Serializers serializers, Iterable serialized, 48 | {FullType specifiedType: FullType.unspecified}) { 49 | final result = new AppStateBuilder(); 50 | 51 | final iterator = serialized.iterator; 52 | while (iterator.moveNext()) { 53 | final key = iterator.current as String; 54 | iterator.moveNext(); 55 | final dynamic value = iterator.current; 56 | switch (key) { 57 | case 'isLoading': 58 | result.isLoading = serializers.deserialize(value, 59 | specifiedType: const FullType(bool)) as bool; 60 | break; 61 | case 'authState': 62 | result.authState.replace(serializers.deserialize(value, 63 | specifiedType: const FullType(AuthState)) as AuthState); 64 | break; 65 | case 'uiState': 66 | result.uiState.replace(serializers.deserialize(value, 67 | specifiedType: const FullType(UIState)) as UIState); 68 | break; 69 | case 'dataState': 70 | result.dataState.replace(serializers.deserialize(value, 71 | specifiedType: const FullType(DataState)) as DataState); 72 | break; 73 | } 74 | } 75 | 76 | return result.build(); 77 | } 78 | } 79 | 80 | class _$AppState extends AppState { 81 | @override 82 | final bool isLoading; 83 | @override 84 | final AuthState authState; 85 | @override 86 | final UIState uiState; 87 | @override 88 | final DataState dataState; 89 | 90 | factory _$AppState([void updates(AppStateBuilder b)]) => 91 | (new AppStateBuilder()..update(updates)).build(); 92 | 93 | _$AppState._({this.isLoading, this.authState, this.uiState, this.dataState}) 94 | : super._() { 95 | if (isLoading == null) 96 | throw new BuiltValueNullFieldError('AppState', 'isLoading'); 97 | if (authState == null) 98 | throw new BuiltValueNullFieldError('AppState', 'authState'); 99 | if (uiState == null) 100 | throw new BuiltValueNullFieldError('AppState', 'uiState'); 101 | if (dataState == null) 102 | throw new BuiltValueNullFieldError('AppState', 'dataState'); 103 | } 104 | 105 | @override 106 | AppState rebuild(void updates(AppStateBuilder b)) => 107 | (toBuilder()..update(updates)).build(); 108 | 109 | @override 110 | AppStateBuilder toBuilder() => new AppStateBuilder()..replace(this); 111 | 112 | @override 113 | bool operator ==(dynamic other) { 114 | if (identical(other, this)) return true; 115 | if (other is! AppState) return false; 116 | return isLoading == other.isLoading && 117 | authState == other.authState && 118 | uiState == other.uiState && 119 | dataState == other.dataState; 120 | } 121 | 122 | @override 123 | int get hashCode { 124 | return $jf($jc( 125 | $jc($jc($jc(0, isLoading.hashCode), authState.hashCode), 126 | uiState.hashCode), 127 | dataState.hashCode)); 128 | } 129 | 130 | @override 131 | String toString() { 132 | return (newBuiltValueToStringHelper('AppState') 133 | ..add('isLoading', isLoading) 134 | ..add('authState', authState) 135 | ..add('uiState', uiState) 136 | ..add('dataState', dataState)) 137 | .toString(); 138 | } 139 | } 140 | 141 | class AppStateBuilder implements Builder { 142 | _$AppState _$v; 143 | 144 | bool _isLoading; 145 | bool get isLoading => _$this._isLoading; 146 | set isLoading(bool isLoading) => _$this._isLoading = isLoading; 147 | 148 | AuthStateBuilder _authState; 149 | AuthStateBuilder get authState => 150 | _$this._authState ??= new AuthStateBuilder(); 151 | set authState(AuthStateBuilder authState) => _$this._authState = authState; 152 | 153 | UIStateBuilder _uiState; 154 | UIStateBuilder get uiState => _$this._uiState ??= new UIStateBuilder(); 155 | set uiState(UIStateBuilder uiState) => _$this._uiState = uiState; 156 | 157 | DataStateBuilder _dataState; 158 | DataStateBuilder get dataState => 159 | _$this._dataState ??= new DataStateBuilder(); 160 | set dataState(DataStateBuilder dataState) => _$this._dataState = dataState; 161 | 162 | AppStateBuilder(); 163 | 164 | AppStateBuilder get _$this { 165 | if (_$v != null) { 166 | _isLoading = _$v.isLoading; 167 | _authState = _$v.authState?.toBuilder(); 168 | _uiState = _$v.uiState?.toBuilder(); 169 | _dataState = _$v.dataState?.toBuilder(); 170 | _$v = null; 171 | } 172 | return this; 173 | } 174 | 175 | @override 176 | void replace(AppState other) { 177 | if (other == null) throw new ArgumentError.notNull('other'); 178 | _$v = other as _$AppState; 179 | } 180 | 181 | @override 182 | void update(void updates(AppStateBuilder b)) { 183 | if (updates != null) updates(this); 184 | } 185 | 186 | @override 187 | _$AppState build() { 188 | _$AppState _$result; 189 | try { 190 | _$result = _$v ?? 191 | new _$AppState._( 192 | isLoading: isLoading, 193 | authState: authState.build(), 194 | uiState: uiState.build(), 195 | dataState: dataState.build()); 196 | } catch (_) { 197 | String _$failedField; 198 | try { 199 | _$failedField = 'authState'; 200 | authState.build(); 201 | _$failedField = 'uiState'; 202 | uiState.build(); 203 | _$failedField = 'dataState'; 204 | dataState.build(); 205 | } catch (e) { 206 | throw new BuiltValueNestedFieldError( 207 | 'AppState', _$failedField, e.toString()); 208 | } 209 | rethrow; 210 | } 211 | replace(_$result); 212 | return _$result; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /lib/redux/app/data_reducer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_redux_starter/redux/app/data_state.dart'; 2 | // STARTER: import - do not remove comment 3 | 4 | DataState dataReducer(DataState state, action) { 5 | 6 | return state.rebuild((b) => b 7 | // STARTER: reducer - do not remove comment 8 | ); 9 | } 10 | 11 | -------------------------------------------------------------------------------- /lib/redux/app/data_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | // STARTER: import - do not remove comment 4 | 5 | part 'data_state.g.dart'; 6 | 7 | abstract class DataState implements Built { 8 | 9 | // STARTER: fields - do not remove comment 10 | 11 | factory DataState() { 12 | return _$DataState._( 13 | // STARTER: constructor - do not remove comment 14 | ); 15 | } 16 | 17 | DataState._(); 18 | static Serializer get serializer => _$dataStateSerializer; 19 | } -------------------------------------------------------------------------------- /lib/redux/app/data_state.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'data_state.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: always_put_control_body_on_new_line 10 | // ignore_for_file: annotate_overrides 11 | // ignore_for_file: avoid_annotating_with_dynamic 12 | // ignore_for_file: avoid_returning_this 13 | // ignore_for_file: omit_local_variable_types 14 | // ignore_for_file: prefer_expression_function_bodies 15 | // ignore_for_file: sort_constructors_first 16 | 17 | Serializer _$dataStateSerializer = new _$DataStateSerializer(); 18 | 19 | class _$DataStateSerializer implements StructuredSerializer { 20 | @override 21 | final Iterable types = const [DataState, _$DataState]; 22 | @override 23 | final String wireName = 'DataState'; 24 | 25 | @override 26 | Iterable serialize(Serializers serializers, DataState object, 27 | {FullType specifiedType: FullType.unspecified}) { 28 | return []; 29 | } 30 | 31 | @override 32 | DataState deserialize(Serializers serializers, Iterable serialized, 33 | {FullType specifiedType: FullType.unspecified}) { 34 | return new DataStateBuilder().build(); 35 | } 36 | } 37 | 38 | class _$DataState extends DataState { 39 | factory _$DataState([void updates(DataStateBuilder b)]) => 40 | (new DataStateBuilder()..update(updates)).build(); 41 | 42 | _$DataState._() : super._(); 43 | 44 | @override 45 | DataState rebuild(void updates(DataStateBuilder b)) => 46 | (toBuilder()..update(updates)).build(); 47 | 48 | @override 49 | DataStateBuilder toBuilder() => new DataStateBuilder()..replace(this); 50 | 51 | @override 52 | bool operator ==(dynamic other) { 53 | if (identical(other, this)) return true; 54 | if (other is! DataState) return false; 55 | return true; 56 | } 57 | 58 | @override 59 | int get hashCode { 60 | return 190850634; 61 | } 62 | 63 | @override 64 | String toString() { 65 | return newBuiltValueToStringHelper('DataState').toString(); 66 | } 67 | } 68 | 69 | class DataStateBuilder implements Builder { 70 | _$DataState _$v; 71 | 72 | DataStateBuilder(); 73 | 74 | @override 75 | void replace(DataState other) { 76 | if (other == null) throw new ArgumentError.notNull('other'); 77 | _$v = other as _$DataState; 78 | } 79 | 80 | @override 81 | void update(void updates(DataStateBuilder b)) { 82 | if (updates != null) updates(this); 83 | } 84 | 85 | @override 86 | _$DataState build() { 87 | final _$result = _$v ?? new _$DataState._(); 88 | replace(_$result); 89 | return _$result; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/redux/auth/auth_actions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_redux_starter/redux/app/app_actions.dart'; 4 | import 'package:flutter_redux_starter/redux/app/app_state.dart'; 5 | 6 | class LoadStateRequest { 7 | final BuildContext context; 8 | LoadStateRequest(this.context); 9 | } 10 | class LoadStateSuccess { 11 | final AppState state; 12 | LoadStateSuccess(this.state); 13 | } 14 | 15 | class LoadUserLogin { 16 | final BuildContext context; 17 | LoadUserLogin(this.context); 18 | } 19 | 20 | class UserLoginLoaded { 21 | final String email; 22 | 23 | UserLoginLoaded(this.email); 24 | } 25 | 26 | class UserLoginRequest implements StartLoading { 27 | final Completer completer; 28 | final String email; 29 | final String password; 30 | 31 | UserLoginRequest(this.completer, this.email, this.password); 32 | } 33 | 34 | class UserLoginSuccess implements StopLoading {} 35 | 36 | class UserLoginFailure implements StopLoading { 37 | final String error; 38 | 39 | UserLoginFailure(this.error); 40 | } 41 | 42 | class UserLogout implements PersistData {} 43 | 44 | -------------------------------------------------------------------------------- /lib/redux/auth/auth_middleware.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_redux_starter/data/repositories/auth_repository.dart'; 3 | import 'package:flutter_redux_starter/redux/app/app_state.dart'; 4 | import 'package:flutter_redux_starter/redux/auth/auth_actions.dart'; 5 | import 'package:flutter_redux_starter/ui/auth/login_vm.dart'; 6 | import 'package:redux/redux.dart'; 7 | import 'package:shared_preferences/shared_preferences.dart'; 8 | 9 | List> createStoreAuthMiddleware([ 10 | AuthRepository repository = const AuthRepository(), 11 | ]) { 12 | final loginInit = _createLoginInit(); 13 | final loginRequest = _createLoginRequest(repository); 14 | 15 | return [ 16 | TypedMiddleware(loginInit), 17 | TypedMiddleware(loginRequest), 18 | ]; 19 | } 20 | 21 | _saveAuthLocal(action) async { 22 | SharedPreferences prefs = await SharedPreferences.getInstance(); 23 | prefs.setString('email', action.email); 24 | } 25 | 26 | _loadAuthLocal(Store store, action) async { 27 | SharedPreferences prefs = await SharedPreferences.getInstance(); 28 | 29 | String email = prefs.getString('email'); 30 | 31 | store.dispatch(UserLoginLoaded(email)); 32 | Navigator.of(action.context).pushReplacementNamed(LoginScreen.route); 33 | } 34 | 35 | Middleware _createLoginInit() { 36 | return (Store store, action, NextDispatcher next) { 37 | _loadAuthLocal(store, action); 38 | 39 | next(action); 40 | }; 41 | } 42 | 43 | Middleware _createLoginRequest(AuthRepository repository) { 44 | return (Store store, action, NextDispatcher next) { 45 | repository 46 | .login(action.email, action.password) 47 | .then((data) { 48 | _saveAuthLocal(action); 49 | 50 | store.dispatch(UserLoginSuccess()); 51 | action.completer.complete(null); 52 | }).catchError((error) { 53 | print(error); 54 | store.dispatch(UserLoginFailure(error)); 55 | }); 56 | 57 | next(action); 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /lib/redux/auth/auth_reducer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_redux_starter/redux/auth/auth_actions.dart'; 2 | import 'package:flutter_redux_starter/redux/auth/auth_state.dart'; 3 | import 'package:redux/redux.dart'; 4 | 5 | Reducer authReducer = combineReducers([ 6 | TypedReducer(userLoginLoadedReducer), 7 | TypedReducer(userLoginRequestReducer), 8 | TypedReducer(userLoginSuccessReducer), 9 | TypedReducer(userLoginFailureReducer), 10 | ]); 11 | 12 | AuthState userLoginLoadedReducer(AuthState authState, UserLoginLoaded action) { 13 | return authState.rebuild((b) => b 14 | ..isInitialized = true 15 | ..email = action.email ?? ''); 16 | } 17 | 18 | AuthState userLoginRequestReducer( 19 | AuthState authState, UserLoginRequest action) { 20 | return authState.rebuild((b) => b 21 | ..email = action.email 22 | ..password = action.password); 23 | } 24 | 25 | AuthState userLoginSuccessReducer( 26 | AuthState authState, UserLoginSuccess action) { 27 | return authState.rebuild((b) => b 28 | ..isAuthenticated = true 29 | ..password = ''); 30 | } 31 | 32 | AuthState userLoginFailureReducer( 33 | AuthState authState, UserLoginFailure action) { 34 | return authState.rebuild((b) => b..error = action.error); 35 | } 36 | -------------------------------------------------------------------------------- /lib/redux/auth/auth_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | 4 | part 'auth_state.g.dart'; 5 | 6 | abstract class AuthState implements Built { 7 | 8 | String get email; 9 | String get password; 10 | bool get isInitialized; 11 | bool get isAuthenticated; 12 | 13 | @nullable 14 | String get error; 15 | 16 | factory AuthState() { 17 | return _$AuthState._( 18 | email: '', 19 | password: '', 20 | isAuthenticated: false, 21 | isInitialized: false, 22 | ); 23 | } 24 | 25 | AuthState._(); 26 | static Serializer get serializer => _$authStateSerializer; 27 | } 28 | -------------------------------------------------------------------------------- /lib/redux/auth/auth_state.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'auth_state.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: always_put_control_body_on_new_line 10 | // ignore_for_file: annotate_overrides 11 | // ignore_for_file: avoid_annotating_with_dynamic 12 | // ignore_for_file: avoid_returning_this 13 | // ignore_for_file: omit_local_variable_types 14 | // ignore_for_file: prefer_expression_function_bodies 15 | // ignore_for_file: sort_constructors_first 16 | 17 | Serializer _$authStateSerializer = new _$AuthStateSerializer(); 18 | 19 | class _$AuthStateSerializer implements StructuredSerializer { 20 | @override 21 | final Iterable types = const [AuthState, _$AuthState]; 22 | @override 23 | final String wireName = 'AuthState'; 24 | 25 | @override 26 | Iterable serialize(Serializers serializers, AuthState object, 27 | {FullType specifiedType: FullType.unspecified}) { 28 | final result = [ 29 | 'email', 30 | serializers.serialize(object.email, 31 | specifiedType: const FullType(String)), 32 | 'password', 33 | serializers.serialize(object.password, 34 | specifiedType: const FullType(String)), 35 | 'isInitialized', 36 | serializers.serialize(object.isInitialized, 37 | specifiedType: const FullType(bool)), 38 | 'isAuthenticated', 39 | serializers.serialize(object.isAuthenticated, 40 | specifiedType: const FullType(bool)), 41 | ]; 42 | if (object.error != null) { 43 | result 44 | ..add('error') 45 | ..add(serializers.serialize(object.error, 46 | specifiedType: const FullType(String))); 47 | } 48 | 49 | return result; 50 | } 51 | 52 | @override 53 | AuthState deserialize(Serializers serializers, Iterable serialized, 54 | {FullType specifiedType: FullType.unspecified}) { 55 | final result = new AuthStateBuilder(); 56 | 57 | final iterator = serialized.iterator; 58 | while (iterator.moveNext()) { 59 | final key = iterator.current as String; 60 | iterator.moveNext(); 61 | final dynamic value = iterator.current; 62 | switch (key) { 63 | case 'email': 64 | result.email = serializers.deserialize(value, 65 | specifiedType: const FullType(String)) as String; 66 | break; 67 | case 'password': 68 | result.password = serializers.deserialize(value, 69 | specifiedType: const FullType(String)) as String; 70 | break; 71 | case 'isInitialized': 72 | result.isInitialized = serializers.deserialize(value, 73 | specifiedType: const FullType(bool)) as bool; 74 | break; 75 | case 'isAuthenticated': 76 | result.isAuthenticated = serializers.deserialize(value, 77 | specifiedType: const FullType(bool)) as bool; 78 | break; 79 | case 'error': 80 | result.error = serializers.deserialize(value, 81 | specifiedType: const FullType(String)) as String; 82 | break; 83 | } 84 | } 85 | 86 | return result.build(); 87 | } 88 | } 89 | 90 | class _$AuthState extends AuthState { 91 | @override 92 | final String email; 93 | @override 94 | final String password; 95 | @override 96 | final bool isInitialized; 97 | @override 98 | final bool isAuthenticated; 99 | @override 100 | final String error; 101 | 102 | factory _$AuthState([void updates(AuthStateBuilder b)]) => 103 | (new AuthStateBuilder()..update(updates)).build(); 104 | 105 | _$AuthState._( 106 | {this.email, 107 | this.password, 108 | this.isInitialized, 109 | this.isAuthenticated, 110 | this.error}) 111 | : super._() { 112 | if (email == null) throw new BuiltValueNullFieldError('AuthState', 'email'); 113 | if (password == null) 114 | throw new BuiltValueNullFieldError('AuthState', 'password'); 115 | if (isInitialized == null) 116 | throw new BuiltValueNullFieldError('AuthState', 'isInitialized'); 117 | if (isAuthenticated == null) 118 | throw new BuiltValueNullFieldError('AuthState', 'isAuthenticated'); 119 | } 120 | 121 | @override 122 | AuthState rebuild(void updates(AuthStateBuilder b)) => 123 | (toBuilder()..update(updates)).build(); 124 | 125 | @override 126 | AuthStateBuilder toBuilder() => new AuthStateBuilder()..replace(this); 127 | 128 | @override 129 | bool operator ==(dynamic other) { 130 | if (identical(other, this)) return true; 131 | if (other is! AuthState) return false; 132 | return email == other.email && 133 | password == other.password && 134 | isInitialized == other.isInitialized && 135 | isAuthenticated == other.isAuthenticated && 136 | error == other.error; 137 | } 138 | 139 | @override 140 | int get hashCode { 141 | return $jf($jc( 142 | $jc( 143 | $jc($jc($jc(0, email.hashCode), password.hashCode), 144 | isInitialized.hashCode), 145 | isAuthenticated.hashCode), 146 | error.hashCode)); 147 | } 148 | 149 | @override 150 | String toString() { 151 | return (newBuiltValueToStringHelper('AuthState') 152 | ..add('email', email) 153 | ..add('password', password) 154 | ..add('isInitialized', isInitialized) 155 | ..add('isAuthenticated', isAuthenticated) 156 | ..add('error', error)) 157 | .toString(); 158 | } 159 | } 160 | 161 | class AuthStateBuilder implements Builder { 162 | _$AuthState _$v; 163 | 164 | String _email; 165 | String get email => _$this._email; 166 | set email(String email) => _$this._email = email; 167 | 168 | String _password; 169 | String get password => _$this._password; 170 | set password(String password) => _$this._password = password; 171 | 172 | bool _isInitialized; 173 | bool get isInitialized => _$this._isInitialized; 174 | set isInitialized(bool isInitialized) => 175 | _$this._isInitialized = isInitialized; 176 | 177 | bool _isAuthenticated; 178 | bool get isAuthenticated => _$this._isAuthenticated; 179 | set isAuthenticated(bool isAuthenticated) => 180 | _$this._isAuthenticated = isAuthenticated; 181 | 182 | String _error; 183 | String get error => _$this._error; 184 | set error(String error) => _$this._error = error; 185 | 186 | AuthStateBuilder(); 187 | 188 | AuthStateBuilder get _$this { 189 | if (_$v != null) { 190 | _email = _$v.email; 191 | _password = _$v.password; 192 | _isInitialized = _$v.isInitialized; 193 | _isAuthenticated = _$v.isAuthenticated; 194 | _error = _$v.error; 195 | _$v = null; 196 | } 197 | return this; 198 | } 199 | 200 | @override 201 | void replace(AuthState other) { 202 | if (other == null) throw new ArgumentError.notNull('other'); 203 | _$v = other as _$AuthState; 204 | } 205 | 206 | @override 207 | void update(void updates(AuthStateBuilder b)) { 208 | if (updates != null) updates(this); 209 | } 210 | 211 | @override 212 | _$AuthState build() { 213 | final _$result = _$v ?? 214 | new _$AuthState._( 215 | email: email, 216 | password: password, 217 | isInitialized: isInitialized, 218 | isAuthenticated: isAuthenticated, 219 | error: error); 220 | replace(_$result); 221 | return _$result; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /lib/redux/ui/entity_ui_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_redux_starter/redux/ui/list_ui_state.dart'; 2 | 3 | abstract class EntityUIState { 4 | 5 | bool get isSelectedNew; 6 | ListUIState get listUIState; 7 | } -------------------------------------------------------------------------------- /lib/redux/ui/list_ui_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | 4 | part 'list_ui_state.g.dart'; 5 | 6 | abstract class ListUIState implements Built { 7 | 8 | @nullable 9 | String get search; 10 | 11 | String get sortField; 12 | bool get sortAscending; 13 | 14 | factory ListUIState(sortField) { 15 | return _$ListUIState._( 16 | sortField: sortField, 17 | sortAscending: true, 18 | ); 19 | } 20 | 21 | ListUIState._(); 22 | static Serializer get serializer => _$listUIStateSerializer; 23 | } -------------------------------------------------------------------------------- /lib/redux/ui/list_ui_state.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'list_ui_state.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: always_put_control_body_on_new_line 10 | // ignore_for_file: annotate_overrides 11 | // ignore_for_file: avoid_annotating_with_dynamic 12 | // ignore_for_file: avoid_returning_this 13 | // ignore_for_file: omit_local_variable_types 14 | // ignore_for_file: prefer_expression_function_bodies 15 | // ignore_for_file: sort_constructors_first 16 | 17 | Serializer _$listUIStateSerializer = new _$ListUIStateSerializer(); 18 | 19 | class _$ListUIStateSerializer implements StructuredSerializer { 20 | @override 21 | final Iterable types = const [ListUIState, _$ListUIState]; 22 | @override 23 | final String wireName = 'ListUIState'; 24 | 25 | @override 26 | Iterable serialize(Serializers serializers, ListUIState object, 27 | {FullType specifiedType: FullType.unspecified}) { 28 | final result = [ 29 | 'sortField', 30 | serializers.serialize(object.sortField, 31 | specifiedType: const FullType(String)), 32 | 'sortAscending', 33 | serializers.serialize(object.sortAscending, 34 | specifiedType: const FullType(bool)), 35 | ]; 36 | if (object.search != null) { 37 | result 38 | ..add('search') 39 | ..add(serializers.serialize(object.search, 40 | specifiedType: const FullType(String))); 41 | } 42 | 43 | return result; 44 | } 45 | 46 | @override 47 | ListUIState deserialize(Serializers serializers, Iterable serialized, 48 | {FullType specifiedType: FullType.unspecified}) { 49 | final result = new ListUIStateBuilder(); 50 | 51 | final iterator = serialized.iterator; 52 | while (iterator.moveNext()) { 53 | final key = iterator.current as String; 54 | iterator.moveNext(); 55 | final dynamic value = iterator.current; 56 | switch (key) { 57 | case 'search': 58 | result.search = serializers.deserialize(value, 59 | specifiedType: const FullType(String)) as String; 60 | break; 61 | case 'sortField': 62 | result.sortField = serializers.deserialize(value, 63 | specifiedType: const FullType(String)) as String; 64 | break; 65 | case 'sortAscending': 66 | result.sortAscending = serializers.deserialize(value, 67 | specifiedType: const FullType(bool)) as bool; 68 | break; 69 | } 70 | } 71 | 72 | return result.build(); 73 | } 74 | } 75 | 76 | class _$ListUIState extends ListUIState { 77 | @override 78 | final String search; 79 | @override 80 | final String sortField; 81 | @override 82 | final bool sortAscending; 83 | 84 | factory _$ListUIState([void updates(ListUIStateBuilder b)]) => 85 | (new ListUIStateBuilder()..update(updates)).build(); 86 | 87 | _$ListUIState._({this.search, this.sortField, this.sortAscending}) 88 | : super._() { 89 | if (sortField == null) 90 | throw new BuiltValueNullFieldError('ListUIState', 'sortField'); 91 | if (sortAscending == null) 92 | throw new BuiltValueNullFieldError('ListUIState', 'sortAscending'); 93 | } 94 | 95 | @override 96 | ListUIState rebuild(void updates(ListUIStateBuilder b)) => 97 | (toBuilder()..update(updates)).build(); 98 | 99 | @override 100 | ListUIStateBuilder toBuilder() => new ListUIStateBuilder()..replace(this); 101 | 102 | @override 103 | bool operator ==(dynamic other) { 104 | if (identical(other, this)) return true; 105 | if (other is! ListUIState) return false; 106 | return search == other.search && 107 | sortField == other.sortField && 108 | sortAscending == other.sortAscending; 109 | } 110 | 111 | @override 112 | int get hashCode { 113 | return $jf($jc($jc($jc(0, search.hashCode), sortField.hashCode), 114 | sortAscending.hashCode)); 115 | } 116 | 117 | @override 118 | String toString() { 119 | return (newBuiltValueToStringHelper('ListUIState') 120 | ..add('search', search) 121 | ..add('sortField', sortField) 122 | ..add('sortAscending', sortAscending)) 123 | .toString(); 124 | } 125 | } 126 | 127 | class ListUIStateBuilder implements Builder { 128 | _$ListUIState _$v; 129 | 130 | String _search; 131 | String get search => _$this._search; 132 | set search(String search) => _$this._search = search; 133 | 134 | String _sortField; 135 | String get sortField => _$this._sortField; 136 | set sortField(String sortField) => _$this._sortField = sortField; 137 | 138 | bool _sortAscending; 139 | bool get sortAscending => _$this._sortAscending; 140 | set sortAscending(bool sortAscending) => 141 | _$this._sortAscending = sortAscending; 142 | 143 | ListUIStateBuilder(); 144 | 145 | ListUIStateBuilder get _$this { 146 | if (_$v != null) { 147 | _search = _$v.search; 148 | _sortField = _$v.sortField; 149 | _sortAscending = _$v.sortAscending; 150 | _$v = null; 151 | } 152 | return this; 153 | } 154 | 155 | @override 156 | void replace(ListUIState other) { 157 | if (other == null) throw new ArgumentError.notNull('other'); 158 | _$v = other as _$ListUIState; 159 | } 160 | 161 | @override 162 | void update(void updates(ListUIStateBuilder b)) { 163 | if (updates != null) updates(this); 164 | } 165 | 166 | @override 167 | _$ListUIState build() { 168 | final _$result = _$v ?? 169 | new _$ListUIState._( 170 | search: search, sortField: sortField, sortAscending: sortAscending); 171 | replace(_$result); 172 | return _$result; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /lib/redux/ui/ui_actions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_redux_starter/redux/app/app_actions.dart'; 2 | 3 | class UpdateCurrentRoute implements PersistUI { 4 | final String route; 5 | UpdateCurrentRoute(this.route); 6 | } -------------------------------------------------------------------------------- /lib/redux/ui/ui_reducer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_redux_starter/redux/ui/ui_actions.dart'; 2 | import 'package:flutter_redux_starter/redux/ui/ui_state.dart'; 3 | import 'package:redux/redux.dart'; 4 | // STARTER: import - do not remove comment 5 | 6 | UIState uiReducer(UIState state, action) { 7 | 8 | return state.rebuild((b) => b 9 | ..currentRoute = currentRouteReducer(state.currentRoute, action) 10 | // STARTER: reducer - do not remove comment 11 | ); 12 | } 13 | 14 | Reducer currentRouteReducer = combineReducers([ 15 | TypedReducer(updateCurrentRouteReducer), 16 | ]); 17 | 18 | String updateCurrentRouteReducer(String currentRoute, UpdateCurrentRoute action) { 19 | return action.route; 20 | } 21 | -------------------------------------------------------------------------------- /lib/redux/ui/ui_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:flutter_redux_starter/ui/home/home_screen.dart'; 4 | // STARTER: import - do not remove comment 5 | 6 | part 'ui_state.g.dart'; 7 | 8 | abstract class UIState implements Built { 9 | 10 | String get currentRoute; 11 | 12 | // STARTER: properties - do not remove comment 13 | 14 | factory UIState() { 15 | return _$UIState._( 16 | //currentRoute: LoginScreen.route, 17 | currentRoute: HomeScreen.route, 18 | // STARTER: constructor - do not remove comment 19 | ); 20 | } 21 | 22 | UIState._(); 23 | static Serializer get serializer => _$uIStateSerializer; 24 | } 25 | 26 | -------------------------------------------------------------------------------- /lib/redux/ui/ui_state.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'ui_state.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: always_put_control_body_on_new_line 10 | // ignore_for_file: annotate_overrides 11 | // ignore_for_file: avoid_annotating_with_dynamic 12 | // ignore_for_file: avoid_returning_this 13 | // ignore_for_file: omit_local_variable_types 14 | // ignore_for_file: prefer_expression_function_bodies 15 | // ignore_for_file: sort_constructors_first 16 | 17 | Serializer _$uIStateSerializer = new _$UIStateSerializer(); 18 | 19 | class _$UIStateSerializer implements StructuredSerializer { 20 | @override 21 | final Iterable types = const [UIState, _$UIState]; 22 | @override 23 | final String wireName = 'UIState'; 24 | 25 | @override 26 | Iterable serialize(Serializers serializers, UIState object, 27 | {FullType specifiedType: FullType.unspecified}) { 28 | final result = [ 29 | 'currentRoute', 30 | serializers.serialize(object.currentRoute, 31 | specifiedType: const FullType(String)), 32 | ]; 33 | 34 | return result; 35 | } 36 | 37 | @override 38 | UIState deserialize(Serializers serializers, Iterable serialized, 39 | {FullType specifiedType: FullType.unspecified}) { 40 | final result = new UIStateBuilder(); 41 | 42 | final iterator = serialized.iterator; 43 | while (iterator.moveNext()) { 44 | final key = iterator.current as String; 45 | iterator.moveNext(); 46 | final dynamic value = iterator.current; 47 | switch (key) { 48 | case 'currentRoute': 49 | result.currentRoute = serializers.deserialize(value, 50 | specifiedType: const FullType(String)) as String; 51 | break; 52 | } 53 | } 54 | 55 | return result.build(); 56 | } 57 | } 58 | 59 | class _$UIState extends UIState { 60 | @override 61 | final String currentRoute; 62 | 63 | factory _$UIState([void updates(UIStateBuilder b)]) => 64 | (new UIStateBuilder()..update(updates)).build(); 65 | 66 | _$UIState._({this.currentRoute}) : super._() { 67 | if (currentRoute == null) 68 | throw new BuiltValueNullFieldError('UIState', 'currentRoute'); 69 | } 70 | 71 | @override 72 | UIState rebuild(void updates(UIStateBuilder b)) => 73 | (toBuilder()..update(updates)).build(); 74 | 75 | @override 76 | UIStateBuilder toBuilder() => new UIStateBuilder()..replace(this); 77 | 78 | @override 79 | bool operator ==(dynamic other) { 80 | if (identical(other, this)) return true; 81 | if (other is! UIState) return false; 82 | return currentRoute == other.currentRoute; 83 | } 84 | 85 | @override 86 | int get hashCode { 87 | return $jf($jc(0, currentRoute.hashCode)); 88 | } 89 | 90 | @override 91 | String toString() { 92 | return (newBuiltValueToStringHelper('UIState') 93 | ..add('currentRoute', currentRoute)) 94 | .toString(); 95 | } 96 | } 97 | 98 | class UIStateBuilder implements Builder { 99 | _$UIState _$v; 100 | 101 | String _currentRoute; 102 | String get currentRoute => _$this._currentRoute; 103 | set currentRoute(String currentRoute) => _$this._currentRoute = currentRoute; 104 | 105 | UIStateBuilder(); 106 | 107 | UIStateBuilder get _$this { 108 | if (_$v != null) { 109 | _currentRoute = _$v.currentRoute; 110 | _$v = null; 111 | } 112 | return this; 113 | } 114 | 115 | @override 116 | void replace(UIState other) { 117 | if (other == null) throw new ArgumentError.notNull('other'); 118 | _$v = other as _$UIState; 119 | } 120 | 121 | @override 122 | void update(void updates(UIStateBuilder b)) { 123 | if (updates != null) updates(this); 124 | } 125 | 126 | @override 127 | _$UIState build() { 128 | final _$result = _$v ?? new _$UIState._(currentRoute: currentRoute); 129 | replace(_$result); 130 | return _$result; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /lib/ui/app/actions_menu_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_redux_starter/data/models/models.dart'; 3 | 4 | class ActionMenuChoice { 5 | const ActionMenuChoice({ 6 | @required this.action, 7 | @required this.label, 8 | @required this.icon}); 9 | 10 | final String label; 11 | final IconData icon; 12 | final EntityAction action; 13 | } 14 | 15 | class ActionMenuButton extends StatelessWidget { 16 | final BaseEntity entity; 17 | final List customActions; 18 | final Function(BuildContext, EntityAction) onSelected; 19 | final bool isLoading; 20 | 21 | ActionMenuButton({ 22 | @required this.entity, 23 | @required this.onSelected, 24 | @required this.isLoading, 25 | this.customActions, 26 | }); 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | List> actions = []; 31 | 32 | if (isLoading) { 33 | return IconButton( 34 | onPressed: null, 35 | icon: SizedBox( 36 | child: CircularProgressIndicator( 37 | valueColor: AlwaysStoppedAnimation(Colors.white), 38 | ), 39 | ), 40 | ); 41 | } 42 | 43 | customActions?.forEach((action) { 44 | if (action != null) { 45 | actions.add(PopupMenuItem( 46 | value: action.action, 47 | child: Row( 48 | children: [ 49 | Icon(action.icon), 50 | SizedBox(width: 15.0), 51 | Text(action.label ?? ''), 52 | ], 53 | ), 54 | )); 55 | } 56 | }); 57 | 58 | if (actions.length > 0) { 59 | actions.add(PopupMenuDivider()); 60 | } 61 | 62 | if (entity.isActive) { 63 | actions.add(PopupMenuItem( 64 | value: EntityAction.delete, 65 | child: Row( 66 | children: [ 67 | Icon(Icons.delete), 68 | SizedBox(width: 15.0), 69 | Text('Delete'), 70 | ], 71 | ), 72 | )); 73 | } 74 | 75 | return PopupMenuButton( 76 | icon: Icon(Icons.more_vert), 77 | itemBuilder: (BuildContext context) => actions, 78 | onSelected: (EntityAction action) { 79 | this.onSelected(context, action); 80 | }, 81 | ); 82 | } 83 | } -------------------------------------------------------------------------------- /lib/ui/app/app_bottom_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_redux/flutter_redux.dart'; 3 | import 'package:flutter_redux_starter/redux/app/app_state.dart'; 4 | import 'package:flutter_redux_starter/redux/ui/list_ui_state.dart'; 5 | import 'package:flutter_redux_starter/data/models/models.dart'; 6 | import 'package:redux/redux.dart'; 7 | 8 | class AppBottomBar extends StatefulWidget { 9 | 10 | final List sortFields; 11 | final Function(String) onSelectedSortField; 12 | final EntityType entityType; 13 | 14 | AppBottomBar( 15 | {this.sortFields, 16 | this.onSelectedSortField, 17 | this.entityType}); 18 | 19 | @override 20 | _AppBottomBarState createState() => new _AppBottomBarState(); 21 | } 22 | 23 | class _AppBottomBarState extends State { 24 | PersistentBottomSheetController _sortController; 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | final _showSortSheet = () { 29 | if (_sortController != null) { 30 | _sortController.close(); 31 | return; 32 | } 33 | 34 | _sortController = Scaffold.of(context).showBottomSheet((context) { 35 | return StoreConnector( 36 | converter: (Store store) => store.state.getListState(widget.entityType), 37 | builder: (BuildContext context, listUIState) { 38 | return Container( 39 | color: Theme.of(context).backgroundColor, 40 | child: Column( 41 | mainAxisSize: MainAxisSize.min, 42 | children: widget.sortFields.map((sortField) { 43 | return RadioListTile( 44 | dense: true, 45 | // TODO replace with localization 46 | title: Text(sortField[0].toUpperCase() + sortField.substring(1)), 47 | subtitle: sortField == listUIState.sortField 48 | ? Text(listUIState.sortAscending 49 | ? 'Ascending' 50 | : 'Descending') 51 | : null, 52 | groupValue: listUIState.sortField, 53 | onChanged: (value) { 54 | widget.onSelectedSortField(value); 55 | }, 56 | value: sortField, 57 | ); 58 | }).toList()), 59 | ); 60 | }, 61 | ); 62 | }); 63 | 64 | _sortController.closed.whenComplete(() { 65 | _sortController = null; 66 | }); 67 | }; 68 | 69 | return new BottomAppBar( 70 | shape: CircularNotchedRectangle(), 71 | child: Row( 72 | children: [ 73 | IconButton( 74 | tooltip: 'Sort', 75 | icon: Icon(Icons.sort_by_alpha), 76 | onPressed: _showSortSheet, 77 | ), 78 | ], 79 | ), 80 | ); 81 | } 82 | } -------------------------------------------------------------------------------- /lib/ui/app/app_drawer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_redux/flutter_redux.dart'; 3 | import 'package:flutter_redux_starter/redux/app/app_state.dart'; 4 | import 'package:flutter_redux_starter/redux/ui/ui_actions.dart'; 5 | import 'package:flutter_redux_starter/ui/app/app_drawer_vm.dart'; 6 | import 'package:flutter_redux_starter/ui/home/home_screen.dart'; 7 | import 'package:redux/redux.dart'; 8 | // STARTER: import - do not remove comment 9 | 10 | class AppDrawer extends StatelessWidget { 11 | final AppDrawerVM viewModel; 12 | 13 | AppDrawer({ 14 | Key key, 15 | @required this.viewModel, 16 | }) : super(key: key); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | Store store = StoreProvider.of(context); 21 | NavigatorState navigator = Navigator.of(context); 22 | 23 | return Drawer( 24 | child: ListView( 25 | children: [ 26 | Container( 27 | child: DrawerHeader( 28 | child: Container(), 29 | ), 30 | ), 31 | ListTile( 32 | leading: Icon(Icons.home), 33 | title: Text('Home'), 34 | onTap: () { 35 | store.dispatch(UpdateCurrentRoute(HomeScreen.route)); 36 | navigator.pushReplacementNamed(HomeScreen.route); 37 | }, 38 | ), 39 | // STARTER: menu - do not remove comment 40 | AboutListTile( 41 | applicationName: '', 42 | icon: Icon(Icons.info_outline), 43 | ), 44 | ], 45 | ), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/ui/app/app_drawer_vm.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_redux/flutter_redux.dart'; 4 | import 'package:flutter_redux_starter/redux/app/app_state.dart'; 5 | import 'package:flutter_redux_starter/ui/app/app_drawer.dart'; 6 | import 'package:redux/redux.dart'; 7 | 8 | class AppDrawerBuilder extends StatelessWidget { 9 | AppDrawerBuilder({Key key}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return StoreConnector( 14 | converter: AppDrawerVM.fromStore, 15 | builder: (context, viewModel) { 16 | return AppDrawer(viewModel: viewModel); 17 | }, 18 | ); 19 | } 20 | } 21 | 22 | class AppDrawerVM { 23 | 24 | AppDrawerVM(); 25 | 26 | static AppDrawerVM fromStore(Store store) { 27 | 28 | return AppDrawerVM(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/ui/app/app_search.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_redux/flutter_redux.dart'; 3 | import 'package:flutter_redux_starter/data/models/models.dart'; 4 | import 'package:flutter_redux_starter/redux/app/app_state.dart'; 5 | import 'package:flutter_redux_starter/redux/ui/list_ui_state.dart'; 6 | import 'package:redux/redux.dart'; 7 | 8 | class AppSearch extends StatelessWidget { 9 | final EntityType entityType; 10 | final String search; 11 | final Function(String) onSearchChanged; 12 | 13 | AppSearch({ 14 | this.entityType, 15 | this.search, 16 | this.onSearchChanged, 17 | }); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return StoreConnector( 22 | converter: (Store store) => 23 | store.state.getListState(entityType), 24 | builder: (BuildContext context, listUIState) { 25 | var title = entityType.plural.toString(); 26 | return listUIState.search == null 27 | // TODO replace with localization 28 | ? Text(title[0].toUpperCase() + title.substring(1)) 29 | : Container( 30 | padding: const EdgeInsets.only(left: 8.0), 31 | height: 38.0, 32 | margin: EdgeInsets.only(bottom: 2.0), 33 | decoration: BoxDecoration( 34 | color: listUIState.search != null && 35 | listUIState.search.isNotEmpty 36 | ? Colors.yellow[200] 37 | : Colors.grey[100], 38 | border: Border.all(color: Colors.grey[400], width: 1.0), 39 | borderRadius: BorderRadius.circular(6.0)), 40 | child: TextField( 41 | decoration: InputDecoration( 42 | prefixIcon: Padding( 43 | padding: EdgeInsets.only(right: 8.0), 44 | child: Icon(Icons.search), 45 | ), 46 | border: InputBorder.none, 47 | hintText: 'Search'), 48 | autofocus: true, 49 | autocorrect: false, 50 | onChanged: (value) => onSearchChanged(value), 51 | ), 52 | ); 53 | }, 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/ui/app/app_search_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_redux/flutter_redux.dart'; 3 | import 'package:flutter_redux_starter/data/models/models.dart'; 4 | import 'package:flutter_redux_starter/redux/app/app_state.dart'; 5 | import 'package:flutter_redux_starter/redux/ui/list_ui_state.dart'; 6 | import 'package:redux/redux.dart'; 7 | 8 | class AppSearchButton extends StatelessWidget { 9 | 10 | final EntityType entityType; 11 | final Function onSearchPressed; 12 | 13 | AppSearchButton({ 14 | this.entityType, 15 | this.onSearchPressed, 16 | }); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return StoreConnector( 21 | converter: (Store store) => 22 | store.state.getListState(entityType), 23 | distinct: true, 24 | builder: (BuildContext context, listUIState) { 25 | return IconButton( 26 | icon: Icon(listUIState.search == null ? Icons.search : Icons.close), 27 | tooltip: 'Search', 28 | onPressed: () => onSearchPressed(listUIState.search == null ? '' : null), 29 | ); 30 | }, 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/ui/app/dismissible_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_redux_starter/data/models/models.dart'; 3 | 4 | class DismissibleEntity extends StatelessWidget { 5 | 6 | DismissibleEntity({ 7 | this.entity, 8 | this.child, 9 | this.onDismissed, 10 | this.onTap,}); 11 | 12 | final BaseEntity entity; 13 | final Widget child; 14 | final Function onDismissed; 15 | final Function onTap; 16 | 17 | //static final _itemKey = (int id) => Key('__client_item_${id}__'); 18 | static final _itemKey = (int id) => Key('__item_${id}__'); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return Dismissible( 23 | key: _itemKey(entity.id), 24 | onDismissed: onDismissed, 25 | child: child, 26 | background: entity.isDeleted ? Container( 27 | color: Colors.blue, 28 | child: const ListTile( 29 | leading: 30 | const Icon(Icons.restore, color: Colors.white, size: 36.0)), 31 | ) : Container( 32 | color: Colors.red, 33 | child: const ListTile( 34 | leading: 35 | const Icon(Icons.delete, color: Colors.white, size: 36.0))), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/ui/app/form_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class FormCard extends StatelessWidget { 4 | 5 | FormCard({ 6 | Key key, 7 | @required this.children, 8 | }) : super(key: key); 9 | 10 | final List children; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Padding( 15 | padding: const EdgeInsets.all(12.0), 16 | child: Card( 17 | elevation: 2.0, 18 | child: Padding( 19 | padding: const EdgeInsets.all(16.0), 20 | child: Column( 21 | mainAxisSize: MainAxisSize.min, 22 | children: children, 23 | ), 24 | ), 25 | ), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/ui/app/icon_message.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class IconMessage extends StatelessWidget { 4 | final String message; 5 | final IconData icon; 6 | 7 | IconMessage({ 8 | this.message, 9 | this.icon = Icons.check_circle, 10 | }); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Row( 15 | children: [ 16 | Icon(this.icon), 17 | Padding( 18 | padding: EdgeInsets.only(left: 10.0), 19 | child: Text(this.message), 20 | ) 21 | ], 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/ui/app/init.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_redux/flutter_redux.dart'; 3 | import 'package:flutter_redux_starter/redux/app/app_state.dart'; 4 | import 'package:flutter_redux_starter/redux/auth/auth_actions.dart'; 5 | import 'package:redux/redux.dart'; 6 | 7 | class InitScreen extends StatelessWidget { 8 | static final String route = '/'; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return StoreBuilder( 13 | onInit: (Store store) => 14 | store.dispatch(LoadStateRequest(context)), 15 | builder: (BuildContext context, Store store) { 16 | return Container( 17 | color: Colors.white, 18 | child: Column( 19 | children: [ 20 | Expanded( 21 | child: Container(), 22 | ), 23 | SizedBox( 24 | height: 4.0, 25 | child: LinearProgressIndicator(), 26 | ) 27 | ], 28 | ), 29 | ); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/ui/app/save_icon_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SaveIconButton extends StatelessWidget { 4 | 5 | SaveIconButton({this.isLoading, this.onPressed}); 6 | final bool isLoading; 7 | final Function onPressed; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | 12 | if (isLoading) { 13 | return IconButton( 14 | onPressed: null, 15 | icon: SizedBox( 16 | child: CircularProgressIndicator( 17 | valueColor: AlwaysStoppedAnimation(Colors.white), 18 | ), 19 | ), 20 | ); 21 | } 22 | 23 | return IconButton( 24 | onPressed: onPressed, 25 | tooltip: 'Save', 26 | icon: Icon( 27 | Icons.cloud_upload, 28 | color: Colors.white, 29 | ), 30 | ); 31 | } 32 | } -------------------------------------------------------------------------------- /lib/ui/auth/login.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_redux_starter/keys.dart'; 3 | import 'package:flutter_redux_starter/ui/auth/login_vm.dart'; 4 | 5 | class LoginView extends StatefulWidget { 6 | final LoginVM viewModel; 7 | 8 | LoginView({ 9 | Key key, 10 | @required this.viewModel, 11 | }) : super(key: key); 12 | 13 | @override 14 | _LoginState createState() => new _LoginState(); 15 | } 16 | 17 | class _LoginState extends State { 18 | static final GlobalKey _formKey = GlobalKey(); 19 | 20 | final _emailController = TextEditingController(); 21 | final _passwordController = TextEditingController(); 22 | 23 | static final ValueKey _emailKey = new Key(LoginKeys.emailKey); 24 | static final ValueKey _passwordKey = new Key(LoginKeys.passwordKey); 25 | 26 | @override 27 | void didChangeDependencies() { 28 | var authState = widget.viewModel.authState; 29 | _emailController.text = authState.email; 30 | 31 | super.didChangeDependencies(); 32 | } 33 | 34 | @override 35 | void dispose() { 36 | _emailController.dispose(); 37 | _passwordController.dispose(); 38 | 39 | super.dispose(); 40 | } 41 | 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | var viewModel = widget.viewModel; 46 | 47 | if (!viewModel.authState.isInitialized) { 48 | return Container(); 49 | } 50 | 51 | return Padding( 52 | padding: const EdgeInsets.all(12.0), 53 | child: Column( 54 | children: [ 55 | Form( 56 | key: _formKey, 57 | child: FormCard( 58 | children: [ 59 | TextFormField( 60 | controller: _emailController, 61 | key: _emailKey, 62 | autocorrect: false, 63 | decoration: InputDecoration( 64 | labelText: 'Email'), 65 | keyboardType: TextInputType.emailAddress, 66 | validator: (val) => val.isEmpty || val.trim().length == 0 67 | ? 'Please enter your email' 68 | : null, 69 | ), 70 | TextFormField( 71 | controller: _passwordController, 72 | key: _passwordKey, 73 | autocorrect: false, 74 | decoration: InputDecoration( 75 | labelText: 'Password'), 76 | validator: (val) => val.isEmpty || val.trim().length == 0 77 | ? 'Please enter your password' 78 | : null, 79 | obscureText: true, 80 | ), 81 | viewModel.authState.error == null 82 | ? Container() 83 | : Container( 84 | padding: EdgeInsets.only(top: 26.0, bottom: 4.0), 85 | child: Center( 86 | child: Text( 87 | viewModel.authState.error, 88 | style: TextStyle( 89 | color: Colors.red, 90 | fontWeight: FontWeight.bold, 91 | ), 92 | ), 93 | ), 94 | ), 95 | ], 96 | ), 97 | ), 98 | RaisedButton( 99 | child: Text('LOGIN'), 100 | onPressed: () { 101 | if (!_formKey.currentState.validate()) { 102 | return; 103 | } 104 | viewModel.onLoginPressed( 105 | context, 106 | _emailController.text, 107 | _passwordController.text 108 | ); 109 | }, 110 | ), 111 | ], 112 | ), 113 | ); 114 | } 115 | } 116 | 117 | class FormCard extends StatelessWidget { 118 | 119 | FormCard({ 120 | Key key, 121 | @required this.children, 122 | }) : super(key: key); 123 | 124 | final List children; 125 | 126 | @override 127 | Widget build(BuildContext context) { 128 | return Padding( 129 | padding: const EdgeInsets.all(12.0), 130 | child: Card( 131 | elevation: 2.0, 132 | child: Padding( 133 | padding: const EdgeInsets.all(16.0), 134 | child: Column( 135 | children: children, 136 | ), 137 | ), 138 | ), 139 | ); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /lib/ui/auth/login_vm.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | import 'package:flutter_redux/flutter_redux.dart'; 6 | import 'package:flutter_redux_starter/redux/app/app_state.dart'; 7 | import 'package:flutter_redux_starter/redux/auth/auth_actions.dart'; 8 | import 'package:flutter_redux_starter/redux/auth/auth_state.dart'; 9 | import 'package:flutter_redux_starter/ui/auth/login.dart'; 10 | import 'package:redux/redux.dart'; 11 | 12 | class LoginScreen extends StatelessWidget { 13 | LoginScreen({Key key}) : super(key: key); 14 | 15 | static final String route = '/login'; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Scaffold( 20 | body: StoreConnector( 21 | converter: LoginVM.fromStore, 22 | builder: (context, vm) { 23 | return LoginView( 24 | viewModel: vm, 25 | ); 26 | }, 27 | ), 28 | ); 29 | } 30 | } 31 | 32 | class LoginVM { 33 | bool isLoading; 34 | AuthState authState; 35 | final Function(BuildContext, String, String) onLoginPressed; 36 | 37 | LoginVM({ 38 | @required this.isLoading, 39 | @required this.authState, 40 | @required this.onLoginPressed, 41 | }); 42 | 43 | static LoginVM fromStore(Store store) { 44 | return LoginVM( 45 | isLoading: store.state.isLoading, 46 | authState: store.state.authState, 47 | onLoginPressed: (BuildContext context, String email, String password) { 48 | if (store.state.isLoading) { 49 | return; 50 | } 51 | final Completer completer = new Completer(); 52 | store.dispatch(UserLoginRequest( 53 | completer, email.trim(), password.trim())); 54 | completer.future.then((_) { 55 | //Navigator.of(context).pushReplacementNamed(DashboardScreen.route); 56 | //store.dispatch(UpdateCurrentRoute(DashboardScreen.route)); 57 | }); 58 | }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/ui/home/home_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_redux_starter/ui/app/app_drawer_vm.dart'; 3 | 4 | class HomeScreen extends StatelessWidget { 5 | 6 | static final String route = '/home'; 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Scaffold( 11 | appBar: AppBar( 12 | title: Text('Home'), 13 | ), 14 | drawer: AppDrawerBuilder(), 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://www.dartlang.org/tools/pub/glossary#lockfile 3 | packages: 4 | analyzer: 5 | dependency: transitive 6 | description: 7 | name: analyzer 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "0.33.6+1" 11 | analyzer_plugin: 12 | dependency: transitive 13 | description: 14 | name: analyzer_plugin 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "0.0.1-alpha.5" 18 | args: 19 | dependency: transitive 20 | description: 21 | name: args 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.5.1" 25 | async: 26 | dependency: transitive 27 | description: 28 | name: async 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "2.0.8" 32 | build: 33 | dependency: transitive 34 | description: 35 | name: build 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.0.2" 39 | build_config: 40 | dependency: transitive 41 | description: 42 | name: build_config 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "0.3.1+4" 46 | build_resolvers: 47 | dependency: transitive 48 | description: 49 | name: build_resolvers 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "0.2.2+7" 53 | build_runner: 54 | dependency: "direct dev" 55 | description: 56 | name: build_runner 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "1.1.2" 60 | build_runner_core: 61 | dependency: transitive 62 | description: 63 | name: build_runner_core 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "1.1.2" 67 | built_collection: 68 | dependency: "direct main" 69 | description: 70 | name: built_collection 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "4.1.0" 74 | built_value: 75 | dependency: "direct main" 76 | description: 77 | name: built_value 78 | url: "https://pub.dartlang.org" 79 | source: hosted 80 | version: "6.2.0" 81 | built_value_generator: 82 | dependency: "direct dev" 83 | description: 84 | name: built_value_generator 85 | url: "https://pub.dartlang.org" 86 | source: hosted 87 | version: "6.2.0" 88 | charcode: 89 | dependency: transitive 90 | description: 91 | name: charcode 92 | url: "https://pub.dartlang.org" 93 | source: hosted 94 | version: "1.1.2" 95 | code_builder: 96 | dependency: transitive 97 | description: 98 | name: code_builder 99 | url: "https://pub.dartlang.org" 100 | source: hosted 101 | version: "3.1.3" 102 | collection: 103 | dependency: transitive 104 | description: 105 | name: collection 106 | url: "https://pub.dartlang.org" 107 | source: hosted 108 | version: "1.14.11" 109 | convert: 110 | dependency: transitive 111 | description: 112 | name: convert 113 | url: "https://pub.dartlang.org" 114 | source: hosted 115 | version: "2.1.1" 116 | crypto: 117 | dependency: transitive 118 | description: 119 | name: crypto 120 | url: "https://pub.dartlang.org" 121 | source: hosted 122 | version: "2.0.6" 123 | csslib: 124 | dependency: transitive 125 | description: 126 | name: csslib 127 | url: "https://pub.dartlang.org" 128 | source: hosted 129 | version: "0.14.6" 130 | cupertino_icons: 131 | dependency: "direct main" 132 | description: 133 | name: cupertino_icons 134 | url: "https://pub.dartlang.org" 135 | source: hosted 136 | version: "0.1.2" 137 | dart_style: 138 | dependency: transitive 139 | description: 140 | name: dart_style 141 | url: "https://pub.dartlang.org" 142 | source: hosted 143 | version: "1.2.0" 144 | fixnum: 145 | dependency: transitive 146 | description: 147 | name: fixnum 148 | url: "https://pub.dartlang.org" 149 | source: hosted 150 | version: "0.10.9" 151 | flutter: 152 | dependency: "direct main" 153 | description: flutter 154 | source: sdk 155 | version: "0.0.0" 156 | flutter_redux: 157 | dependency: "direct main" 158 | description: 159 | name: flutter_redux 160 | url: "https://pub.dartlang.org" 161 | source: hosted 162 | version: "0.5.2" 163 | front_end: 164 | dependency: transitive 165 | description: 166 | name: front_end 167 | url: "https://pub.dartlang.org" 168 | source: hosted 169 | version: "0.1.6+9" 170 | glob: 171 | dependency: transitive 172 | description: 173 | name: glob 174 | url: "https://pub.dartlang.org" 175 | source: hosted 176 | version: "1.1.7" 177 | graphs: 178 | dependency: transitive 179 | description: 180 | name: graphs 181 | url: "https://pub.dartlang.org" 182 | source: hosted 183 | version: "0.1.3+1" 184 | html: 185 | dependency: transitive 186 | description: 187 | name: html 188 | url: "https://pub.dartlang.org" 189 | source: hosted 190 | version: "0.13.3+3" 191 | http: 192 | dependency: "direct main" 193 | description: 194 | name: http 195 | url: "https://pub.dartlang.org" 196 | source: hosted 197 | version: "0.11.3+17" 198 | http_multi_server: 199 | dependency: transitive 200 | description: 201 | name: http_multi_server 202 | url: "https://pub.dartlang.org" 203 | source: hosted 204 | version: "2.0.5" 205 | http_parser: 206 | dependency: transitive 207 | description: 208 | name: http_parser 209 | url: "https://pub.dartlang.org" 210 | source: hosted 211 | version: "3.1.3" 212 | io: 213 | dependency: transitive 214 | description: 215 | name: io 216 | url: "https://pub.dartlang.org" 217 | source: hosted 218 | version: "0.3.3" 219 | js: 220 | dependency: transitive 221 | description: 222 | name: js 223 | url: "https://pub.dartlang.org" 224 | source: hosted 225 | version: "0.6.1+1" 226 | json_annotation: 227 | dependency: transitive 228 | description: 229 | name: json_annotation 230 | url: "https://pub.dartlang.org" 231 | source: hosted 232 | version: "2.0.0" 233 | kernel: 234 | dependency: transitive 235 | description: 236 | name: kernel 237 | url: "https://pub.dartlang.org" 238 | source: hosted 239 | version: "0.3.6+9" 240 | logging: 241 | dependency: transitive 242 | description: 243 | name: logging 244 | url: "https://pub.dartlang.org" 245 | source: hosted 246 | version: "0.11.3+2" 247 | matcher: 248 | dependency: transitive 249 | description: 250 | name: matcher 251 | url: "https://pub.dartlang.org" 252 | source: hosted 253 | version: "0.12.4" 254 | memoize: 255 | dependency: "direct main" 256 | description: 257 | name: memoize 258 | url: "https://pub.dartlang.org" 259 | source: hosted 260 | version: "2.0.0" 261 | meta: 262 | dependency: transitive 263 | description: 264 | name: meta 265 | url: "https://pub.dartlang.org" 266 | source: hosted 267 | version: "1.1.6" 268 | mime: 269 | dependency: transitive 270 | description: 271 | name: mime 272 | url: "https://pub.dartlang.org" 273 | source: hosted 274 | version: "0.9.6+2" 275 | package_config: 276 | dependency: transitive 277 | description: 278 | name: package_config 279 | url: "https://pub.dartlang.org" 280 | source: hosted 281 | version: "1.0.5" 282 | path: 283 | dependency: transitive 284 | description: 285 | name: path 286 | url: "https://pub.dartlang.org" 287 | source: hosted 288 | version: "1.6.2" 289 | path_provider: 290 | dependency: "direct main" 291 | description: 292 | name: path_provider 293 | url: "https://pub.dartlang.org" 294 | source: hosted 295 | version: "0.4.1" 296 | pedantic: 297 | dependency: transitive 298 | description: 299 | name: pedantic 300 | url: "https://pub.dartlang.org" 301 | source: hosted 302 | version: "1.4.0" 303 | plugin: 304 | dependency: transitive 305 | description: 306 | name: plugin 307 | url: "https://pub.dartlang.org" 308 | source: hosted 309 | version: "0.2.0+3" 310 | pool: 311 | dependency: transitive 312 | description: 313 | name: pool 314 | url: "https://pub.dartlang.org" 315 | source: hosted 316 | version: "1.3.6" 317 | pub_semver: 318 | dependency: transitive 319 | description: 320 | name: pub_semver 321 | url: "https://pub.dartlang.org" 322 | source: hosted 323 | version: "1.4.2" 324 | pubspec_parse: 325 | dependency: transitive 326 | description: 327 | name: pubspec_parse 328 | url: "https://pub.dartlang.org" 329 | source: hosted 330 | version: "0.1.3" 331 | quiver: 332 | dependency: transitive 333 | description: 334 | name: quiver 335 | url: "https://pub.dartlang.org" 336 | source: hosted 337 | version: "2.0.1" 338 | redux: 339 | dependency: transitive 340 | description: 341 | name: redux 342 | url: "https://pub.dartlang.org" 343 | source: hosted 344 | version: "3.0.0" 345 | redux_logging: 346 | dependency: "direct main" 347 | description: 348 | name: redux_logging 349 | url: "https://pub.dartlang.org" 350 | source: hosted 351 | version: "0.3.0" 352 | shared_preferences: 353 | dependency: "direct main" 354 | description: 355 | name: shared_preferences 356 | url: "https://pub.dartlang.org" 357 | source: hosted 358 | version: "0.4.3" 359 | shelf: 360 | dependency: transitive 361 | description: 362 | name: shelf 363 | url: "https://pub.dartlang.org" 364 | source: hosted 365 | version: "0.7.4" 366 | shelf_web_socket: 367 | dependency: transitive 368 | description: 369 | name: shelf_web_socket 370 | url: "https://pub.dartlang.org" 371 | source: hosted 372 | version: "0.2.2+4" 373 | sky_engine: 374 | dependency: transitive 375 | description: flutter 376 | source: sdk 377 | version: "0.0.99" 378 | source_gen: 379 | dependency: transitive 380 | description: 381 | name: source_gen 382 | url: "https://pub.dartlang.org" 383 | source: hosted 384 | version: "0.9.3" 385 | source_span: 386 | dependency: transitive 387 | description: 388 | name: source_span 389 | url: "https://pub.dartlang.org" 390 | source: hosted 391 | version: "1.4.1" 392 | stack_trace: 393 | dependency: transitive 394 | description: 395 | name: stack_trace 396 | url: "https://pub.dartlang.org" 397 | source: hosted 398 | version: "1.9.3" 399 | stream_channel: 400 | dependency: transitive 401 | description: 402 | name: stream_channel 403 | url: "https://pub.dartlang.org" 404 | source: hosted 405 | version: "1.6.8" 406 | stream_transform: 407 | dependency: transitive 408 | description: 409 | name: stream_transform 410 | url: "https://pub.dartlang.org" 411 | source: hosted 412 | version: "0.0.14+1" 413 | string_scanner: 414 | dependency: transitive 415 | description: 416 | name: string_scanner 417 | url: "https://pub.dartlang.org" 418 | source: hosted 419 | version: "1.0.4" 420 | timing: 421 | dependency: transitive 422 | description: 423 | name: timing 424 | url: "https://pub.dartlang.org" 425 | source: hosted 426 | version: "0.1.1+1" 427 | typed_data: 428 | dependency: transitive 429 | description: 430 | name: typed_data 431 | url: "https://pub.dartlang.org" 432 | source: hosted 433 | version: "1.1.6" 434 | utf: 435 | dependency: transitive 436 | description: 437 | name: utf 438 | url: "https://pub.dartlang.org" 439 | source: hosted 440 | version: "0.9.0+5" 441 | vector_math: 442 | dependency: transitive 443 | description: 444 | name: vector_math 445 | url: "https://pub.dartlang.org" 446 | source: hosted 447 | version: "2.0.8" 448 | watcher: 449 | dependency: transitive 450 | description: 451 | name: watcher 452 | url: "https://pub.dartlang.org" 453 | source: hosted 454 | version: "0.9.7+10" 455 | web_socket_channel: 456 | dependency: transitive 457 | description: 458 | name: web_socket_channel 459 | url: "https://pub.dartlang.org" 460 | source: hosted 461 | version: "1.0.9" 462 | yaml: 463 | dependency: transitive 464 | description: 465 | name: yaml 466 | url: "https://pub.dartlang.org" 467 | source: hosted 468 | version: "2.1.15" 469 | sdks: 470 | dart: ">=2.1.0-dev.5.0 <3.0.0" 471 | flutter: ">=0.1.4 <2.0.0" 472 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_redux_starter 2 | version: 0.0.1 3 | description: 4 | author: 5 | homepage: 6 | 7 | dependencies: 8 | flutter: 9 | sdk: flutter 10 | flutter_redux: ^0.5.2 11 | redux_logging: ^0.3.0 12 | http: ^0.11.3+17 13 | path_provider: ^0.4.1 14 | shared_preferences: ^0.4.3 15 | built_value: ^6.1.4 16 | built_collection: ^4.0.0 17 | memoize: ^2.0.0 18 | 19 | # The following adds the Cupertino Icons font to your application. 20 | # Use with the CupertinoIcons class for iOS style icons. 21 | cupertino_icons: ^0.1.2 22 | 23 | dev_dependencies: 24 | #flutter_driver: 25 | #sdk: flutter 26 | #flutter_test: 27 | #sdk: flutter 28 | build_runner: ^1.0.0 29 | built_value_generator: ^6.1.4 30 | 31 | 32 | # For information on the generic Dart part of this file, see the 33 | # following page: https://www.dartlang.org/tools/pub/pubspec 34 | 35 | # The following section is specific to Flutter. 36 | flutter: 37 | 38 | # The following line ensures that the Material Icons font is 39 | # included with your application, so that you can use the icons in 40 | # the material Icons class. 41 | uses-material-design: true 42 | 43 | # To add assets to your application, add an assets section, like this: 44 | # assets: 45 | # - images/a_dot_burr.jpeg 46 | # - images/a_dot_ham.jpeg 47 | 48 | # An image asset can refer to one or more resolution-specific "variants", see 49 | # https://flutter.io/assets-and-images/#resolution-aware. 50 | 51 | # For details regarding adding assets from package dependencies, see 52 | # https://flutter.io/assets-and-images/#from-packages 53 | 54 | # To add custom fonts to your application, add a fonts section here, 55 | # in this "flutter" section. Each entry in this list should have a 56 | # "family" key with the font family name, and a "fonts" key with a 57 | # list giving the asset and other descriptors for the font. For 58 | # example: 59 | # fonts: 60 | # - family: Schyler 61 | # fonts: 62 | # - asset: fonts/Schyler-Regular.ttf 63 | # - asset: fonts/Schyler-Italic.ttf 64 | # style: italic 65 | # - family: Trajan Pro 66 | # fonts: 67 | # - asset: fonts/TrajanPro.ttf 68 | # - asset: fonts/TrajanPro_Bold.ttf 69 | # weight: 700 70 | # 71 | # For details regarding fonts from package dependencies, 72 | # see https://flutter.io/custom-fonts/#from-packages 73 | -------------------------------------------------------------------------------- /starter.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Flutter/Redux Starter by @hillelcoren" 4 | 5 | [ $# -eq 0 ] && { echo "Usage: $0 init or $0 make "; exit 1; } 6 | 7 | action="$1" 8 | lineBreak='\'$'\n' 9 | 10 | if [ ${action} = "cleanup" ] || [ ${action} = "clean" ]; then 11 | 12 | echo "Cleaning temporary built files.." 13 | find . -name "*-e" -type f -delete 14 | 15 | elif [ ${action} = "build" ]; then 16 | 17 | echo "Generating built files.." 18 | flutter packages pub run build_runner clean 19 | flutter packages pub run build_runner build --delete-conflicting-outputs 20 | 21 | elif [ ${action} = "watch" ]; then 22 | 23 | echo "Watching for changes and generating built files.." 24 | # flutter packages pub run build_runner clean 25 | flutter packages pub run build_runner watch --delete-conflicting-outputs 26 | 27 | elif [ ${action} = "init" ]; then 28 | 29 | company="$2" 30 | package="$3" 31 | url="$4" 32 | 33 | echo "Company: $company" 34 | echo "Package: $package" 35 | echo "URL: $url" 36 | 37 | flutter pub get 38 | 39 | echo "Creating files..." 40 | 41 | sed -i -e "s,__API_URL__,$url,g" ./lib/constants.dart 42 | 43 | mv "./android/app/src/main/java/com/hillelcoren" "./android/app/src/main/java/com/$company" 44 | mv "./android/app/src/main/java/com/$company/flutterreduxstarter" "./android/app/src/main/java/com/$company/$package" 45 | 46 | # Replace 'hillelcoren' 47 | declare -a files=( 48 | './ios/Runner.xcodeproj/project.pbxproj' 49 | './android/app/build.gradle' 50 | './android/app/src/main/AndroidManifest.xml' 51 | "./android/app/src/main/java/com/$company/$package/MainActivity.java") 52 | 53 | for i in "${files[@]}" 54 | do 55 | sed -i -e "s/hillelcoren/$company/g" $i 56 | done 57 | 58 | # Replace 'flutterReduxStarter' 59 | declare -a files=( 60 | "./android/app/src/main/java/com/$company/$package/MainActivity.java") 61 | 62 | for i in "${files[@]}" 63 | do 64 | sed -i -e "s/flutterReduxStarter/$package/g" $i 65 | done 66 | 67 | # Replace 'flutterreduxstarter' 68 | declare -a files=( 69 | "./ios/Runner.xcodeproj/project.pbxproj") 70 | 71 | for i in "${files[@]}" 72 | do 73 | sed -i -e "s/flutterreduxstarter/$package/g" $i 74 | done 75 | 76 | declare -a files=( 77 | './.packages' 78 | './pubspec.yaml' 79 | './ios/Runner/Info.plist' 80 | './ios/Flutter/Generated.xcconfig' 81 | './android/app/build.gradle' 82 | './android/app/src/main/AndroidManifest.xml' 83 | './lib/main.dart' 84 | './lib/redux/app/app_state.dart' 85 | './lib/redux/app/app_reducer.dart' 86 | './lib/redux/app/app_actions.dart' 87 | './lib/redux/app/app_middleware.dart' 88 | './lib/redux/app/data_reducer.dart' 89 | './lib/redux/auth/auth_state.dart' 90 | './lib/redux/auth/auth_actions.dart' 91 | './lib/redux/auth/auth_middleware.dart' 92 | './lib/redux/auth/auth_reducer.dart' 93 | './lib/redux/ui/ui_actions.dart' 94 | './lib/redux/ui/ui_reducer.dart' 95 | './lib/redux/ui/entity_ui_state.dart' 96 | './lib/redux/ui/list_ui_state.dart' 97 | './lib/data/repositories/auth_repository.dart' 98 | './lib/data/repositories/persistence_repository.dart' 99 | './lib/data/models/serializers.dart' 100 | './test/login_test.dart' 101 | './lib/redux/ui/ui_state.dart' 102 | './lib/ui/auth/login.dart' 103 | './lib/ui/auth/login_vm.dart' 104 | './lib/ui/app/app_drawer.dart' 105 | './lib/ui/app/init.dart' 106 | './lib/ui/app/app_drawer_vm.dart' 107 | './lib/ui/app/actions_menu_button.dart' 108 | './lib/ui/app/app_bottom_bar.dart' 109 | './lib/ui/app/app_search.dart' 110 | './lib/ui/app/app_search_button.dart' 111 | './lib/ui/app/dismissible_entity.dart' 112 | './lib/ui/home/home_screen.dart' 113 | './stubs/data/models/stub_model' 114 | './stubs/data/repositories/stub_repository' 115 | './stubs/redux/stub/stub_actions' 116 | './stubs/redux/stub/stub_reducer' 117 | './stubs/redux/stub/stub_state' 118 | './stubs/redux/stub/stub_middleware' 119 | './stubs/redux/stub/stub_selectors' 120 | './stubs/ui/stub/edit/stub_edit' 121 | './stubs/ui/stub/edit/stub_edit_vm' 122 | './stubs/ui/stub/view/stub_view' 123 | './stubs/ui/stub/view/stub_view_vm' 124 | './stubs/ui/stub/stub_item' 125 | './stubs/ui/stub/stub_list_vm' 126 | './stubs/ui/stub/stub_list' 127 | './stubs/ui/stub/stub_screen') 128 | 129 | for i in "${files[@]}" 130 | do 131 | sed -i -e "s/flutter_redux_starter/$package/g" $i 132 | done 133 | 134 | elif [ ${action} = "make" ]; then 135 | 136 | package="$2" 137 | module="$3" 138 | Module="$(tr '[:lower:]' '[:upper:]' <<< ${module:0:1})${module:1}" 139 | fields="$4" 140 | IFS=', ' read -r -a fieldsArray <<< "$fields" 141 | 142 | echo "Make..." 143 | echo "Creating $module module" 144 | 145 | # Create new directories 146 | if [ ! -d "lib/redux/$module" ] 147 | then 148 | echo "Creating directory: lib/redux/$module" 149 | mkdir "lib/redux/$module" 150 | fi 151 | 152 | if [ ! -d "lib/ui/$module" ] 153 | then 154 | echo "Creating directory: lib/ui/$module" 155 | mkdir "lib/ui/$module" 156 | fi 157 | 158 | if [ ! -d "lib/ui/$module/view" ] 159 | then 160 | echo "Creating directory: lib/ui/$module/view" 161 | mkdir "lib/ui/$module/view" 162 | fi 163 | 164 | if [ ! -d "lib/ui/$module/edit" ] 165 | then 166 | echo "Creating directory: lib/ui/$module/edit" 167 | mkdir "lib/ui/$module/edit" 168 | fi 169 | 170 | # Create new module files 171 | declare -a files=( 172 | './stubs/data/models/stub_model' 173 | './stubs/data/repositories/stub_repository' 174 | './stubs/redux/stub/stub_actions' 175 | './stubs/redux/stub/stub_reducer' 176 | './stubs/redux/stub/stub_state' 177 | './stubs/redux/stub/stub_middleware' 178 | './stubs/redux/stub/stub_selectors' 179 | './stubs/ui/stub/edit/stub_edit' 180 | './stubs/ui/stub/edit/stub_edit_vm' 181 | './stubs/ui/stub/view/stub_view' 182 | './stubs/ui/stub/view/stub_view_vm' 183 | './stubs/ui/stub/stub_item' 184 | './stubs/ui/stub/stub_list_vm' 185 | './stubs/ui/stub/stub_list' 186 | './stubs/ui/stub/stub_screen') 187 | 188 | for i in "${files[@]}" 189 | do 190 | filename=$(echo $i | sed "s/stubs/lib/g" | sed "s/stub/$module/g") 191 | echo "Creating file: $filename.dart" 192 | cp $i "$filename.dart" 193 | sed -i -e "s/stub/$module/g" "$filename.dart" 194 | sed -i -e "s/Stub/$Module/g" "$filename.dart" 195 | done 196 | 197 | # Link in new module 198 | comment="STARTER: import - do not remove comment" 199 | code="import 'package:${package}\/redux\/${module}\/${module}_state.dart';${lineBreak}" 200 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/redux/app/app_state.dart 201 | 202 | comment="STARTER: states switch - do not remove comment" 203 | code="case EntityType.${module}:${lineBreak}return ${module}UIState;${lineBreak}" 204 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/redux/app/app_state.dart 205 | 206 | comment="STARTER: state getters - do not remove comment" 207 | code="${Module}State get ${module}State => this.dataState.${module}State;${lineBreak}" 208 | code="${code}ListUIState get ${module}ListState => this.uiState.${module}UIState.listUIState;${lineBreak}" 209 | code="${code}${Module}UIState get ${module}UIState => this.uiState.${module}UIState;${lineBreak}${lineBreak}" 210 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/redux/app/app_state.dart 211 | 212 | for (( idx=${#fieldsArray[@]}-1 ; idx>=0 ; idx-- )) ; do 213 | elements="${fieldsArray[idx]}" 214 | IFS=':' read -r -a elementArray <<< "$elements" 215 | 216 | element="${elementArray[0]}" 217 | type="${elementArray[1]}" 218 | Element="$(tr '[:lower:]' '[:upper:]' <<< ${element:0:1})${element:1}" 219 | 220 | comment="STARTER: fields - do not remove comment" 221 | code="static const String ${element} = '${element}';${lineBreak}" 222 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" "./lib/data/models/${module}_model.dart" 223 | 224 | comment="STARTER: properties - do not remove comment" 225 | code="String get ${element};${lineBreak}" 226 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" "./lib/data/models/${module}_model.dart" 227 | 228 | comment="STARTER: sort switch - do not remove comment" 229 | code="case ${Module}Fields.${element}:${lineBreak}" 230 | code="${code}response = ${module}A.${element}.compareTo(${module}B.${element});${lineBreak}break;${lineBreak}" 231 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" "./lib/data/models/${module}_model.dart" 232 | 233 | comment="STARTER: search - do not remove comment" 234 | code="if (${element}.toLowerCase().contains(search)){${lineBreak}" 235 | code="${code}return true;${lineBreak}" 236 | code="${code}}${lineBreak}" 237 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" "./lib/data/models/${module}_model.dart" 238 | 239 | comment="STARTER: constructor - do not remove comment" 240 | code="${element}: '',${lineBreak}" 241 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" "./lib/data/models/${module}_model.dart" 242 | 243 | comment="STARTER: controllers - do not remove comment" 244 | code="final _${element}Controller = TextEditingController();${lineBreak}" 245 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" "./lib/ui/${module}/edit/${module}_edit.dart" 246 | 247 | comment="STARTER: array - do not remove comment" 248 | code="_${element}Controller,${lineBreak}" 249 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" "./lib/ui/${module}/edit/${module}_edit.dart" 250 | 251 | comment="STARTER: read value - do not remove comment" 252 | code="_${element}Controller.text = ${module}.${element};${lineBreak}" 253 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" "./lib/ui/${module}/edit/${module}_edit.dart" 254 | 255 | comment="STARTER: set value - do not remove comment" 256 | code="..${element} = _${element}Controller.text.trim()${lineBreak}" 257 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" "./lib/ui/${module}/edit/${module}_edit.dart" 258 | 259 | comment="STARTER: widgets - do not remove comment" 260 | code="TextFormField(${lineBreak}" 261 | code="${code}controller: _${element}Controller,${lineBreak}" 262 | code="${code}autocorrect: false,${lineBreak}" 263 | if [ "$type" = "textarea" ]; then 264 | code="${code}maxLines: 4,${lineBreak}" 265 | fi 266 | code="${code}decoration: InputDecoration(${lineBreak}" 267 | code="${code}labelText: '${Element}',${lineBreak}" 268 | code="${code}),${lineBreak}" 269 | code="${code}),${lineBreak}" 270 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" "./lib/ui/${module}/edit/${module}_edit.dart" 271 | 272 | comment="STARTER: widgets - do not remove comment" 273 | if [ ${element} = ${fieldsArray[0]} ]; then 274 | code="Text(${module}.${element}, style: Theme.of(context).textTheme.title),${lineBreak}" 275 | code="${code}SizedBox(height: 12.0),${lineBreak}" 276 | else 277 | code="Text(${module}.${element})," 278 | fi 279 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" "./lib/ui/${module}/view/${module}_view.dart" 280 | 281 | comment="STARTER: sort - do not remove comment" 282 | code="${Module}Fields.${element},${lineBreak}" 283 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" "./lib/ui/${module}/${module}_screen.dart" 284 | 285 | if [ "$idx" -eq 0 ]; then 286 | comment="STARTER: sort default - do not remove comment" 287 | code="return ${module}A.${element}.compareTo(${module}B.${element});${lineBreak}" 288 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" "./lib/data/models/${module}_model.dart" 289 | 290 | comment="STARTER: display name - do not remove comment" 291 | code="return ${element};${lineBreak}" 292 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" "./lib/data/models/${module}_model.dart" 293 | fi 294 | 295 | if [ "$idx" -eq 1 ]; then 296 | comment="STARTER: subtitle - do not remove comment" 297 | code="subtitle: Text(${module}.${element}, maxLines: 4),${lineBreak}" 298 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" "./lib/ui/${module}/${module}_item.dart" 299 | fi 300 | done 301 | 302 | 303 | comment="STARTER: import - do not remove comment" 304 | code="import 'package:${package}\/ui\/${module}\/${module}_screen.dart';${lineBreak}" 305 | code="${code}import 'package:${package}\/ui\/${module}\/edit\/${module}_edit_vm.dart';${lineBreak}" 306 | code="${code}import 'package:${package}\/ui\/${module}\/view\/${module}_view_vm.dart';${lineBreak}" 307 | code="${code}import 'package:${package}\/redux\/${module}\/${module}_actions.dart';${lineBreak}" 308 | code="${code}import 'package:${package}\/redux\/${module}\/${module}_middleware.dart';${lineBreak}" 309 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/main.dart 310 | 311 | comment="STARTER: middleware - do not remove comment" 312 | code="..addAll(createStore${Module}sMiddleware())${lineBreak}" 313 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/main.dart 314 | 315 | comment="STARTER: routes - do not remove comment" 316 | code="${Module}Screen.route: (context) {${lineBreak}" 317 | code="${code}widget.store.dispatch(Load${Module}s());${lineBreak}" 318 | code="${code}return ${Module}Screen();${lineBreak}" 319 | code="${code}},${lineBreak}" 320 | code="${code}${Module}ViewScreen.route: (context) => ${Module}ViewScreen(),${lineBreak}" 321 | code="${code}${Module}EditScreen.route: (context) => ${Module}EditScreen(),${lineBreak}" 322 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/main.dart 323 | 324 | comment="STARTER: import - do not remove comment" 325 | code="import 'package:${package}\/data\/models\/${module}_model.dart';${lineBreak}" 326 | code="${code}import 'package:${package}\/redux\/${module}\/${module}_state.dart';${lineBreak}" 327 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/data/models/serializers.dart 328 | 329 | comment="STARTER: serializers - do not remove comment" 330 | code="${Module}Entity,${lineBreak}" 331 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/data/models/serializers.dart 332 | 333 | comment="STARTER: import - do not remove comment" 334 | code="import 'package:${package}\/redux\/${module}\/${module}_state.dart';${lineBreak}" 335 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/redux/app/data_state.dart 336 | 337 | comment="STARTER: fields - do not remove comment" 338 | code="${Module}State get ${module}State;${lineBreak}" 339 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/redux/app/data_state.dart 340 | 341 | comment="STARTER: constructor - do not remove comment" 342 | code="${module}State: ${Module}State(),${lineBreak}" 343 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/redux/app/data_state.dart 344 | 345 | comment="STARTER: import - do not remove comment" 346 | code="import 'package:${package}\/redux\/${module}\/${module}_reducer.dart';${lineBreak}" 347 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/redux/app/data_reducer.dart 348 | 349 | comment="STARTER: reducer - do not remove comment" 350 | code="..${module}State.replace(${module}sReducer(state.${module}State, action))${lineBreak}" 351 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/redux/app/data_reducer.dart 352 | 353 | comment="STARTER: import - do not remove comment" 354 | code="import 'package:${package}\/redux\/${module}\/${module}_actions.dart';${lineBreak}" 355 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/ui/app/app_drawer.dart 356 | 357 | comment="STARTER: menu - do not remove comment" 358 | code="ListTile(${lineBreak}" 359 | code="${code}leading: Icon(Icons.widgets),${lineBreak}" 360 | code="${code}title: Text('${Module}s'),${lineBreak}" 361 | code="${code}onTap: () => store.dispatch(View${Module}List(context)),${lineBreak}" 362 | code="${code}),${lineBreak}" 363 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/ui/app/app_drawer.dart 364 | 365 | comment="STARTER: types - do not remove comment" 366 | code="static const EntityType ${module} = _$" 367 | code="${code}${module};${lineBreak}" 368 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/data/models/models.dart 369 | 370 | comment="STARTER: import - do not remove comment" 371 | code="import 'package:${package}\/redux\/${module}\/${module}_state.dart';${lineBreak}" 372 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/redux/ui/ui_state.dart 373 | 374 | comment="STARTER: properties - do not remove comment" 375 | code="${Module}UIState get ${module}UIState;${lineBreak}" 376 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/redux/ui/ui_state.dart 377 | 378 | comment="STARTER: constructor - do not remove comment" 379 | code="${module}UIState: ${Module}UIState(),${lineBreak}" 380 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/redux/ui/ui_state.dart 381 | 382 | comment="STARTER: import - do not remove comment" 383 | code="import 'package:${package}\/redux\/${module}\/${module}_reducer.dart';${lineBreak}" 384 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/redux/ui/ui_reducer.dart 385 | 386 | comment="STARTER: reducer - do not remove comment" 387 | code="..${module}UIState.replace(${module}UIReducer(state.${module}UIState, action))${lineBreak}" 388 | sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/redux/ui/ui_reducer.dart 389 | 390 | echo "Generating built files.." 391 | flutter packages pub run build_runner clean 392 | flutter packages pub run build_runner build --delete-conflicting-outputs 393 | fi 394 | 395 | echo "Successfully completed" 396 | -------------------------------------------------------------------------------- /stubs/data/models/stub_model: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:flutter_redux_starter/data/models/models.dart'; 4 | 5 | part 'stub_model.g.dart'; 6 | 7 | class StubFields { 8 | // STARTER: fields - do not remove comment 9 | } 10 | 11 | abstract class StubEntity extends Object with BaseEntity implements Built { 12 | 13 | // STARTER: properties - do not remove comment 14 | 15 | static int counter = 0; 16 | factory StubEntity() { 17 | return _$StubEntity._( 18 | id: 0, 19 | // STARTER: constructor - do not remove comment 20 | ); 21 | } 22 | 23 | String get displayName { 24 | // STARTER: display name - do not remove comment 25 | } 26 | 27 | int compareTo(StubEntity stub, String sortField, bool sortAscending) { 28 | int response = 0; 29 | StubEntity stubA = sortAscending ? this : stub; 30 | StubEntity stubB = sortAscending ? stub: this; 31 | 32 | switch (sortField) { 33 | // STARTER: sort switch - do not remove comment 34 | } 35 | 36 | if (response == 0) { 37 | // STARTER: sort default - do not remove comment 38 | } else { 39 | return response; 40 | } 41 | } 42 | 43 | bool matchesSearch(String search) { 44 | if (search == null || search.isEmpty) { 45 | return true; 46 | } 47 | 48 | search = search.toLowerCase(); 49 | 50 | // STARTER: search - do not remove comment 51 | 52 | return false; 53 | } 54 | 55 | StubEntity._(); 56 | static Serializer get serializer => _$stubEntitySerializer; 57 | } 58 | -------------------------------------------------------------------------------- /stubs/data/repositories/stub_repository: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:core'; 3 | import 'dart:convert'; 4 | import 'package:built_collection/built_collection.dart'; 5 | import 'package:flutter_redux_starter/data/models/models.dart'; 6 | import 'package:flutter_redux_starter/data/models/serializers.dart'; 7 | import 'package:flutter_redux_starter/redux/auth/auth_state.dart'; 8 | import 'package:flutter_redux_starter/data/models/stub_model.dart'; 9 | import 'package:flutter_redux_starter/data/web_client.dart'; 10 | import 'package:flutter_redux_starter/constants.dart'; 11 | 12 | class StubRepository { 13 | final WebClient webClient; 14 | 15 | const StubRepository({ 16 | this.webClient = const WebClient(), 17 | }); 18 | 19 | Future> loadList(AuthState auth) async { 20 | final response = await webClient.get(kApiUrl + '/stubs'); 21 | 22 | var list = new BuiltList(response.map((stub) { 23 | return serializers.deserializeWith(StubEntity.serializer, stub); 24 | })); 25 | 26 | return list; 27 | } 28 | 29 | Future saveData(AuthState auth, StubEntity stub, [EntityAction action]) async { 30 | 31 | var data = serializers.serializeWith(StubEntity.serializer, stub); 32 | var response; 33 | 34 | if (stub.isNew) { 35 | response = await webClient.post( 36 | kApiUrl + '/stubs', json.encode(data)); 37 | } else { 38 | var url = kApiUrl + '/stubs/' + stub.id.toString(); 39 | response = await webClient.put(url, json.encode(data)); 40 | } 41 | 42 | return serializers.deserializeWith(StubEntity.serializer, response); 43 | } 44 | } -------------------------------------------------------------------------------- /stubs/redux/stub/stub_actions: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:built_collection/built_collection.dart'; 4 | import 'package:flutter_redux_starter/data/models/stub_model.dart'; 5 | import 'package:flutter_redux_starter/redux/app/app_actions.dart'; 6 | 7 | 8 | class ViewStubList implements PersistUI { 9 | final BuildContext context; 10 | ViewStubList(this.context); 11 | } 12 | 13 | class ViewStub implements PersistUI { 14 | final StubEntity stub; 15 | final BuildContext context; 16 | ViewStub({this.stub, this.context}); 17 | } 18 | 19 | class EditStub implements PersistUI { 20 | final StubEntity stub; 21 | final BuildContext context; 22 | EditStub({this.stub, this.context}); 23 | } 24 | 25 | 26 | class LoadStubs { 27 | final Completer completer; 28 | final bool force; 29 | 30 | LoadStubs([this.completer, this.force = false]); 31 | } 32 | 33 | class LoadStubsRequest implements StartLoading {} 34 | 35 | class LoadStubsFailure implements StopLoading { 36 | final dynamic error; 37 | LoadStubsFailure(this.error); 38 | 39 | @override 40 | String toString() { 41 | return 'LoadStubsFailure{error: $error}'; 42 | } 43 | } 44 | 45 | class LoadStubsSuccess implements StopLoading, PersistData { 46 | final BuiltList stubs; 47 | LoadStubsSuccess(this.stubs); 48 | 49 | @override 50 | String toString() { 51 | return 'LoadStubsSuccess{stubs: $stubs}'; 52 | } 53 | } 54 | 55 | 56 | class UpdateStub implements PersistUI { 57 | final StubEntity stub; 58 | UpdateStub(this.stub); 59 | } 60 | 61 | class SaveStubRequest implements StartLoading { 62 | final Completer completer; 63 | final StubEntity stub; 64 | SaveStubRequest({this.completer, this.stub}); 65 | } 66 | 67 | class AddStubSuccess implements StopLoading, PersistData { 68 | final StubEntity stub; 69 | AddStubSuccess(this.stub); 70 | } 71 | 72 | 73 | class SaveStubSuccess implements StopLoading, PersistData { 74 | final StubEntity stub; 75 | 76 | SaveStubSuccess(this.stub); 77 | } 78 | 79 | class SaveStubFailure implements StopLoading { 80 | final String error; 81 | SaveStubFailure (this.error); 82 | } 83 | 84 | class DeleteStubRequest implements StartLoading { 85 | final Completer completer; 86 | final int stubId; 87 | 88 | DeleteStubRequest(this.completer, this.stubId); 89 | } 90 | 91 | class DeleteStubSuccess implements StopLoading, PersistData { 92 | final StubEntity stub; 93 | DeleteStubSuccess(this.stub); 94 | } 95 | 96 | class DeleteStubFailure implements StopLoading { 97 | final StubEntity stub; 98 | DeleteStubFailure(this.stub); 99 | } 100 | 101 | 102 | 103 | class SearchStubs { 104 | final String search; 105 | SearchStubs(this.search); 106 | } 107 | 108 | class SortStubs implements PersistUI { 109 | final String field; 110 | SortStubs(this.field); 111 | } 112 | -------------------------------------------------------------------------------- /stubs/redux/stub/stub_middleware: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:redux/redux.dart'; 3 | import 'package:flutter_redux_starter/ui/stub/stub_screen.dart'; 4 | import 'package:flutter_redux_starter/data/models/models.dart'; 5 | import 'package:flutter_redux_starter/redux/stub/stub_actions.dart'; 6 | import 'package:flutter_redux_starter/redux/ui/ui_actions.dart'; 7 | import 'package:flutter_redux_starter/ui/stub/edit/stub_edit_vm.dart'; 8 | import 'package:flutter_redux_starter/ui/stub/view/stub_view_vm.dart'; 9 | import 'package:flutter_redux_starter/redux/app/app_state.dart'; 10 | import 'package:flutter_redux_starter/data/repositories/stub_repository.dart'; 11 | 12 | List> createStoreStubsMiddleware([ 13 | StubRepository repository = const StubRepository(), 14 | ]) { 15 | final viewStubList = _viewStubList(); 16 | final viewStub = _viewStub(); 17 | final editStub = _editStub(); 18 | final loadStubs = _loadStubs(repository); 19 | final saveStub = _saveStub(repository); 20 | final deleteStub = _deleteStub(repository); 21 | 22 | return [ 23 | TypedMiddleware(viewStubList), 24 | TypedMiddleware(viewStub), 25 | TypedMiddleware(editStub), 26 | TypedMiddleware(loadStubs), 27 | TypedMiddleware(saveStub), 28 | TypedMiddleware(deleteStub), 29 | ]; 30 | } 31 | 32 | Middleware _viewStubList() { 33 | return (Store store, action, NextDispatcher next) { 34 | next(action); 35 | 36 | store.dispatch(UpdateCurrentRoute(StubScreen.route)); 37 | Navigator.of(action.context).pushReplacementNamed(StubScreen.route); 38 | }; 39 | } 40 | 41 | Middleware _viewStub() { 42 | return (Store store, action, NextDispatcher next) { 43 | next(action); 44 | 45 | store.dispatch(UpdateCurrentRoute(StubViewScreen.route)); 46 | Navigator.of(action.context).pushNamed(StubViewScreen.route); 47 | }; 48 | } 49 | 50 | Middleware _editStub() { 51 | return (Store store, action, NextDispatcher next) { 52 | next(action); 53 | 54 | store.dispatch(UpdateCurrentRoute(StubEditScreen.route)); 55 | Navigator.of(action.context).pushNamed(StubEditScreen.route); 56 | }; 57 | } 58 | 59 | 60 | Middleware _deleteStub(StubRepository repository) { 61 | return (Store store, action, NextDispatcher next) { 62 | var origStub = store.state.stubState.map[action.stubId]; 63 | repository 64 | .saveData(store.state.authState, 65 | origStub, EntityAction.delete) 66 | .then((stub) { 67 | store.dispatch(DeleteStubSuccess(stub)); 68 | if (action.completer != null) { 69 | action.completer.complete(null); 70 | } 71 | }).catchError((error) { 72 | print(error); 73 | store.dispatch(DeleteStubFailure(origStub)); 74 | }); 75 | 76 | next(action); 77 | }; 78 | } 79 | 80 | Middleware _saveStub(StubRepository repository) { 81 | return (Store store, action, NextDispatcher next) { 82 | repository 83 | .saveData( 84 | store.state.authState, action.stub) 85 | .then((stub) { 86 | if (action.stub.isNew) { 87 | store.dispatch(AddStubSuccess(stub)); 88 | } else { 89 | store.dispatch(SaveStubSuccess(stub)); 90 | } 91 | action.completer.complete(null); 92 | }).catchError((error) { 93 | print(error); 94 | store.dispatch(SaveStubFailure(error)); 95 | }); 96 | 97 | next(action); 98 | }; 99 | } 100 | 101 | Middleware _loadStubs(StubRepository repository) { 102 | return (Store store, action, NextDispatcher next) { 103 | 104 | AppState state = store.state; 105 | 106 | if (!state.stubState.isStale && !action.force) { 107 | next(action); 108 | return; 109 | } 110 | 111 | if (state.isLoading) { 112 | next(action); 113 | return; 114 | } 115 | 116 | store.dispatch(LoadStubsRequest()); 117 | repository 118 | .loadList(state.authState) 119 | .then((data) { 120 | store.dispatch(LoadStubsSuccess(data)); 121 | 122 | if (action.completer != null) { 123 | action.completer.complete(null); 124 | } 125 | }).catchError((error) { 126 | print(error); 127 | store.dispatch(LoadStubsFailure(error)); 128 | }); 129 | 130 | next(action); 131 | }; 132 | } 133 | -------------------------------------------------------------------------------- /stubs/redux/stub/stub_reducer: -------------------------------------------------------------------------------- 1 | import 'package:redux/redux.dart'; 2 | import 'package:flutter_redux_starter/data/models/stub_model.dart'; 3 | import 'package:flutter_redux_starter/redux/ui/entity_ui_state.dart'; 4 | import 'package:flutter_redux_starter/redux/ui/list_ui_state.dart'; 5 | import 'package:flutter_redux_starter/redux/stub/stub_actions.dart'; 6 | import 'package:flutter_redux_starter/redux/stub/stub_state.dart'; 7 | 8 | EntityUIState stubUIReducer(StubUIState state, action) { 9 | return state.rebuild((b) => b 10 | ..listUIState.replace(stubListReducer(state.listUIState, action)) 11 | ..selected.replace(editingReducer(state.selected, action)) 12 | ); 13 | } 14 | 15 | final editingReducer = combineReducers([ 16 | TypedReducer(_updateEditing), 17 | TypedReducer(_updateEditing), 18 | TypedReducer(_updateEditing), 19 | TypedReducer(_updateEditing), 20 | TypedReducer(_updateEditing), 21 | ]); 22 | 23 | /* 24 | StubEntity _clearEditing(StubEntity stub, action) { 25 | return StubEntity(); 26 | } 27 | */ 28 | 29 | StubEntity _updateEditing(StubEntity stub, action) { 30 | return action.stub; 31 | } 32 | 33 | final stubListReducer = combineReducers([ 34 | TypedReducer(_sortStubs), 35 | TypedReducer(_searchStubs), 36 | ]); 37 | 38 | ListUIState _searchStubs(ListUIState stubListState, SearchStubs action) { 39 | return stubListState.rebuild((b) => b 40 | ..search = action.search 41 | ); 42 | } 43 | 44 | ListUIState _sortStubs(ListUIState stubListState, SortStubs action) { 45 | return stubListState.rebuild((b) => b 46 | ..sortAscending = b.sortField != action.field || ! b.sortAscending 47 | ..sortField = action.field 48 | ); 49 | } 50 | 51 | 52 | final stubsReducer = combineReducers([ 53 | TypedReducer(_updateStub), 54 | TypedReducer(_addStub), 55 | TypedReducer(_setLoadedStubs), 56 | TypedReducer(_setNoStubs), 57 | 58 | TypedReducer(_deleteStubRequest), 59 | TypedReducer(_deleteStubSuccess), 60 | TypedReducer(_deleteStubFailure), 61 | ]); 62 | 63 | StubState _deleteStubRequest(StubState stubState, DeleteStubRequest action) { 64 | var stub = stubState.map[action.stubId].rebuild((b) => b 65 | ); 66 | 67 | return stubState.rebuild((b) => b 68 | ..map[action.stubId] = stub 69 | ); 70 | } 71 | 72 | StubState _deleteStubSuccess(StubState stubState, DeleteStubSuccess action) { 73 | return stubState.rebuild((b) => b 74 | ..map[action.stub.id] = action.stub 75 | ); 76 | } 77 | 78 | StubState _deleteStubFailure(StubState stubState, DeleteStubFailure action) { 79 | return stubState.rebuild((b) => b 80 | ..map[action.stub.id] = action.stub 81 | ); 82 | } 83 | 84 | StubState _addStub( 85 | StubState stubState, AddStubSuccess action) { 86 | return stubState.rebuild((b) => b 87 | ..map[action.stub.id] = action.stub 88 | ..list.add(action.stub.id) 89 | ); 90 | } 91 | 92 | StubState _updateStub( 93 | StubState stubState, SaveStubSuccess action) { 94 | return stubState.rebuild((b) => b 95 | ..map[action.stub.id] = action.stub 96 | ); 97 | } 98 | 99 | StubState _setNoStubs( 100 | StubState stubState, LoadStubsFailure action) { 101 | return stubState; 102 | } 103 | 104 | StubState _setLoadedStubs( 105 | StubState stubState, LoadStubsSuccess action) { 106 | return stubState.rebuild( 107 | (b) => b 108 | ..lastUpdated = DateTime.now().millisecondsSinceEpoch 109 | ..map.addAll(Map.fromIterable( 110 | action.stubs, 111 | key: (item) => item.id, 112 | value: (item) => item, 113 | )) 114 | ..list.replace(action.stubs.map( 115 | (stub) => stub.id).toList()) 116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /stubs/redux/stub/stub_selectors: -------------------------------------------------------------------------------- 1 | import 'package:memoize/memoize.dart'; 2 | import 'package:built_collection/built_collection.dart'; 3 | import 'package:flutter_redux_starter/data/models/stub_model.dart'; 4 | import 'package:flutter_redux_starter/redux/ui/list_ui_state.dart'; 5 | 6 | var memoizedStubList = memo3(( 7 | BuiltMap stubMap, 8 | BuiltList stubList, 9 | ListUIState stubListState) => visibleStubsSelector(stubMap, stubList, stubListState) 10 | ); 11 | 12 | List visibleStubsSelector( 13 | BuiltMap stubMap, 14 | BuiltList stubList, 15 | ListUIState stubListState) { 16 | 17 | var list = stubList.where((stubId) { 18 | var stub = stubMap[stubId]; 19 | return stub.matchesSearch(stubListState.search); 20 | }).toList(); 21 | 22 | list.sort((stubAId, stubBId) { 23 | var stubA = stubMap[stubAId]; 24 | var stubB = stubMap[stubBId]; 25 | return stubA.compareTo(stubB, stubListState.sortField, stubListState.sortAscending); 26 | }); 27 | 28 | return list; 29 | } -------------------------------------------------------------------------------- /stubs/redux/stub/stub_state: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:built_collection/built_collection.dart'; 4 | import 'package:flutter_redux_starter/constants.dart'; 5 | import 'package:flutter_redux_starter/data/models/stub_model.dart'; 6 | import 'package:flutter_redux_starter/redux/ui/entity_ui_state.dart'; 7 | import 'package:flutter_redux_starter/redux/ui/list_ui_state.dart'; 8 | 9 | part 'stub_state.g.dart'; 10 | 11 | abstract class StubState implements Built { 12 | 13 | @nullable 14 | int get lastUpdated; 15 | 16 | BuiltMap get map; 17 | BuiltList get list; 18 | 19 | factory StubState() { 20 | return _$StubState._( 21 | map: BuiltMap(), 22 | list: BuiltList(), 23 | ); 24 | } 25 | 26 | bool get isStale { 27 | if (! isLoaded) { 28 | return true; 29 | } 30 | 31 | return DateTime.now().millisecondsSinceEpoch - lastUpdated > kMillisecondsToRefreshData; 32 | } 33 | 34 | bool get isLoaded { 35 | return lastUpdated != null; 36 | } 37 | 38 | StubState._(); 39 | static Serializer get serializer => _$stubStateSerializer; 40 | } 41 | 42 | abstract class StubUIState extends Object with EntityUIState implements Built { 43 | 44 | @nullable 45 | StubEntity get selected; 46 | 47 | bool get isSelectedNew => selected.isNew; 48 | 49 | factory StubUIState() { 50 | return _$StubUIState._( 51 | listUIState: ListUIState(''), 52 | selected: StubEntity(), 53 | ); 54 | } 55 | 56 | StubUIState._(); 57 | static Serializer get serializer => _$stubUIStateSerializer; 58 | } -------------------------------------------------------------------------------- /stubs/ui/stub/edit/stub_edit: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_redux_starter/ui/app/form_card.dart'; 4 | import 'package:flutter_redux_starter/ui/stub/edit/stub_edit_vm.dart'; 5 | import 'package:flutter_redux_starter/ui/app/save_icon_button.dart'; 6 | 7 | class StubEdit extends StatefulWidget { 8 | final StubEditVM viewModel; 9 | 10 | StubEdit({ 11 | Key key, 12 | @required this.viewModel, 13 | }) : super(key: key); 14 | 15 | @override 16 | _StubEditState createState() => _StubEditState(); 17 | } 18 | 19 | class _StubEditState extends State { 20 | static final GlobalKey _formKey = GlobalKey(); 21 | 22 | // STARTER: controllers - do not remove comment 23 | 24 | var _controllers = []; 25 | 26 | @override 27 | void didChangeDependencies() { 28 | 29 | _controllers = [ 30 | // STARTER: array - do not remove comment 31 | ]; 32 | 33 | _controllers.forEach((controller) => controller.removeListener(_onChanged)); 34 | 35 | var stub = widget.viewModel.stub; 36 | // STARTER: read value - do not remove comment 37 | 38 | _controllers.forEach((controller) => controller.addListener(_onChanged)); 39 | 40 | super.didChangeDependencies(); 41 | } 42 | 43 | @override 44 | void dispose() { 45 | _controllers.forEach((controller) { 46 | controller.removeListener(_onChanged); 47 | controller.dispose(); 48 | }); 49 | 50 | super.dispose(); 51 | } 52 | 53 | _onChanged() { 54 | var stub = widget.viewModel.stub.rebuild((b) => b 55 | // STARTER: set value - do not remove comment 56 | ); 57 | if (stub != widget.viewModel.stub) { 58 | widget.viewModel.onChanged(stub); 59 | } 60 | } 61 | 62 | @override 63 | Widget build(BuildContext context) { 64 | var viewModel = widget.viewModel; 65 | 66 | return WillPopScope( 67 | onWillPop: () async { 68 | viewModel.onBackPressed(); 69 | return true; 70 | }, 71 | child: Scaffold( 72 | appBar: AppBar( 73 | title: Text(viewModel.stub.isNew 74 | ? 'New Stub' 75 | : viewModel.stub.displayName), 76 | actions: [ 77 | Builder(builder: (BuildContext context) { 78 | return SaveIconButton( 79 | isLoading: viewModel.isLoading, 80 | onPressed: () { 81 | if (!_formKey.currentState.validate()) { 82 | return; 83 | } 84 | 85 | viewModel.onSavePressed(context); 86 | }, 87 | ); 88 | }), 89 | ], 90 | ), 91 | body: Form( 92 | key: _formKey, 93 | child: ListView( 94 | children: [ 95 | FormCard( 96 | children: [ 97 | // STARTER: widgets - do not remove comment 98 | ], 99 | ), 100 | ], 101 | ), 102 | ), 103 | ), 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /stubs/ui/stub/edit/stub_edit_vm: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_redux/flutter_redux.dart'; 5 | import 'package:flutter_redux_starter/redux/ui/ui_actions.dart'; 6 | import 'package:flutter_redux_starter/ui/stub/stub_screen.dart'; 7 | import 'package:redux/redux.dart'; 8 | import 'package:flutter_redux_starter/redux/stub/stub_actions.dart'; 9 | import 'package:flutter_redux_starter/data/models/stub_model.dart'; 10 | import 'package:flutter_redux_starter/ui/stub/edit/stub_edit.dart'; 11 | import 'package:flutter_redux_starter/redux/app/app_state.dart'; 12 | import 'package:flutter_redux_starter/ui/app/icon_message.dart'; 13 | 14 | class StubEditScreen extends StatelessWidget { 15 | static final String route = '/stub/edit'; 16 | StubEditScreen({Key key}) : super(key: key); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return StoreConnector( 21 | converter: (Store store) { 22 | return StubEditVM.fromStore(store); 23 | }, 24 | builder: (context, vm) { 25 | return StubEdit( 26 | viewModel: vm, 27 | ); 28 | }, 29 | ); 30 | } 31 | } 32 | 33 | class StubEditVM { 34 | final StubEntity stub; 35 | final Function(StubEntity) onChanged; 36 | final Function(BuildContext) onSavePressed; 37 | final Function onBackPressed; 38 | final bool isLoading; 39 | 40 | StubEditVM({ 41 | @required this.stub, 42 | @required this.onChanged, 43 | @required this.onSavePressed, 44 | @required this.onBackPressed, 45 | @required this.isLoading, 46 | }); 47 | 48 | factory StubEditVM.fromStore(Store store) { 49 | final stub = store.state.stubUIState.selected; 50 | 51 | return StubEditVM( 52 | isLoading: store.state.isLoading, 53 | stub: stub, 54 | onChanged: (StubEntity stub) { 55 | store.dispatch(UpdateStub(stub)); 56 | }, 57 | onBackPressed: () { 58 | store.dispatch(UpdateCurrentRoute(StubScreen.route)); 59 | }, 60 | onSavePressed: (BuildContext context) { 61 | final Completer completer = new Completer(); 62 | store.dispatch(SaveStubRequest(completer: completer, stub: stub)); 63 | return completer.future.then((_) { 64 | Scaffold.of(context).showSnackBar(SnackBar( 65 | content: IconMessage( 66 | message: stub.isNew 67 | ? 'Successfully Created Stub' 68 | : 'Successfully Updated Stub', 69 | ), 70 | duration: Duration(seconds: 3))); 71 | }); 72 | }, 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /stubs/ui/stub/stub_item: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_redux_starter/data/models/stub_model.dart'; 4 | //import 'package:flutter_redux_starter/ui/app/dismissible_entity.dart'; 5 | 6 | 7 | class StubItem extends StatelessWidget { 8 | final DismissDirectionCallback onDismissed; 9 | final GestureTapCallback onTap; 10 | final StubEntity stub; 11 | 12 | static final stubItemKey = (int id) => Key('__stub_item_${id}__'); 13 | 14 | StubItem({ 15 | @required this.onDismissed, 16 | @required this.onTap, 17 | @required this.stub, 18 | }); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | /* 23 | return DismissibleEntity( 24 | entity: stub, 25 | onDismissed: onDismissed, 26 | onTap: onTap, 27 | child: ListTile( 28 | onTap: onTap, 29 | title: Container( 30 | width: MediaQuery.of(context).size.width, 31 | child: Row( 32 | children: [ 33 | Expanded( 34 | child: Text( 35 | stub.displayName, 36 | style: Theme.of(context).textTheme.title, 37 | ), 38 | ), 39 | ], 40 | ), 41 | ), 42 | // STARTER: subtitle - do not remove comment 43 | ), 44 | ); 45 | } 46 | */ 47 | 48 | return ListTile( 49 | onTap: onTap, 50 | title: Container( 51 | width: MediaQuery.of(context).size.width, 52 | child: Row( 53 | children: [ 54 | Expanded( 55 | child: Text( 56 | stub.displayName, 57 | style: Theme.of(context).textTheme.title, 58 | ), 59 | ), 60 | ], 61 | ), 62 | ), 63 | // STARTER: subtitle - do not remove comment 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /stubs/ui/stub/stub_list: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_redux_starter/ui/stub/stub_item.dart'; 4 | import 'package:flutter_redux_starter/ui/stub/stub_list_vm.dart'; 5 | 6 | class StubList extends StatelessWidget { 7 | final StubListVM viewModel; 8 | 9 | StubList({ 10 | Key key, 11 | @required this.viewModel, 12 | }) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | if (! viewModel.isLoaded) { 17 | return Center(child: CircularProgressIndicator()); 18 | } 19 | 20 | return _buildListView(context); 21 | } 22 | 23 | Widget _buildListView(BuildContext context) { 24 | return RefreshIndicator( 25 | onRefresh: () => viewModel.onRefreshed(context), 26 | child: ListView.builder( 27 | shrinkWrap: true, 28 | itemCount: viewModel.stubList.length, 29 | itemBuilder: (BuildContext context, index) { 30 | var stubId = viewModel.stubList[index]; 31 | var stub = viewModel.stubMap[stubId]; 32 | return Column(children: [ 33 | StubItem( 34 | stub: stub, 35 | onDismissed: (DismissDirection direction) => 36 | viewModel.onDismissed(context, stub, direction), 37 | onTap: () => viewModel.onStubTap(context, stub), 38 | ), 39 | Divider( 40 | height: 1.0, 41 | ), 42 | ]); 43 | }), 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /stubs/ui/stub/stub_list_vm: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:redux/redux.dart'; 3 | import 'package:built_collection/built_collection.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:flutter/widgets.dart'; 7 | import 'package:flutter_redux/flutter_redux.dart'; 8 | import 'package:flutter_redux_starter/redux/stub/stub_selectors.dart'; 9 | import 'package:flutter_redux_starter/ui/app/icon_message.dart'; 10 | import 'package:flutter_redux_starter/data/models/stub_model.dart'; 11 | import 'package:flutter_redux_starter/ui/stub/stub_list.dart'; 12 | import 'package:flutter_redux_starter/redux/app/app_state.dart'; 13 | import 'package:flutter_redux_starter/redux/stub/stub_actions.dart'; 14 | 15 | class StubListBuilder extends StatelessWidget { 16 | static final String route = '/stubs/edit'; 17 | StubListBuilder({Key key}) : super(key: key); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return StoreConnector( 22 | converter: StubListVM.fromStore, 23 | builder: (context, vm) { 24 | return StubList( 25 | viewModel: vm, 26 | ); 27 | }, 28 | ); 29 | } 30 | } 31 | 32 | class StubListVM { 33 | final List stubList; 34 | final BuiltMap stubMap; 35 | final bool isLoading; 36 | final bool isLoaded; 37 | final Function(BuildContext, StubEntity) onStubTap; 38 | final Function(BuildContext, StubEntity, DismissDirection) onDismissed; 39 | final Function(BuildContext) onRefreshed; 40 | 41 | StubListVM({ 42 | @required this.stubList, 43 | @required this.stubMap, 44 | @required this.isLoading, 45 | @required this.isLoaded, 46 | @required this.onStubTap, 47 | @required this.onDismissed, 48 | @required this.onRefreshed, 49 | }); 50 | 51 | static StubListVM fromStore(Store store) { 52 | Future _handleRefresh(BuildContext context) { 53 | final Completer completer = new Completer(); 54 | store.dispatch(LoadStubs(completer, true)); 55 | return completer.future.then((_) { 56 | Scaffold.of(context).showSnackBar(SnackBar( 57 | content: IconMessage( 58 | message: 'Refresh complete', 59 | ), 60 | duration: Duration(seconds: 3))); 61 | }); 62 | } 63 | 64 | return StubListVM( 65 | stubList: memoizedStubList(store.state.stubState.map, 66 | store.state.stubState.list, store.state.stubListState), 67 | stubMap: store.state.stubState.map, 68 | isLoading: store.state.isLoading, 69 | isLoaded: store.state.stubState.isLoaded, 70 | onStubTap: (context, stub) { 71 | store.dispatch(ViewStub(stub: stub, context: context)); 72 | }, 73 | onRefreshed: (context) => _handleRefresh(context), 74 | onDismissed: (BuildContext context, StubEntity stub, 75 | DismissDirection direction) { 76 | final Completer completer = new Completer(); 77 | store.dispatch(DeleteStubRequest(completer, stub.id)); 78 | var message = 'Successfully Deleted Stub'; 79 | return completer.future.then((_) { 80 | Scaffold.of(context).showSnackBar(SnackBar( 81 | content: IconMessage( 82 | message: message, 83 | ), 84 | duration: Duration(seconds: 3))); 85 | }); 86 | }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /stubs/ui/stub/stub_screen: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_redux/flutter_redux.dart'; 3 | import 'package:flutter_redux_starter/ui/app/app_search.dart'; 4 | import 'package:flutter_redux_starter/ui/app/app_search_button.dart'; 5 | import 'package:flutter_redux_starter/redux/app/app_state.dart'; 6 | import 'package:flutter_redux_starter/data/models/models.dart'; 7 | import 'package:flutter_redux_starter/data/models/stub_model.dart'; 8 | import 'package:flutter_redux_starter/ui/stub/stub_list_vm.dart'; 9 | import 'package:flutter_redux_starter/redux/stub/stub_actions.dart'; 10 | import 'package:flutter_redux_starter/ui/app/app_drawer_vm.dart'; 11 | import 'package:flutter_redux_starter/ui/app/app_bottom_bar.dart'; 12 | 13 | class StubScreen extends StatelessWidget { 14 | static final String route = '/stub'; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | var store = StoreProvider.of(context); 19 | 20 | return Scaffold( 21 | appBar: AppBar( 22 | title: AppSearch( 23 | entityType: EntityType.stub, 24 | onSearchChanged: (value) { 25 | store.dispatch(SearchStubs(value)); 26 | }, 27 | ), 28 | actions: [ 29 | AppSearchButton( 30 | entityType: EntityType.stub, 31 | onSearchPressed: (value) { 32 | store.dispatch(SearchStubs(value)); 33 | }, 34 | ), 35 | ], 36 | ), 37 | drawer: AppDrawerBuilder(), 38 | body: StubListBuilder(), 39 | bottomNavigationBar: AppBottomBar( 40 | entityType: EntityType.stub, 41 | onSelectedSortField: (value) { 42 | store.dispatch(SortStubs(value)); 43 | }, 44 | sortFields: [ 45 | // STARTER: sort - do not remove comment 46 | ], 47 | ), 48 | floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, 49 | floatingActionButton: FloatingActionButton( 50 | backgroundColor: Theme.of(context).primaryColorDark, 51 | onPressed: () { 52 | store.dispatch(EditStub(stub: StubEntity(), context: context)); 53 | }, 54 | child: Icon(Icons.add,color: Colors.white,), 55 | tooltip: 'New Stub', 56 | ), 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /stubs/ui/stub/view/stub_view: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_redux_starter/ui/app/actions_menu_button.dart'; 4 | import 'package:flutter_redux_starter/ui/stub/view/stub_view_vm.dart'; 5 | import 'package:flutter_redux_starter/ui/app/form_card.dart'; 6 | 7 | class StubView extends StatefulWidget { 8 | final StubViewVM viewModel; 9 | 10 | StubView({ 11 | Key key, 12 | @required this.viewModel, 13 | }) : super(key: key); 14 | 15 | @override 16 | _StubViewState createState() => new _StubViewState(); 17 | } 18 | 19 | class _StubViewState extends State { 20 | @override 21 | Widget build(BuildContext context) { 22 | var viewModel = widget.viewModel; 23 | var stub = viewModel.stub; 24 | 25 | return Scaffold( 26 | appBar: AppBar( 27 | title: Text(stub.displayName), 28 | actions: stub.isNew 29 | ? [] 30 | : [ 31 | IconButton( 32 | icon: Icon(Icons.edit), 33 | onPressed: () { 34 | viewModel.onEditPressed(context); 35 | }, 36 | ), 37 | ActionMenuButton( 38 | isLoading: viewModel.isLoading, 39 | entity: stub, 40 | onSelected: viewModel.onActionSelected, 41 | ) 42 | ], 43 | ), 44 | body: FormCard( 45 | children: [ 46 | // STARTER: widgets - do not remove comment 47 | ] 48 | ), 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /stubs/ui/stub/view/stub_view_vm: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:redux/redux.dart'; 5 | import 'package:flutter_redux/flutter_redux.dart'; 6 | import 'package:flutter_redux_starter/redux/stub/stub_actions.dart'; 7 | import 'package:flutter_redux_starter/data/models/stub_model.dart'; 8 | import 'package:flutter_redux_starter/data/models/models.dart'; 9 | import 'package:flutter_redux_starter/ui/stub/view/stub_view.dart'; 10 | import 'package:flutter_redux_starter/redux/app/app_state.dart'; 11 | import 'package:flutter_redux_starter/ui/app/icon_message.dart'; 12 | 13 | class StubViewScreen extends StatelessWidget { 14 | static final String route = '/stub/view'; 15 | StubViewScreen({Key key}) : super(key: key); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return StoreConnector( 20 | converter: (Store store) { 21 | return StubViewVM.fromStore(store); 22 | }, 23 | builder: (context, vm) { 24 | return StubView( 25 | viewModel: vm, 26 | ); 27 | }, 28 | ); 29 | } 30 | } 31 | 32 | class StubViewVM { 33 | final StubEntity stub; 34 | final Function(BuildContext, EntityAction) onActionSelected; 35 | final Function(BuildContext) onEditPressed; 36 | final bool isLoading; 37 | 38 | StubViewVM({ 39 | @required this.stub, 40 | @required this.onActionSelected, 41 | @required this.onEditPressed, 42 | @required this.isLoading, 43 | }); 44 | 45 | factory StubViewVM.fromStore(Store store) { 46 | final stub = store.state.stubUIState.selected; 47 | 48 | return StubViewVM( 49 | isLoading: store.state.isLoading, 50 | stub: stub, 51 | onEditPressed: (BuildContext context) { 52 | store.dispatch(EditStub(stub: stub, context: context)); 53 | }, 54 | onActionSelected: (BuildContext context, EntityAction action) { 55 | final Completer completer = new Completer(); 56 | var message; 57 | switch (action) { 58 | case EntityAction.delete: 59 | store.dispatch(DeleteStubRequest(completer, stub.id)); 60 | message = 'Successfully Deleted Stub'; 61 | break; 62 | } 63 | if (message != null) { 64 | return completer.future.then((_) { 65 | Scaffold.of(context).showSnackBar(SnackBar( 66 | content: IconMessage( 67 | message: message, 68 | ), 69 | duration: Duration(seconds: 3))); 70 | }); 71 | } 72 | } 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test/login_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_redux_starter/keys.dart'; 2 | import 'package:flutter_driver/flutter_driver.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('LOGIN TEST', () { 7 | 8 | FlutterDriver driver; 9 | String loginEmail, loginPassword; 10 | 11 | setUp(() async { 12 | driver = await FlutterDriver.connect(); 13 | 14 | // read config file 15 | //loginEmail = Config.testLoginEmail; 16 | //loginPassword = Config.testLoginPassword; 17 | }); 18 | 19 | tearDown(() async { 20 | if(driver!=null) { 21 | driver.close(); 22 | } 23 | }); 24 | 25 | test('No input provided by user test', () async { 26 | await driver.tap(find.text('LOGIN')); 27 | await driver.waitFor(find.text('Please enter your email')); 28 | await driver.waitFor(find.text('Please enter your password')); 29 | }); 30 | 31 | test('Details filled by user and login', () async { 32 | 33 | await driver.tap(find.byValueKey(LoginKeys.emailKey)); 34 | await driver.enterText(loginEmail); 35 | 36 | await driver.tap(find.byValueKey(LoginKeys.passwordKey)); 37 | await driver.enterText(loginPassword); 38 | 39 | await driver.tap(find.text('LOGIN')); 40 | 41 | //await driver.waitFor(find.byType('')); 42 | }); 43 | }); 44 | } -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // To perform an interaction with a widget in your test, use the WidgetTester utility that Flutter 3 | // provides. For example, you can send tap and scroll gestures. You can also use WidgetTester to 4 | // find child widgets in the widget tree, read text, and verify that the values of widget properties 5 | // are correct. 6 | 7 | import 'package:flutter_test/flutter_test.dart'; 8 | 9 | 10 | void main() { 11 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 12 | 13 | }); 14 | } 15 | --------------------------------------------------------------------------------