Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Work In Progress] Initial Commit using Wire and MVC working example #187

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions wire/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/

# IntelliJ related
*.iml
*.ipr
*.iws
.idea/

# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/

# Flutter/Dart/Pub related
**/doc/api/
.dart_tool/
.flutter-plugins
.packages
.pub-cache/
.pub/
/build/

# Android related
**/android/**/gradle-wrapper.jar
**/android/.gradle
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java

# iOS/XCode related
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/Flutter/flutter_export_environment.sh
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*

# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
10 changes: 10 additions & 0 deletions wire/.metadata
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.

version:
revision: 27321ebbad34b0a3fafe99fac037102196d655ff
channel: stable

project_type: app
197 changes: 197 additions & 0 deletions wire/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# wire_flutter_todo example

The wire_flutter_todo example uses only the core Widgets and Classes that Flutter provides out of the box to manage state. The most important of these: the `StatefulWidget`.

## Key Concepts

* Share State by Lifting State Up - If two Widgets need access to the same state (aka data), "lift" the state up to a parent `StatefulWidget` that passes the state down to each child Widget that needs it.
* Updating State with callbacks - To update state, pass a callback function from the Parent `StatefulWidget` to the child widgets.
* Local persistence - The list of todos is serialized to JSON and stored as a file on disk whenever the State is updated.
* Testing - Pull Business logic out of Widgets into Plain Old Dart Object (PODOs).

## Share State by Lifting State Up

Let's start with a Simple Example. Say our app had only 1 Tab: The `List of Todos Tab`.

```
+------------+
| |
| List |
| of |
| Todos |
| Tab |
| |
+------------+
```

Now, we add a sibling Widget: The `Stats Tab`! But wait, it needs access to the List of Todos so it can calculate how many of them are active and how many are complete. So how do we share that data?

It can be difficult for siblings to pass their state to each other. For example, say both Widgets were displayed side-by-side at the same time: How would Flutter know when to re-build the `Stats Tab` to reflect the latest count when the List of Todos changes?

```
+-------------+ +-------------+
| | | |
| | Gimme dem Todos | |
| List of | <-------------- + Stats |
| Todos | | Tab |
| Tab | No. Mine. | |
| + --------------> | |
| | | |
+-------------+ +-------------+
```

So how do we share state between these two sibling Widgets? Let's Lift the state up to a Parent Widget and pass it down to each child that needs it!

```
+-------------------------+
| Keeper of the Todos |
| (StatefulWidget) |
+-----+--------------+----+
| |
+----------|--+ +---|---------+
| v | | v |
| List of | | Stats |
| Todos | | Tab |
| | | |
+-------------+ +-------------+
```

Now, when we change the List of Todos in the `Keeper of the Todos` widget, both children will reflect the updated State! This concept scales to an entire app. Any time you need to share State between Widgets or Routes, lift it up to a common parent Widget. Here's a diagram of what our app state actually looks like!

```
+------------------------------------------+
| |
| VanillaApp (StatefulWidget) |
| |
| Manages List<Todo> and "isLoading" |
| |
+---------+---------------------------+----+
| |
+------------|---------------+ +--------|--------+
| v | | v |
| Main Tabs Screen | | Add Todo Screen |
| (Stateful) | | (Stateless) |
| | | |
| Manages current tab and | | |
| Visibility filter | | |
| | | |
| +----------+ +----------+ | | |
| | | | | | | |
| | List | | | | | |
| | of | | Stats | | | |
| | Todos | | Tab | | | |
| | Tab | | | | | |
| +----+-----+ +----------+ | | |
| | | | |
+------|---------------------+ +-----------------+
|
+------|---------+
| v |
| |
| Todo Details |
| Screen |
| |
+------+---------+
|
+------|---------+
| v |
| |
| Edit Todo |
| Screen |
| |
+----------------+
```

Careful Observers might note: We don't lift *all* state up to the parent in this pattern. Only the State that's shared! The Main Tabs Screen can handle which tab is currently active on its own, for example, because this state isn't relevant to other Widgets!

## Updating State with callbacks

Ok, so now we have an idea of how to pass data down from parent to child, but how do we update the list of Todos from these different screens / Routes / Widgets?

We'll also **pass callback functions** from the parent to the children as well. These callback functions are responsible for updating the State at the Parent Widget and calling `setState` so Flutter knows it needs to rebuild!

In this app, we have a few callbacks we will pass down:

1. AddTodo callback
2. RemoveTodo Callback
3. UpdateTodo Callback
4. MarkAllComplete callback
5. ClearComplete callback

In this app, we pass the `AddTodo` callback from our `VanillaApp` Widget to the `Add Todo Screen`. Now all our `Add Todo Screen` needs to do is call the `AddTodo` callback with a new `Todo` when a user finishes filling in the form. This will send the `Todo` up to the `VanillaApp` so it can handle how it adds the new todo to the list!

This demonstrates a core concept of State management in Flutter: Pass data and callback functions down from a parent to a child, and use invoke those callbacks in the Child to send data back up to the parent.

To make this concrete, Let's see how our callbacks flow in the this app.

```
+----------------------------------------------------+
| |
| VanillaApp (StatefulWidget) |
| |
| Manages List<Todo> and "isLoading" |
| ^ |
+---------+----------------------+------------|------+
| | |
| UpdateTodo | AddTodo | Invoke AddTodo
| RemoveTodo | | Callback, sending
| MarkAllComplete | | Data up to the
| ClearComplete | | parent.
| | |
+------------|---------------+ +---|------------+-+
| v | | v |
| Main Tabs Screen | | Add Todo Screen |
| (Stateful) | | (Stateless) |
| | | |
| Manages current tab and | | |
| Visibility filter | | |
| | | |
| +----------+ +----------+ | | |
| | | | | | | |
| | List | | | | | |
| | of | | Stats | | | |
| | Todos | | Tab | | | |
| | Tab | | | | | |
| +----+-----+ +----------+ | | |
| | | | |
+------|---------------------+ +------------------+
|
| UpdateTodo
| RemoveTodo
|
+------|---------+
| v |
| |
| Todo Details |
| Screen |
| |
+------+---------+
|
| UpdateTodo
|
+------|---------+
| v |
| |
| Edit Todo |
| Screen |
| |
+----------------+
```

## Testing

There are a few Strategies for testing:

1. Store state and the state mutations (methods) within a `StatefulWidget`
2. Extract this State and logic out into a "Plain Old Dart Objects" (PODOs) and test those. The `StatefulWidget` can then delegate to this object.

So which should you choose? For View-related State: Option #1. For App State / Business Logic: Option #2.

While Option #1 works because Flutter provides a nice set of Widget testing utilities out of the box, Option #2 will generally prove easier to test because it has no Flutter dependencies and does not require you to test against a Widget tree, but simply against an Object.

## Addendum

Since Flutter is quite similar to React with regards to State management, many of the resources on the React site are pertinent when thinking about State in flutter. In fact, the ideas in this example were lifted directly from the React site:

* [Lifting State Up in React](https://reactjs.org/docs/lifting-state-up.html)
* [Thinking in React](https://reactjs.org/docs/thinking-in-react.html)
7 changes: 7 additions & 0 deletions wire/android/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
67 changes: 67 additions & 0 deletions wire/android/app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}

def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}

def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}

def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
compileSdkVersion 28

sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}

lintOptions {
disable 'InvalidPackage'
}

defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.wire_flutter_todo"
minSdkVersion 16
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}

flutter {
source '../..'
}

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}
7 changes: 7 additions & 0 deletions wire/android/app/src/debug/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.wire_flutter_todo">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
30 changes: 30 additions & 0 deletions wire/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.wire_flutter_todo">
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<application
android:name="io.flutter.app.FlutterApplication"
android:label="wire_flutter_todo"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
Loading