├── .github
├── ISSUE_TEMPLATE
│ └── default.md
└── workflows
│ ├── release.yml
│ ├── update-docs.yml
│ └── validate-documentation.yml
├── .gitignore
├── .markdownlint.rb
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── RELEASING.md
├── deploy_website.sh
├── docs
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── RELEASING.md
├── code-recipes.md
├── css
│ └── app.css
├── development-process.md
├── faq.md
├── glossary.md
├── historical.md
├── images
│ ├── down_the_view_tree.svg
│ ├── email_browser_workflow_schematic.svg
│ ├── email_inbox_workflow_schematic.svg
│ ├── email_message_workflow_schematic.svg
│ ├── email_schematic_renderings_only.svg
│ ├── game_workflow_schematic.svg
│ ├── icon-square.png
│ ├── split_screen_schematic.svg
│ ├── split_screen_update.svg
│ ├── swift
│ │ ├── nested_workflow_rendering.png
│ │ └── workflow_rendering.png
│ └── workflow_schematic.svg
├── index.md
├── sequence_diagrams
│ ├── README.md
│ ├── nested_workflow_rendering.seq
│ └── workflow_rendering.seq
└── userguide
│ ├── common-patterns.md
│ ├── concepts.md
│ ├── implementation.md
│ ├── testing-concepts.md
│ ├── ui-concepts.md
│ ├── ui-in-code.md
│ ├── whyworkflow.md
│ ├── worker-in-code.md
│ └── workflow-in-code.md
├── drawings.graffle
├── lint_docs.sh
├── mkdocs.yml
├── requirements.txt
└── wolfcrow.png
/.github/ISSUE_TEMPLATE/default.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Default
3 | about: Default template for new issues
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | Are you filing this issue about something that is specific to Kotlin or Swift workflow libraries? If so, please file on the appropriate repository instead:
11 |
12 | Kotlin: github.com/square/workflow-kotlin
13 | Swift: github.com/square/workflow-swift
14 |
15 | Issues addressing platform inconsistencies may be filed here.
16 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release Workflow
2 |
3 | on:
4 | repository_dispatch:
5 | types: [release]
6 |
7 | env:
8 | RELEASE_TYPE: ${{ github.event.client_payload.release_type }}
9 | WORKFLOW_VERSION: ${{ github.event.client_payload.workflow_version }}
10 | PREFIX_FOR_TEST: ${{ github.event.client_payload.test_prefix }}
11 |
12 | jobs:
13 | bump-main:
14 | runs-on: macos-latest
15 |
16 | steps:
17 | - name: Calculate Release Branch
18 | run: |
19 | MAJOR=$(cut -d'.' -f1 <<<'${{ env.WORKFLOW_VERSION }}')
20 | MINOR=$(cut -d'.' -f2 <<<'${{ env.WORKFLOW_VERSION }}')
21 | echo "::set-env name=RELEASE_BRANCH::${{ env.PREFIX_FOR_TEST }}release-v$MAJOR.$MINOR.x"
22 |
23 | - name: Checkout
24 | uses: actions/checkout@v2
25 |
26 | - name: Checkout Main
27 | uses: actions/checkout@v2
28 | with:
29 | ref: ${{ env.PREFIX_FOR_TEST }}main
30 | path: main
31 |
32 | - name: Setup Release Branch (major, minor)
33 | if: env.RELEASE_TYPE == 'major' || env.RELEASE_TYPE == 'minor'
34 | run: |
35 | cp -R main release
36 | cd release
37 | git checkout -b ${{ env.RELEASE_BRANCH }}
38 |
39 | - name: Setup Release Branch (patch)
40 | if: env.RELEASE_TYPE == 'patch'
41 | uses: actions/checkout@v2
42 | with:
43 | ref: ${{ env.RELEASE_BRANCH }}
44 | path: release
45 |
46 | - name: Push changes to main
47 | env:
48 | GIT_USERNAME: ${{ github.actor }}
49 | GIT_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
50 | run: |
51 | cd main
52 | git add -A . && git commit -m "Releasing ${{ env.WORKFLOW_VERSION }}" && git push -f
53 |
54 | - name: Push Release Branch
55 | run: |
56 | cd release
57 | ls Workflow*.podspec | xargs sed -i '' -e "s/ s.version\( *=\).*/ s.version\1 '${{ env.WORKFLOW_VERSION }}'/"
58 | git add -A .; git commit -m "Releasing ${{ env.WORKFLOW_VERSION }}"
59 | git tag ${{ env.PREFIX_FOR_TEST }}v${{ env.WORKFLOW_VERSION }}
60 | git push origin ${{ env.RELEASE_BRANCH }} ${{ env.PREFIX_FOR_TEST }}v${{ env.WORKFLOW_VERSION }}
61 |
62 | # Publish Documentation
63 | # Swift caches (keys must match those defined in swift.yml)
64 | - name: Load gem cache
65 | uses: actions/cache@v1
66 | with:
67 | path: release/.bundle
68 | key: gems-${{ hashFiles('Gemfile.lock') }}
69 |
70 | - name: Set up Swift environment
71 | run: |
72 | # Set global bundle path so it gets used by build_swift_docs.sh running in the nested repo as well.
73 | cd release
74 | bundle config --global path "$(pwd)/.bundle"
75 | bundle check || bundle install
76 | # Don't need to run pod gen, the website script does that itself.
77 | brew install sourcedocs
78 | sudo xcode-select -s /Applications/Xcode_11.4.app
79 |
80 | # Docs dependencies
81 | - name: Set up Python
82 | uses: actions/setup-python@v1
83 | with:
84 | python-version: '3.12.3'
85 |
86 | - name: Install Python dependencies
87 | run: |
88 | python -m pip install --upgrade pip
89 | pip install -r requirements.txt
90 |
91 | # This environment variable step should be run after all 3rd-party actions to ensure nothing
92 | # else accidentally overrides any of our special variables.
93 | - name: 'If in test-mode: enable dry run'
94 | if: env.PREFIX_FOR_TEST != ''
95 | run: |
96 | # When PREFIX_FOR_TEST is not empty, we shouldn't actually deploy, just do a dry run to make
97 | # sure all the dependencies are set up correctly.
98 | echo "::set-env name=DRY_RUN::true"
99 |
100 | - name: Debug info
101 | run: |
102 | cd release
103 | echo event_name=${{ github.event_name }}
104 | echo GITHUB_REF=$GITHUB_REF
105 | echo GITHUB_HEAD_REF=$GITHUB_HEAD_REF
106 | echo DRY_RUN=$DRY_RUN
107 | git remote -v
108 |
109 | ## Main steps
110 | - name: Build and deploy website
111 | env:
112 | WORKFLOW_GOOGLE_ANALYTICS_KEY: ${{ secrets.WORKFLOW_GOOGLE_ANALYTICS_KEY }}
113 | GIT_USERNAME: ${{ github.actor }}
114 | GIT_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
115 | run: |
116 | cd release
117 | ./deploy_website.sh ${{ env.PREFIX_FOR_TEST }}v${{ env.WORKFLOW_VERSION }}
118 |
119 |
120 | - name: Create Github Release
121 | run: |
122 | echo "TODO: Create Github Release"
123 |
124 |
125 |
--------------------------------------------------------------------------------
/.github/workflows/update-docs.yml:
--------------------------------------------------------------------------------
1 | name: Publish Documentation Site
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | main-ref:
7 | description: 'Main Workflow repo ref to publish'
8 | default: 'main'
9 | required: true
10 | kotlin-ref:
11 | description: 'Kotlin Git ref to publish'
12 | default: 'main'
13 | required: true
14 | docs-branch:
15 | description: 'Branch name for updated documentation to be published'
16 | required: true
17 |
18 | jobs:
19 | build-docs:
20 | runs-on: macos-latest
21 |
22 | steps:
23 | - name: Check out main repo
24 | uses: actions/checkout@v3
25 | with:
26 | ref: ${{ github.event.inputs.main-ref }}
27 | path: 'workflow'
28 |
29 | - name: Check out Kotlin repo
30 | uses: actions/checkout@v3
31 | with:
32 | repository: 'square/workflow-kotlin'
33 | ref: ${{ github.event.inputs.kotlin-ref }}
34 | path: 'workflow-kotlin'
35 |
36 | # Docs dependencies
37 | - name: Set up Python
38 | uses: actions/setup-python@v4
39 | with:
40 | python-version: '3.12.3'
41 |
42 | - name: Install Python dependencies
43 | run: |
44 | cd workflow
45 | python -m pip install --upgrade pip
46 | pip install -r requirements.txt
47 |
48 | - name: Set up JDK 17
49 | uses: actions/setup-java@v2
50 | with:
51 | distribution: 'zulu'
52 | java-version: 17
53 |
54 | # Build Kotlin docs
55 | - name: Build Kotlin docs
56 | run: |
57 | cd workflow-kotlin
58 | ./gradlew assemble --build-cache --quiet
59 | ./gradlew siteDokka --build-cache --quiet
60 |
61 | mkdir -p ../workflow/docs/kotlin/api
62 | mv build/dokka/htmlMultiModule ../workflow/docs/kotlin/api
63 |
64 | # Generate the mkdocs site
65 | - name: Generate site with mkdocs
66 | env:
67 | WORKFLOW_GOOGLE_ANALYTICS_KEY: ${{ secrets.WORKFLOW_GOOGLE_ANALYTICS_KEY }}
68 | run: |
69 | cd workflow
70 |
71 | echo "Building documentation site"
72 | mkdocs build
73 |
74 | # Push docs to new branch
75 | - name: Create new docs branch
76 | uses: actions/checkout@v3
77 | with:
78 | ref: gh-pages
79 | path: 'workflow-publish'
80 |
81 | - name: Commit updated docs
82 | run: |
83 | # Get the source repo SHAs
84 | KOTLIN_REF=$(git --git-dir workflow-kotlin/.git log -1 --format='%H')
85 |
86 | cd workflow-publish
87 | git checkout -b ${{ github.event.inputs.docs-branch }}
88 |
89 | # Copy all the files over from the 'site' directory
90 | cp -R ../workflow/site/* .
91 |
92 | # Commit and push
93 | git add .
94 | git commit -m "Update documentation" -m "Docs built from square/workflow-kotlin@$KOTLIN_REF"
95 | git push origin HEAD:${{ github.event.inputs.docs-branch }}
96 |
--------------------------------------------------------------------------------
/.github/workflows/validate-documentation.yml:
--------------------------------------------------------------------------------
1 | name: Validate documentation site
2 |
3 | on:
4 | pull_request:
5 | paths:
6 | # Rebuild when workflow configs change.
7 | - .github/workflows/validate-documentation.yml
8 | # Or when documentation code changes.
9 | - 'docs/**'
10 | - '**.md'
11 | - mkdocs.yml
12 | - lint_docs.sh
13 | - .markdownlint.rb
14 |
15 | jobs:
16 | mkdocs:
17 | name: Build mkdocs to validate mkdocs.yml
18 | runs-on: ubuntu-latest
19 | steps:
20 | - uses: actions/checkout@v3
21 | - name: Set up Python
22 | uses: actions/setup-python@v4
23 | with:
24 | python-version: '3.12.3'
25 | - name: Upgrade pip
26 | run: python -m pip install --upgrade pip
27 | - name: Install dependencies
28 | run: pip install -r requirements.txt
29 | - name: Run mkdocs
30 | run: mkdocs build
31 |
32 | lint:
33 | name: Lint Markdown files
34 | runs-on: ubuntu-latest
35 | steps:
36 | - uses: actions/checkout@v3
37 | - name: Set up Ruby 2.7
38 | uses: ruby/setup-ruby@v1
39 | with:
40 | ruby-version: 2.7
41 | - name: Install dependencies
42 | run: gem install mdl -v 0.12.0
43 | - name: Lint docs
44 | run: ./lint_docs.sh
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # macOS
2 | .DS_Store
3 |
4 | # Compiled class file
5 | *.class
6 |
7 | # Log file
8 | *.log
9 |
10 | # BlueJ files
11 | *.ctxt
12 |
13 | # Mobile Tools for Java (J2ME)
14 | .mtj.tmp/
15 |
16 | # Package Files #
17 | *.jar
18 | *.war
19 | *.nar
20 | *.ear
21 | *.zip
22 | *.tar.gz
23 | *.rar
24 |
25 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
26 | hs_err_pid*
27 |
28 | # Gradle
29 | out/
30 | .gradle/
31 | build/
32 | local.properties
33 |
34 | # Intellij
35 | *.iml
36 | .idea/
37 |
38 | # cocoapods-generate
39 | gen/
40 |
41 | # Swift Package Manager
42 | .build/
43 | Package.resolved
44 | .swiftpm/
45 |
46 | # CocoaPods
47 | Pods/
48 | gen/
49 |
50 | # Xcode
51 | xcuserdata/
52 |
53 | # Sample workspace
54 | SampleApp.xcworkspace
55 |
56 | # Special Mkdocs files
57 | deploy-kotlin/
58 | deploy-swift/
59 | docs/kotlin/api/
60 | docs/swift/api/
61 | site/
62 |
63 | # ios-snapshot-test-case Failure Diffs
64 | FailureDiffs/
65 |
--------------------------------------------------------------------------------
/.markdownlint.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2019 Square Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | #
16 |
17 | # Configuring rules:
18 | # https://github.com/markdownlint/markdownlint/blob/master/docs/creating_styles.md
19 | # Rule list:
20 | # https://github.com/markdownlint/markdownlint/blob/master/docs/RULES.md
21 |
22 | # Need to explicitly run all rules, so the per-rule configs below aren't used as an allowlist.
23 | all
24 |
25 | # We don't care about line length, because it prevents us from pasting in text from sane tools.
26 | exclude_rule 'MD013'
27 |
28 | # Enable inline HTML.
29 | exclude_rule 'MD033'
30 |
31 | # Allow paragraphs that consiste entirely of emphasized text.
32 | exclude_rule 'MD036'
33 |
34 | # Allow trailing question marks in headers.
35 | rule 'MD026', :punctuation => '.,;:!'
36 |
37 | # Markdownlint can't handle mkdocs' code block tab syntax, so disable code block formatting.
38 | exclude_rule 'MD040'
39 | exclude_rule 'MD046'
40 |
41 | # Don't care about blank lines surround fenced code blocks.
42 | exclude_rule 'MD031'
43 |
44 | # Allow raw URLs.
45 | exclude_rule 'MD034'
46 |
47 | # Py Markdown requires four spaces to indent a sublist
48 | rule 'MD007', :indent => 4
49 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | Open Source Code of Conduct
2 | ===========================
3 |
4 | At Square, we are committed to contributing to the open source community and simplifying the process
5 | of releasing and managing open source software. We’ve seen incredible support and enthusiasm from
6 | thousands of people who have already contributed to our projects — and we want to ensure ourcommunity
7 | continues to be truly open for everyone.
8 |
9 | This code of conduct outlines our expectations for participants, as well as steps to reporting
10 | unacceptable behavior. We are committed to providing a welcoming and inspiring community for all and
11 | expect our code of conduct to be honored.
12 |
13 | Square’s open source community strives to:
14 |
15 | * **Be open**: We invite anyone to participate in any aspect of our projects. Our community is
16 | open, and any responsibility can be carried by a contributor who demonstrates the required
17 | capacity and competence.
18 |
19 | * **Be considerate**: People use our work, and we depend on the work of others. Consider users and
20 | colleagues before taking action. For example, changes to code, infrastructure, policy, and
21 | documentation may negatively impact others.
22 |
23 | * **Be respectful**: We expect people to work together to resolve conflict, assume good intentions,
24 | and act with empathy. Do not turn disagreements into personal attacks.
25 |
26 | * **Be collaborative**: Collaboration reduces redundancy and improves the quality of our work. We
27 | strive for transparency within our open source community, and we work closely with upstream
28 | developers and others in the free software community to coordinate our efforts.
29 |
30 | * **Be pragmatic**: Questions are encouraged and should be asked early in the process to avoid
31 | problems later. Be thoughtful and considerate when seeking out the appropriate forum for your
32 | questions. Those who are asked should be responsive and helpful.
33 |
34 | * **Step down considerately**: Members of every project come and go. When somebody leaves or
35 | disengages from the project, they should make it known and take the proper steps to ensure that
36 | others can pick up where they left off.
37 |
38 | This code is not exhaustive or complete. It serves to distill our common understanding of a
39 | collaborative, shared environment, and goals. We expect it to be followed in spirit as much as in
40 | the letter.
41 |
42 | Diversity Statement
43 | -------------------
44 |
45 | We encourage everyone to participate and are committed to building a community for all. Although we
46 | may not be able to satisfy everyone, we all agree that everyone is equal.
47 |
48 | Whenever a participant has made a mistake, we expect them to take responsibility for it. If someone
49 | has been harmed or offended, it is our responsibility to listen carefully and respectfully, and do
50 | our best to right the wrong.
51 |
52 | Although this list cannot be exhaustive, we explicitly honor diversity in age, culture, ethnicity,
53 | gender identity or expression, language, national origin, political beliefs, profession, race,
54 | religion, sexual orientation, socioeconomic status, and technical ability. We will not tolerate
55 | discrimination based on any of the protected characteristics above, including participants with
56 | disabilities.
57 |
58 | Reporting Issues
59 | ----------------
60 |
61 | If you experience or witness unacceptable behavior — or have any other concerns — please report it by
62 | emailing [codeofconduct@squareup.com][codeofconduct_at]. For more details, please see our Reporting
63 | Guidelines below.
64 |
65 | Thanks
66 | ------
67 |
68 | Some of the ideas and wording for the statements and guidelines above were based on work by the
69 | [Twitter][twitter_coc], [Ubuntu][ubuntu_coc], [GDC][gdc_coc], and [Django][django_coc] communities.
70 | We are thankful for their work.
71 |
72 | Reporting Guide
73 | ---------------
74 |
75 | If you experience or witness unacceptable behavior — or have any other concerns — please report it by
76 | emailing [codeofconduct@squareup.com][codeofconduct_at]. All reports will be handled with
77 | discretion.
78 |
79 | In your report please include:
80 |
81 | * Your contact information.
82 | * Names (real, nicknames, or pseudonyms) of any individuals involved. If there are additional
83 | witnesses, please include them as well.
84 | * Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly
85 | available record (e.g. a mailing list archive or a public IRC logger), please include a link.
86 | * Any additional information that may be helpful.
87 |
88 | After filing a report, a representative from the Square Code of Conduct committee will contact you
89 | personally. The committee will then review the incident, follow up with any additional questions,
90 | and make a decision as to how to respond.
91 |
92 | Anyone asked to stop unacceptable behavior is expected to comply immediately. If an individual
93 | engages in unacceptable behavior, the Square Code of Conduct committee may take any action they deem
94 | appropriate, up to and including a permanent ban from all of Square spaces without warning.
95 |
96 | [codeofconduct_at]: mailto:codeofconduct@squareup.com
97 | [twitter_coc]: https://github.com/twitter/code-of-conduct/blob/master/code-of-conduct.md
98 | [ubuntu_coc]: https://ubuntu.com/community/code-of-conduct
99 | [gdc_coc]: https://www.gdconf.com/code-of-conduct
100 | [django_coc]: https://www.djangoproject.com/conduct/reporting/
101 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Contributing
2 | ============
3 |
4 | If you would like to contribute code to Workflow you can do so through GitHub by
5 | forking the repository and sending a pull request.
6 |
7 | When submitting code, please make every effort to follow existing conventions
8 | and style in order to keep the code as readable as possible. Please also make
9 | sure your code compiles.
10 |
11 | Before your code can be accepted into the project you must also sign the
12 | [Individual Contributor License Agreement (CLA)][1].
13 |
14 | [1]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Overview
2 |
3 | [](https://www.apache.org/licenses/LICENSE-2.0)
4 |
5 | Workflow is an application framework that provides architectural primitives.
6 |
7 | Workflow is:
8 |
9 | * Written in and used for Kotlin and Swift
10 | * A unidirectional data flow library that uses immutable data within each Workflow.
11 | Data flows in a single direction from source to UI, and events in a single direction
12 | from the UI to the business logic.
13 | * A library that supports writing business logic and complex UI navigation logic as
14 | state machines, thereby enabling confident reasoning about state and validation of
15 | correctness.
16 | * Optimized for composability and scalability of features and screens.
17 | * Corresponding UI frameworks that bind Rendering data classes for “views”
18 | (including event callbacks) to Mobile UI frameworks for Android and iOS.
19 | * A corresponding testing framework that facilitates simple-to-write unit
20 | tests for all application business logic and helps ensure correctness.
21 |
22 | ## Using Workflows in your project
23 |
24 | ### Swift
25 |
26 | See the [square/workflow-swift](https://github.com/square/workflow-swift) repository.
27 |
28 | ### Kotlin
29 |
30 | See the [square/workflow-kotlin](https://github.com/square/workflow-kotlin) repository.
31 |
32 | ## Resources
33 |
34 | * Wondering why to use Workflow? See
35 | ["Why Workflow"](https://square.github.io/workflow/userguide/whyworkflow/)
36 | * There is a [Glossary of Terms](https://square.github.io/workflow/glossary/)
37 | * We have a [User Guide](https://square.github.io/workflow/userguide/concepts/)
38 | describing core concepts.
39 | * For Kotlin (and Android), there is a codelab style
40 | [tutorial](https://github.com/square/workflow-kotlin/tree/main/samples/tutorial) in the repo.
41 | * For Swift (and iOS), there is also a Getting Started
42 | [tutorial](https://github.com/square/workflow-swift/tree/main/Samples/Tutorial) in the repo.
43 | * There are also a number of
44 | [Kotlin samples](https://github.com/square/workflow-kotlin/tree/main/samples)
45 | and [Swift samples](https://github.com/square/workflow-swift/tree/main/Samples).
46 |
47 | ### Support & Contact
48 |
49 | Workflow discussion happens in the Workflow Community slack. Use this [open invitation](https://join.slack.com/t/workflow-community/shared_invite/zt-a2wc0ddx-4bvc1royeZ7yjGqEkW1CsQ).
50 |
51 | Workflow maintainers also hang out in the [#squarelibraries](https://kotlinlang.slack.com/messages/C5HT9AL7Q)
52 | channel on the [Kotlin Slack](https://surveys.jetbrains.com/s3/kotlin-slack-sign-up?_ga=2.93235285.916482233.1570572671-654176432.1527183673).
53 |
54 | ## Releasing and Deploying
55 |
56 | See [RELEASING.md](RELEASING.md).
57 |
58 | ## License
59 |
60 |
61 | Copyright 2019 Square Inc.
62 |
63 | Licensed under the Apache License, Version 2.0 (the "License");
64 | you may not use this file except in compliance with the License.
65 | You may obtain a copy of the License at
66 |
67 | http://www.apache.org/licenses/LICENSE-2.0
68 |
69 | Unless required by applicable law or agreed to in writing, software
70 | distributed under the License is distributed on an "AS IS" BASIS,
71 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
72 | See the License for the specific language governing permissions and
73 | limitations under the License.
74 |
75 |
76 | 
77 |
--------------------------------------------------------------------------------
/RELEASING.md:
--------------------------------------------------------------------------------
1 | # Releasing workflow
2 |
3 | ## Deploying the documentation website
4 |
5 | Official Workflow documentation lives at . The website content
6 | consists of three parts:
7 |
8 | 1. Markdown documentation: Lives in the `docs/` folder, and consists of a set of hand-written
9 | Markdown files that document high-level concepts. The static site generator
10 | [mkdocs](https://www.mkdocs.org/) (with [Material](https://squidfunk.github.io/mkdocs-material/)
11 | theming) is used to convert the Markdown to static, styled HTML.
12 | 1. Kotlin API reference: Kdoc embedded in Kotlin source files is converted to GitHub-flavored
13 | Markdown by Dokka and then included in the statically-generated website.
14 | 1. Swift API reference: Markup comments from Swift files are converted Markdown by
15 | [DocC](https://www.swift.org/documentation/docc/) and then published independently at [square.github.io/workflow-swift/documentation](https://square.github.io/workflow-swift/documentation).
16 |
17 | **Note: The documentation site is automatically built and deployed whenever a version tag is pushed.
18 | You only need these steps if you want to work on the site locally.**
19 |
20 | ### Setting up the site generators
21 |
22 | If you've already done this, you can skip to _Deploying the website to production_ below.
23 |
24 | #### Kotlin: Dokka
25 |
26 | Dokka runs as a Gradle plugin, so you need to be able to build the Kotlin source with Gradle, but
27 | that's it. To generate the docs manually, run:
28 |
29 | ```bash
30 | cd kotlin
31 | ./gradlew dokka
32 | ```
33 |
34 | #### Swift: DocC
35 |
36 | The Swift documentation is published by CI in the Swift repo and linked from the cross-platform Workflow docs. For info on how to generate the Swift docs locally, check out [the workflow-swift repo](https://github.com/square/workflow-swift).
37 |
38 | #### mkdocs
39 |
40 | Mkdocs is written in Python, so you'll need Python 3 and pip in order to run it. Assuming those are
41 | set up, run:
42 |
43 | ```bash
44 | pip install -r requirements.txt
45 | ```
46 |
47 | Generate the site manually with:
48 |
49 | ```bash
50 | mkdocs build
51 | ```
52 |
53 | While you're working on the documentation files, you can run the site locally with:
54 |
55 | ```bash
56 | mkdocs serve
57 | ```
58 |
59 | ### Deploying the website to production
60 |
61 | **Note: The documentation site is automatically built and deployed by a Github Workflow whenever a
62 | version tag is pushed. You only need these steps if you want to publish the site manually.**
63 |
64 | Before deploying the website for real, you need to export our Google Analytics key in an environment
65 | variable so that it will get added to the HTML. Get the key from one of the project maintainers,
66 | then add the following to your `.bashrc` and re-source it:
67 |
68 | ```bash
69 | export WORKFLOW_GOOGLE_ANALYTICS_KEY=UA-__________-1
70 | ```
71 |
72 | Now you're ready to publish the site! Just choose a tag or SHA to deploy from, and run:
73 |
74 | ```bash
75 | ./deploy_website.sh TAG_OR_SHA
76 | # For example:
77 | #./deploy_website.sh v0.18.0
78 | ```
79 |
80 | This will clone the repo to a temporary directory, checkout the right SHA, build Kotlin and Swift
81 | API docs, generate HTML, and push the newly-generated content to the `gh-pages` branch on GitHub.
82 |
83 | ### Validating Markdown
84 |
85 | Since all of our high-level documentation is written in Markdown, we run a linter in CI to ensure
86 | we use consistent formatting. Lint errors will fail your PR builds, so to run locally, install
87 | [markdownlint](https://github.com/markdownlint/markdownlint):
88 |
89 | ```bash
90 | gem install mdl
91 | ```
92 |
93 | Run the linter using the `lint_docs.sh`:
94 |
95 | ```bash
96 | ./lint_docs.sh
97 | ```
98 |
99 | Rules can be configured by editing `.markdownlint.rb`.
100 |
101 | ---
102 |
103 | ## Kotlin Notes
104 |
105 | ### Development
106 |
107 | To build and install the current version to your local Maven repository (`~/.m2`), run:
108 |
109 | ```bash
110 | ./gradlew clean installArchives
111 | ```
112 |
113 | ### Deploying
114 |
115 | #### Configuration
116 |
117 | In order to deploy artifacts to a Maven repository, you'll need to set 4 properties in your private
118 | Gradle properties file (`~/.gradle/gradle.properties`):
119 |
120 | ```
121 | RELEASE_REPOSITORY_URL=
122 | SNAPSHOT_REPOSITORY_URL=
124 | SONATYPE_NEXUS_PASSWORD=
125 | ```
126 |
127 | #### Snapshot Releases
128 |
129 | Double-check that `gradle.properties` correctly contains the `-SNAPSHOT` suffix, then upload
130 | snapshot artifacts to Sonatype just like you would for a production release:
131 |
132 | ```bash
133 | ./gradlew clean build && ./gradlew uploadArchives --no-parallel --no-daemon
134 | ```
135 |
136 | You can verify the artifacts are available by visiting
137 | https://oss.sonatype.org/content/repositories/snapshots/com/squareup/workflow/.
138 |
--------------------------------------------------------------------------------
/deploy_website.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 | #
3 | # Copyright 2019 Square Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | # The website is built using MkDocs with the Material theme.
19 | # https://squidfunk.github.io/mkdocs-material/
20 | # It requires Python 3 to run.
21 | # Install the packages with the following command:
22 | # pip install -r requirements.txt
23 | # Preview the site as you're editing it with:
24 | # mkdocs serve
25 | #
26 | # Usage deploy_website.sh --kotlin-ref SHA_OR_REF_TO_DEPLOY
27 | # Set the DRY_RUN environment variable to any non-null value to skip the actual deploy.
28 | # A custom username/password can be used to authenticate to the git repo by setting
29 | # the GIT_USERNAME and GIT_PASSWORD environment variables.
30 | #
31 | # E.g. to test the script: DRY_RUN=true ./deploy_website.sh --kotlin-ref main
32 |
33 | # Automatically exit the script on error.
34 | set -e
35 |
36 | KOTLIN_REPO=square/workflow-kotlin
37 |
38 | if [ -z "$WORKFLOW_GOOGLE_ANALYTICS_KEY" ]; then
39 | echo "Must set WORKFLOW_GOOGLE_ANALYTICS_KEY to deploy." >&2
40 | exit 1
41 | fi
42 |
43 | function getAuthenticatedRepoUrl() {
44 | if (( $# == 0 )); then echo "Must pass repo name, eg 'square/workflow'" >&2; exit 1; fi
45 | local repoName="$1"
46 |
47 | # Accept username/password overrides from environment variables for Github Actions.
48 | if [ -n "$GIT_USERNAME" -a -n "$GIT_PASSWORD" ]; then
49 | echo "Authenticating as $GIT_USERNAME." >&2
50 | gitCredentials="$GIT_USERNAME:$GIT_PASSWORD"
51 | echo "https://${gitCredentials}@github.com/$repoName.git"
52 | else
53 | echo "Authenticating as current user." >&2
54 | echo "git@github.com:$repoName.git"
55 | fi
56 | }
57 |
58 | function buildKotlinDocs() {
59 | local deployRef="$1"
60 | local targetDir="$2"
61 | local workingDir=deploy-kotlin
62 |
63 | if [[ -z "$deployRef" ]]; then echo "buildKotlinDocs: Must pass deploy ref as first arg" >&2; exit 1; fi
64 | if [[ -z "$targetDir" ]]; then echo "buildKotlinDocs: Must pass target dir as second arg" >&2; exit 1; fi
65 |
66 | if [[ -d "$workingDir" ]]; then
67 | echo "Removing old working directory $workingDir..."
68 | rm -rf "$workingDir"
69 | fi
70 |
71 | echo "Shallow-cloning $KOTLIN_REPO from $deployRef into $workingDir..."
72 | git clone --depth 1 --branch $deployRef $(getAuthenticatedRepoUrl $KOTLIN_REPO) $workingDir
73 |
74 | echo "Building Kotlin docs..."
75 | pushd $workingDir
76 | ./gradlew assemble --build-cache --quiet
77 | ./gradlew siteDokka --build-cache --quiet
78 | popd
79 |
80 | echo "Moving generated documentation to $targetDir..."
81 | # Clean the target dir first.
82 | [[ -d "$targetDir" ]] && rm -rf "$targetDir"
83 | mkdir -p "$targetDir"
84 | mv "$workingDir/build/dokka/htmlMultiModule" "$targetDir"
85 |
86 | echo "Removing working directory..."
87 | rm -rf "$workingDir"
88 |
89 | echo "Kotlin docs finished."
90 | }
91 |
92 | # Process arguments. See man zshmodules.
93 | zparseopts -A refs -kotlin-ref:
94 | KOTLIN_REF=${refs[--kotlin-ref]}
95 | if [[ -z $KOTLIN_REF ]]; then
96 | echo "Missing --kotlin-ref argument" >&2
97 | exit 1
98 | fi
99 | echo "Deploying from $KOTLIN_REPO at $KOTLIN_REF"
100 |
101 | echo "Building Kotlin docs…"
102 | buildKotlinDocs $KOTLIN_REF "$(pwd)/docs/kotlin/api"
103 |
104 | # Push the new files up to GitHub.
105 | mkdocsMsg="Deployed docs using mkdocs {version} and script from {sha} from ${KOTLIN_REPO}@$KOTLIN_REF"
106 | if [ -n "$DRY_RUN" ]; then
107 | echo "DRY_RUN enabled, building mkdocs but skipping gh-deploy and push…"
108 | mkdocs build
109 | echo "Would use commit message: $mkdocsMsg"
110 | else
111 | echo "Running mkdocs gh-deploy --force…"
112 | # Build the site and force-push to the gh-pages branch.
113 | mkdocs gh-deploy --force --message "$mkdocsMsg"
114 | fi
115 |
116 | # Delete our temp folder.
117 | echo "Deploy finished."
118 |
--------------------------------------------------------------------------------
/docs/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | You can find the changelogs for the library in the respective language repositories:
4 |
5 | * [Kotlin](https://github.com/square/workflow-kotlin/releases)
6 | * [Swift](https://github.com/square/workflow-swift/releases)
7 |
8 |
--------------------------------------------------------------------------------
/docs/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | ../CODE_OF_CONDUCT.md
--------------------------------------------------------------------------------
/docs/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ../CONTRIBUTING.md
--------------------------------------------------------------------------------
/docs/RELEASING.md:
--------------------------------------------------------------------------------
1 | ../RELEASING.md
--------------------------------------------------------------------------------
/docs/code-recipes.md:
--------------------------------------------------------------------------------
1 | # Code Receipes
2 |
3 | _Coming soon!_
4 |
--------------------------------------------------------------------------------
/docs/css/app.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: cash-market;
3 | src: url("https://cash-f.squarecdn.com/static/fonts/cash-market/v2/CashMarket-Regular.woff2") format("woff2");
4 | font-weight: 400;
5 | font-style: normal
6 | }
7 |
8 | @font-face {
9 | font-family: cash-market;
10 | src: url("https://cash-f.squarecdn.com/static/fonts/cash-market/v2/CashMarket-Medium.woff2") format("woff2");
11 | font-weight: 500;
12 | font-style: normal
13 | }
14 |
15 | @font-face {
16 | font-family: cash-market;
17 | src: url("https://cash-f.squarecdn.com/static/fonts/cash-market/v2/CashMarket-Bold.woff2") format("woff2");
18 | font-weight: 700;
19 | font-style: normal
20 | }
21 |
22 | body, input {
23 | font-family: cash-market,"Helvetica Neue",helvetica,sans-serif;
24 | }
25 |
26 | .md-typeset h1, .md-typeset h2, .md-typeset h3, .md-typeset h4 {
27 | font-family: cash-market,"Helvetica Neue",helvetica,sans-serif;
28 | line-height: normal;
29 | font-weight: bold;
30 | color: #353535;
31 | }
32 |
33 | button.dl {
34 | font-weight: 300;
35 | font-size: 25px;
36 | line-height: 40px;
37 | padding: 3px 10px;
38 | display: inline-block;
39 | border-radius: 6px;
40 | color: #f0f0f0;
41 | margin: 5px 0;
42 | width: auto;
43 | }
44 |
45 | .logo {
46 | text-align: center;
47 | margin-top: 150px;
48 | }
49 |
--------------------------------------------------------------------------------
/docs/development-process.md:
--------------------------------------------------------------------------------
1 | # Development Process
2 |
3 | _Coming soon!_
4 |
--------------------------------------------------------------------------------
/docs/faq.md:
--------------------------------------------------------------------------------
1 | # Frequently Asked Questions
2 |
3 | ## Why do we need another architecture?
4 |
5 | We ask this question too! So we wrote a longer answer for it: ["Why Workflow?"](https://square.github.io/workflow/userguide/whyworkflow).
6 |
7 | ## How do I get involved and/or contribute?
8 |
9 | - [Workflow is open source!](https://github.com/square/workflow)
10 | - See our [CONTRIBUTING](https://github.com/square/workflow/blob/main/CONTRIBUTING.md) doc to get
11 | started.
12 | - Stay tuned! We're considering hosting a public Slack channel for open source contributors.
13 |
14 | ## Isn't this basically React/Elm?
15 |
16 | [React](https://reactjs.org/) and [the Elm architecture](https://guide.elm-lang.org/architecture/)
17 | were both strong influences for this library. However both those libraries are written for
18 | JavaScript. Workflows are written in and for both Kotlin and Swift, making use of features of those
19 | languages, and with usability from those languages as a major design goal. There are some
20 | architectural differences which we can see briefly in the following table:
21 |
22 | | | React | Elm | Workflow |
23 | |---|---|---|---|
24 | | **Modularity** | `Component` | `Module`s for code organization, but not 'composable' in the same way. | A `Workflow` is analogous to React's `Component` |
25 | | **State** | Each `Component` has a `state` property that is read directly and updated via a `setState` method. | State is called `Model` in Elm. | `Workflow`s have an associated state type. The state can only be updated when the props change, or with a `WorkflowAction`. |
26 | | **Views** | `Component`s have a `render` method that returns a tree of elements. | Elm applications have a `view` function that returns a tree of elements. | Since workflows are not tied to any particular UI view layer, they can have an arbitrary rendering type. The `render()` method returns this type. |
27 | | **Injected Dependencies** | React allows parent components to pass "props" down to their children. | N/A | In Swift, `Workflow`s are often structs that need to be initialized with their dependencies and configuration data from their parent. In Kotlin, they have a separate type parameter (`PropsT`) that is always passed down from the parent. `Workflow` instances can also inject dependencies, and play nicely with dependency injection frameworks.
28 | | **Composability** | `Component`s are composed of other `Component`s. | N/A | `Workflow`s can have children; they control their lifecycle and can choose to incorporate child renderings into their own. |
29 | | **Event Handling** | DOM event listeners are hooked up to functions on the `Component`. | The `update` function takes a `Msg` to modify state based on events. | `action` can be sent to the `Sink` to update `State`. |
30 |
31 | ## How is this different than MvRx?
32 |
33 | Besides being very Android and Rx specific, MvRx solves view modeling problems only
34 | per screen. Workflow was mainly inspired by the need to manage and compose
35 | navigation in apps with dozens or hundreds of screens.
36 |
37 | ## This seems clever. Can I stick with a traditional development approach?
38 |
39 | Of course! Workflow was designed to make complex application architecture predictable and safe for
40 | large development teams. We're confident that it brings benefits even to smaller projects, but there
41 | is never only one right way to build software. We recommend to [follow good practices and use an
42 | architecture that makes sense for your project](https://www.thoughtworks.com/insights/blog/write-quality-mobile-apps-any-architecture).
43 |
--------------------------------------------------------------------------------
/docs/glossary.md:
--------------------------------------------------------------------------------
1 | # Glossary of Terms
2 |
3 | ## Reactive Programming
4 |
5 | A style of programming where data or events are pushed to the logic processing them rather than having the logic pull the data and events from a source. A representation of program logic as a series of operations on a stream of data that is performed while a subscription to that stream is active.
6 |
7 | ## Unidirectional Data Flow
8 |
9 | Data travels a single path from business logic to UI, and travels the entirety of that path in a single direction. Events travel a single path from UI to business logic and they travel the entirety of that path in a single direction. There are thus two sets of directed edges in the graph that are handled separately and neither set has any cycles or back edges on its own.
10 |
11 | ## Declarative Programming
12 |
13 | A declarative program declares the state it wants the system to be in rather than how that is accomplished.
14 |
15 | ## Imperative Programming
16 |
17 | An imperative program’s code is a series of statements that directly change a program's state as a result of certain events.
18 |
19 | ## State Machine
20 |
21 | An abstraction that models a program’s logic as a graph of a set of states and the transitions between them (edges). See: en.wikipedia.org/wiki/Finite-state_machine
22 |
23 | ## Idempotent
24 |
25 | A function whose side effects won’t be repeated with multiple invocations, the result is purely a function of the input. In other words, if called multiple times with the same input, the result is the same. For Workflows, the `render()` function must be idempotent, as the runtime offers no guarantees for how many times it may be called.
26 |
27 | ## Workflow Runtime
28 |
29 | An event loop that executes a Workflow Tree. On each pass:
30 |
31 | 1. A Rendering is assembled by calling `render()` on each Node of the Workflow Tree with each parent Workflow given the option to incorporate the Renderings of its children into its own.
32 |
33 | 1. The event loop waits for an Action to be sent to the Sink.
34 |
35 | 1. This Action provides a (possibly updated) State for the Workflow that created it and possibly an Output.
36 |
37 | 1. Any Output emitted is processed in turn by an Action defined by the updated Workflow’s parent again possibly updating its State and emitting an Output cascading up the hierarchy.
38 |
39 | 1. A new `render()` pass is made against the entire Workflow Tree with the updated States.
40 |
41 | We use the term Workflow Runtime to refer to the core code in the framework that executes this event loop, responding to Actions and invoking `render()`.
42 |
43 | ## Workflow (Instance)
44 |
45 | An object that defines the transitions and side effects of a state machine as, effectively, two functions:
46 |
47 | 1. Providing the first state: (Props) -> State
48 | 1. Providing a rendering: (Props and State) -> (Rendering and Side Effect Invocations and Child Workflow Invocations)
49 |
50 | The Child Workflow Invocations declared by the render function result in calls to the children’s `render()` functions in turn, allowing the parent render function to choose to incorporate child Rendering values into its own.
51 |
52 | A Workflow is not itself a state machine, and ideally has no state of its own. It is rather a schema that identifies a particular type of state machine that can be started in `initialState()` by the Workflow Runtime, and advanced by repeated invocations of `render()`.
53 |
54 | Note: there is significant fuzziness in using the term ‘Workflow’, as it can mean at times the class/struct that declares the Workflow behavior as well as the object representing the running Workflow Node. To understand the Runtime behavior, grasping this distinction is necessary and valuable. When using a Workflow, the formal distinction is less valuable than the mental model of how a Workflow will be run.
55 |
56 | ## Workflow (Node)
57 |
58 | An active state machine whose behavior is defined by a Workflow Instance. This is the object that is held by the Workflow Runtime and whose state is updated (or “driven”) according to the behavior declared in the Workflow Instance. In Kotlin and Swift a Workflow Node is implemented with the private `WorkflowNode` class/struct.
59 |
60 | ## Workflow Lifecycle
61 |
62 | Every Workflow or Side Effect Node has a lifecycle that is determined by its parent. In the case of the root Workflow, this lifecycle is determined by how long the host of the root chooses to use the stream of Renderings from the root Workflow. In the case of a non-root Workflow or Side Effect — that is, in the case of a Child — its lifecycle is determined as follows:
63 |
64 | * Start: the first time its parent invokes the Child in the parent’s own `render()` pass.
65 |
66 | * End: the first subsequent `render()` pass that does not invoke the Child.
67 |
68 | Note that in between Start and End, the Workflow, or Side Effect is not “re-invoked” in the sense of starting again with each `render()` pass, but rather the originally invoked instance continues to run until a `render()` call is made without invoking it.
69 |
70 | ## Workflow Tree
71 |
72 | The tree of Workflow Nodes sharing a root. Workflow Nodes can have children and form a hierarchy.
73 |
74 | ## Workflow Root
75 |
76 | The root of a Workflow Tree. This is owned by a host which starts the Workflow Runtime with a particular Workflow instance.
77 |
78 | ## RenderContext
79 |
80 | The object which provides access to the Workflow Runtime from a Workflow render method. Provides three services:
81 |
82 | * a Sink for accepting WorkflowActions
83 |
84 | * recursively rendering Workflow children
85 |
86 | * executing Side Effects
87 |
88 | ## Render Pass
89 |
90 | The portion of the Workflow Runtime event loop which traverses the Workflow tree, calling `render()` on each Workflow Node. When the RenderContext Sink receives an Action an Action Cascade occurs and at the completion of the Action Cascade the Render Pass occurs.
91 |
92 | ## Output Event
93 |
94 | When a Child Workflow emits an Output value, this is an Output Event. Handlers are registered when a Child Workflow is invoked to transform the child’s Output values to Actions, which can advance the state of the parent.
95 |
96 | ## UI Event
97 |
98 | Any occurrence in the UI of a program — e.g. click, drag, keypress — the listener for which has been connected to a callback in the Rendering of a Workflow. UI Event callbacks typically add Actions to the Sink, to advance the state of the Workflow.
99 |
100 | ## Action
101 |
102 | A type associated with a particular Workflow (Instance) that is responsible for transforming a given State into a new State and optionally emitting an Output. Actions are sent to the Sink to be processed by the Workflow Runtime.
103 |
104 | ## Action Cascade
105 |
106 | When an event occurs and the handler provides an Action, this Action may possibly produce an Output for the parent Workflow which in turn has its own handler provide an Action that may produce an Output and onwards up the Workflow Tree. This is an Action Cascade.
107 |
108 | ## Sink
109 |
110 | The handle provided by the RenderContext to send Actions to the Workflow Runtime. These Actions are applied by the Workflow Runtime to advance a Workflow’s State, and optionally produce an Output to be processed by the handler its parent registered.
111 |
112 | ## Props
113 |
114 | The set of input properties for a particular Workflow. This is the public state which is provided to a child Workflow by its parent, or to the root Workflow by its host.
115 |
116 | * For Swift: The set of properties on the struct implementing the Workflow.
117 |
118 | * For Kotlin: Parameter type `PropsT` in the Workflow signature.
119 |
120 | In Kotlin there is a formal distinction between Props and other dependencies, typically provided as constructor parameters.
121 |
122 | ## State
123 |
124 | The type of the internal state of a Workflow implementation.
125 |
126 | ## "Immutable" State
127 |
128 | The State object itself is immutable, in other words, its property values cannot be changed.
129 |
130 | What this means for Workflows is that the Workflow Runtime holds a canonical instance of the internal State of each Workflow. A Workflow’s state is “advanced” when that canonical instance is atomically replaced by one returned when an Action is invoked. State can only be mutated through WorkflowAction which will trigger a re-render. There are a number of benefits to keeping State immutable in this way:
131 |
132 | * Reasoning about and debugging the Workflow is easier because, for any given State, there is a deterministic Rendering and the State cannot change except as a new parameter value to the `render()` method.
133 |
134 | * This assists in making `render()` idempotent as the State will not be modified in the course of the execution of that function.
135 |
136 | Note that this immutability can be enforced only by convention. It is possible to cheat, but that is strongly discouraged.
137 |
138 | ## Rendering
139 |
140 | The externally available public representation of the state of a Workflow. It may include event handling functions. It is given a concrete type in the Workflow signature.
141 |
142 | Note that this “Rendering” does not have to represent the UI of a program. The “Rendering” is simply the published state of the Workflow, and could simply be data. Often that data is used to render UI, but it can be used in other ways — for example, as the implementation of a service API.
143 |
144 | ## Output
145 |
146 | The type of the object that can optionally be delivered to the Workflow’s parent or the host of the root Workflow by an Action.
147 |
148 | ## Child Workflow
149 |
150 | A Workflow which has a parent. A parent may compose a child Workflow’s [Rendering](#rendering) into its own.
151 |
152 | ## Side Effect
153 |
154 | From `render()`, `runningSideEffect()` can be called with a given key and a function that will be called once by the Workflow Runtime.
155 |
156 | * For Swift, a Lifetime object is also passed to `runningSideEffect()` which has an `onEnded()` closure that can be used for cleanup.
157 |
158 | * For Kotlin, a coroutine scope is used to execute the function so it can be `cancelled()` at cleanup time. Given that any property (including the Sink) could be captured by the closure of the Side Effect this is the basic building block that can be used to interact with asynchronous (and often imperative) Workflow Children.
159 |
160 | ## Worker
161 |
162 | A Child Workflow that provides only output, with no [rendering](#rendering) — a pattern for doing asynchronous work in Workflows.
163 |
164 | * For Kotlin, this is an actual Interface which provides a convenient way to specify asynchronous work that produces an Output and a handler for that Output which can provide an Action. There are Kotlin extensions to map Rx Observables and Kotlin Flows to create Worker implementations.
165 |
166 | * For Swift, there are at least 3 different Worker types which are convenience wrappers around reactive APIs that facilitate performing work.
167 |
168 | ## View
169 |
170 | A class or function managing a 2d box in a graphical user interface system, able to paint a defined region of the display and respond to user input events within its bounds. Views are arranged in a hierarchical tree, with parents able to lay out children and manage their painting and event handling.
171 |
172 | Instances supported by Workflow are:
173 |
174 | * For Kotlin:
175 |
176 | * Classic Android: `class android.view.View`
177 | * Android JetPack Compose: `@Composable fun Box()`
178 |
179 | * For Swift: `class NSViewController`
180 |
181 | ## Screen
182 |
183 | An interface / protocol identifying [Renderings](#rendering) that model a View. Workflow UI libraries can map a given Screen type to a View instance that can display a series of such Screens.
184 |
185 | In Kotlin, `Screen` is a marker interface. Each type `S : Screen` is mapped by the Android UI library to a `ScreenViewFactory<S>` that is able to:
186 |
187 | * create instances of `android.view.View` or
188 | * provide a `@Composable fun Content(S)` function to be called from a `Box {}` context.
189 |
190 | Note that the Android UI support is able to interleave Screens bound to `View` or `@Composable` seamlessly.
191 |
192 | In Swift, the `Screen` protocol defines a single function creating `ViewControllerDescription` instances, objects which create and update `ViewController` instances to display Screens of the corresponding type.
193 |
194 | ## Overlay (Kotlin only)
195 |
196 | An interface identifying [Renderings](#rendering) that model a plane covering a base Screen, possibly hosting another Screen — “covering” in that they have a higher z-index, for visibility and event-handling.
197 |
198 | In Kotlin, `Overlay` is a marker interface. Each type `O : Overlay` is mapped by the Android UI library to an `OverlayDialogFactory<O>` able to create and update instances of `android.app.Dialog`
199 |
200 | ## Container Screen
201 |
202 | A design pattern, describing a [Screen](#screen) type whose instances wrap one or more other Screens, commonly to either annotate those Screens or define the relationships between them.
203 |
204 | Wrapping one Screen in another does not necessarily imply that the derived View hierarchy will change. It is common for the Kotlin `ScreenViewFactory` or Swift `ViewControllerDescription` bound to a Container Screen to delegate its construction and updating work to those of the wrapped Screens.
205 |
206 | ## Container View
207 |
208 | A View able to host children that are driven by [Screen](#screen) renderings. A Container View is generally driven by Container Screens of a specific type — e.g., a BackStackContainer View that can display BackStackScreen values. The exception is a root Container View, which is able to display a series of Screen instances of any type.
209 |
210 | * For Kotlin, the root Container Views are `WorkflowLayout : FrameLayout`, and `@Composable Workflow.renderAsState()`. Custom Container Views written to display custom Container Screens can use `WorkflowViewStub : FrameLayout` or `@Composable fun WorkflowRendering()` to display wrapped Screens.
211 |
212 | * For Swift, the root Container View is `ContainerViewController`. Custom Container Views written to render custom Container Screens can be built as subclasses of `ScreenViewController`, and use `DescribedViewController` to display wrapped Screens.
213 |
214 | ## ViewEnvironment
215 |
216 | A read-only key/value map passed from a [Container View](#container-view) down to its children at update time, similar in spirit to Swift UI `EnvironmentValues` and Jetpack `CompositionLocal`. Like them, the ViewEnvironment is primarily intended to allow parents to offer children hints about the context in which they are being displayed — for example, to allow a child to know if it is a member of a back stack, and so decide whether or not to display a Go Back button.
217 |
218 | The ViewEnvironment also can be used judiciously as a service provider for UI-specific concerns, like image loaders — tread carefully.
219 |
--------------------------------------------------------------------------------
/docs/historical.md:
--------------------------------------------------------------------------------
1 | # Pre-1.0 Presentations and Resources
2 |
3 | - [Square Workflow – Droidcon NYC 2019](https://www.droidcon.com/media-detail?video=362741019) ([slides](https://docs.google.com/presentation/d/19-DkVCn-XawssyHQ_cboIX_s-Lf6rNg-ryAehA9xBVs))
4 |
5 | - [SF Android GDG @ Square 2019 - Hello Workflow](https://www.youtube.com/watch?v=8PlYtfsgDKs)
6 | (live coding)
7 |
8 | - [Android Dialogs 5-part Coding Series](https://twitter.com/chiuki/status/1100810374410956800)
9 | - [1](https://www.youtube.com/watch?v=JJ4-8AR5HhA),
10 | - [2](https://www.youtube.com/watch?v=XB6frWBGvp0),
11 | - [3](https://www.youtube.com/watch?v=NdFJMkT-t3c),
12 | - [4](https://www.youtube.com/watch?v=aRxmyO6fwSs),
13 | - [5](https://www.youtube.com/watch?v=aKaZa-1KN2M)
14 |
15 | - [Reactive Workflows a Year Later – Droidcon NYC 2018](https://www.youtube.com/watch?v=cw9ZF9-ilac)
16 |
17 | - [The Reactive Workflow Pattern – Fragmented Podcast](https://www.youtube.com/watch?v=mUBXgYnT7w0)
18 |
19 | - [The Reactive Workflow Pattern Update – Droidcon SF 2017](https://www.youtube.com/watch?v=mvBVkU2mCF4)
20 |
21 | - [The Rx Workflow Pattern – Droidcon NYC 2017](https://www.youtube.com/watch?v=KjoMnsc2lPo)
22 | ([slides](https://speakerdeck.com/rjrjr/reactive-workflows))
23 |
--------------------------------------------------------------------------------
/docs/images/down_the_view_tree.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | Produced by OmniGraffle 6.6.2 2022-05-27 20:49:42 +0000 Canvas 1 Layer 1 Native view system MessageScr een InboxScr een Runtime W orkfl ow r oot container EmailBr owser W orkfl ow SplitScr een( InboxScr een, MessageScr een ) W orkfl ow container Custom split view W orkfl ow container Custom inbox view W orkfl ow container Custom message view
4 |
--------------------------------------------------------------------------------
/docs/images/email_browser_workflow_schematic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | Produced by OmniGraffle 6.6.2 2022-05-25 18:58:42 +0000 Canvas 1 Layer 1 EmailBr owserW orkfl ow State { messages: List<MessageId>, selection: MessageId } SplitScr een
4 |
--------------------------------------------------------------------------------
/docs/images/email_inbox_workflow_schematic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | Produced by OmniGraffle 6.6.2 2022-05-25 18:58:42 +0000 Canvas 1 Layer 1 InboxW orkfl ow MessageId List<MessageId> onMessageSelected () InboxScr een
4 |
--------------------------------------------------------------------------------
/docs/images/email_message_workflow_schematic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | Produced by OmniGraffle 6.6.2 2022-05-25 18:58:42 +0000 Canvas 1 Layer 1 Message W orkfl ow MessageId MessageScr een
4 |
--------------------------------------------------------------------------------
/docs/images/email_schematic_renderings_only.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | Produced by OmniGraffle 6.6.2 2022-05-27 17:33:43 +0000 Canvas 1 Layer 1 EmailBr owser W orkfl ow Inbox W orkfl ow Inbox Scr een Message W orkfl ow Message Scr een SplitScr een( InboxScr een, MessageScr een )
4 |
--------------------------------------------------------------------------------
/docs/images/game_workflow_schematic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | Produced by OmniGraffle 6.6.2 2022-05-25 18:58:42 +0000 Canvas 1 Layer 1 GameW orkfl ow GameState { … } GameOver Players onClick () GameScr een
4 |
--------------------------------------------------------------------------------
/docs/images/icon-square.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/square/workflow/a4a77a3a2dc07a5bf38f1b6489be75f4e15d686d/docs/images/icon-square.png
--------------------------------------------------------------------------------
/docs/images/split_screen_schematic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | Produced by OmniGraffle 6.6.2 2022-05-27 16:16:23 +0000 Canvas 1 Layer 1 EmailBr owserW orkfl ow State { messages: List<MessageId>, selection: MessageId } SplitScr een( InboxScr een, MessageScr een ) InboxW orkfl ow List<MessageId> InboxScr een Message W orkfl ow MessageId MessageScr een
4 |
--------------------------------------------------------------------------------
/docs/images/split_screen_update.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | Produced by OmniGraffle 6.6.2 2022-05-27 16:16:23 +0000 Canvas 1 Layer 1 onMessageSelected () EmailBr owserW orkfl ow State { messages: List<MessageId>, selection: MessageId } SplitScr een( InboxScr een, MessageScr een ) InboxW orkfl ow List<MessageId> InboxScr een Message W orkfl ow MessageId MessageScr een MessageId
4 |
--------------------------------------------------------------------------------
/docs/images/swift/nested_workflow_rendering.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/square/workflow/a4a77a3a2dc07a5bf38f1b6489be75f4e15d686d/docs/images/swift/nested_workflow_rendering.png
--------------------------------------------------------------------------------
/docs/images/swift/workflow_rendering.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/square/workflow/a4a77a3a2dc07a5bf38f1b6489be75f4e15d686d/docs/images/swift/workflow_rendering.png
--------------------------------------------------------------------------------
/docs/images/workflow_schematic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | Produced by OmniGraffle 6.6.2 2022-05-25 18:58:42 +0000 Canvas 1 Layer 1 State Output Pr ops events Rendering
4 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ../README.md
--------------------------------------------------------------------------------
/docs/sequence_diagrams/README.md:
--------------------------------------------------------------------------------
1 | # Sequence Diagrams
2 |
3 | These are the source files used to generate the sequence diagrams via [WebSequenceDiagrams](https://www.websequencediagrams.com/).
4 |
--------------------------------------------------------------------------------
/docs/sequence_diagrams/nested_workflow_rendering.seq:
--------------------------------------------------------------------------------
1 | title Nested Workflow Rendering
2 |
3 | autonumber 1
4 | [->WorkflowHost: init(workflow:)
5 | WorkflowHost->+WorkflowNode: render()
6 | WorkflowNode->+SubtreeManager: render(callback:)
7 | SubtreeManager->+Context: init
8 | Context-->-SubtreeManager: Context
9 | SubtreeManager->SubtreeManager: wrap in RenderContext
10 | SubtreeManager->WorkflowNode: callback(renderContext)
11 |
12 | activate WorkflowNode
13 |
14 | WorkflowNode->+WorkflowA: render(state:context:)
15 | WorkflowA->+Context: render(workflow: WorkflowB(), key:outputMap:)
16 |
17 | alt ChildWorkflow exists
18 | Context->ChildWorkflow: update()
19 | else ChildWorkflow doesn't exist
20 | Context->ChildWorkflow: init()
21 | end
22 |
23 | Context->+ChildWorkflow: render()
24 | participant "WorkflowNode " as WorkflowNode2
25 | "ChildWorkflow"->ref over WorkflowNode2: render()
26 | This is recursive. We go
27 | back to step 2 to render
28 | any child workflows.
29 | end ref -->"ChildWorkflow": Rendering
30 | "ChildWorkflow"-->-Context: WorkflowB.Rendering
31 |
32 | Context-->-WorkflowA: Rendering
33 | WorkflowA-->-WorkflowNode: Rendering
34 | WorkflowNode-->SubtreeManager: Rendering
35 | deactivate WorkflowNode
36 |
37 | SubtreeManager-->-WorkflowNode: Rendering
38 | WorkflowNode-->-WorkflowHost: Rendering
39 |
40 | option footer=bar
41 |
--------------------------------------------------------------------------------
/docs/sequence_diagrams/workflow_rendering.seq:
--------------------------------------------------------------------------------
1 | title Simple Workflow Rendering
2 |
3 | [->WorkflowHost: init(workflow:)
4 | WorkflowHost->+WorkflowNode: render()
5 | WorkflowNode->+SubtreeManager: render(callback:)
6 | SubtreeManager->SubtreeManager: create Context
7 | SubtreeManager->SubtreeManager: wrap in RenderContext
8 | SubtreeManager->WorkflowNode: callback(renderContext)
9 | activate WorkflowNode
10 | WorkflowNode->+Workflow: render(state:context:)
11 | Workflow-->-WorkflowNode: Rendering
12 | WorkflowNode-->SubtreeManager: Rendering
13 | deactivate WorkflowNode
14 | SubtreeManager-->-WorkflowNode: Rendering
15 | WorkflowNode-->-WorkflowHost: Rendering
16 |
17 | option footer=bar
18 |
--------------------------------------------------------------------------------
/docs/userguide/common-patterns.md:
--------------------------------------------------------------------------------
1 | # Common Patterns
2 |
3 | There are a lot associated/generic types in workflow code – that doesn't mean you always need to use
4 | all of them. Here are some common configurations we've seen.
5 |
6 | ## Stateless Workflows
7 |
8 | Remember that workflow state is made up of public and private parts. When a workflow's state
9 | consists entirely of public state (i.e. it's initializer arguments in Swift or `PropsT` in Kotlin),
10 | it can ignore all the machinery for private state. In Swift, the`State` type can be `Void`, and in
11 | `Kotlin` it can be `Unit` – such workflows are often referred to as "stateless", since they have no
12 | state of their own.
13 |
14 | ## Props-less Workflows
15 |
16 | Some workflows manage all of their state internally, and have no public state (aka props). In Swift,
17 | this just means the workflow implementation has no parameters (although this is rare, see
18 | _Injecting Dependencies_ below). In Kotlin, the `PropsT` type can be `Unit`. `RenderContext` has
19 | convenience overloads of most of its functions to implicitly pass `Unit` for these workflows.
20 |
21 | ## Outputless Workflows
22 |
23 | Workflows that only talk to their parent via their `Rendering`, and never emit any output, are
24 | encouraged to indicate that by using the [bottom type](https://en.wikipedia.org/wiki/Bottom_type) as
25 | their `Output` type. In addition to documenting the fact that the workflow will never output, using
26 | the bottom type also lets the compiler enforce it – code that tries to emit outputs will not
27 | compile. In Swift, the `Output` type is specified as [`Never`](https://nshipster.com/never/). In
28 | Kotlin, use [`Nothing`](https://medium.com/@agrawalsuneet/the-nothing-type-kotlin-2e7df43b0111).
29 |
30 | ## Composite Workflows
31 |
32 | Composition is a powerful tool for working with Workflows. A workflow can often accomplish a lot
33 | simply by rendering various children. It may just combine the renderings of multiple children, or
34 | use its props to determine which of a set of children to render. Such workflows can often be
35 | stateless.
36 |
37 | ## One-and-done Workflows (RenderingT v. OutputT)
38 |
39 | A common question is “why can’t I emit output from `initialState`,” or “what if my Workflow realizes it doesn’t actually need to run? The most efficient, and most expressive, way to handle this is to use an optional or conditional `Rendering` type, and an `Output` of [`Never`](https://nshipster.com/never/)/[`Nothing`](https://medium.com/@agrawalsuneet/the-nothing-type-kotlin-2e7df43b0111).
40 |
41 | Imagine a `PromptForPermissionMaybeWorkflow`, that renders a UI to get a passcode, but only if that permission has not already been granted. If you make its `RenderingT` nullable (e.g. `Screen?`), it can return `null` to indicate that its job is done. Its callers will be synchronously informed that the coast is clear, and can immediately render what they actually care about.
42 |
43 | Another variation of this pattern is to use a sealed class / enum type for `Rendering`, with a `Working` type that implements `Screen`, and a unviewable `Finished` type that carries the work product.
44 |
45 | A good rule of thumb for choosing between using `Rendering` or `Output` is to remember that `Output` is event-like, and is always asynchronous. A parent waiting for an output must be given something to render in the meantime. Using `Rendering` is a great idiom for a one-and-done workflow tasked with providing a single product, especially one that might be available instantly.
46 |
47 | ## Props values v. Injected Dependencies
48 |
49 | [Dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) is a technique for making
50 | code less coupled and more testable. In short, it's better for classes/structs to accept their
51 | dependencies when they're created instead of hard-coding them. Workflows typically have dependencies
52 | like specific Workers they need to perform some tasks, child workflows to delegate rendering to, or
53 | helpers for things like network requests, formatting and logging.
54 |
55 | ### Swift
56 |
57 | A Swift workflow typically receives its dependencies as initializer arguments, just like its input
58 | values, and is normally instantiated anew by its parent in each call to the parent’s render method.
59 | The [factory pattern](https://en.wikipedia.org/wiki/Factory_method_pattern) can be employed to keep
60 | knowledge of children’s implementation details from leaking into their parents.
61 |
62 | ### Kotlin
63 |
64 | Kotlin workflows make a more formal distinction between dependencies and props, via the `PropsT`
65 | parameter type on the Kotlin `Workflow` interface. Dependencies (e.g. a network service) are
66 | typically provided as constructor parameters, while props values (e.g. a record locator) are
67 | provided by the parent as an argument to the `RenderContext.renderChild` method. This works
68 | seamlessly with DI libraries like [Dagger](https://dagger.dev/).
69 |
70 | The careful reader will note that this is technically storing "state" in the workflow instance –
71 | something that is generally discouraged. However, since this "state" is never changed, we can make
72 | an exception for this case. If a workflow has properties, they should _only_ be used to store
73 | injected dependencies or dependencies derived from injected ones (e.g. `Worker`s created from
74 | `Observable`s).
75 |
76 | !!! info Swift vs Kotlin
77 | This difference between Swift and Kotlin practices is a side effect of Kotlin’s lack of a
78 | parallel to Swift’s `Self` type. Kotlin has no practical way to provide a method like Swift’s
79 | `Workflow.workflowDidChange`, which accepts a strongly typed reference to the instance from the
80 | previous run of a parent’s `Render` method. Kotlin’s alternative,
81 | `StatefulWorkflow.onPropsChanged`, requires the extra `PropsT` type parameter.
82 |
--------------------------------------------------------------------------------
/docs/userguide/concepts.md:
--------------------------------------------------------------------------------
1 | # Workflow Core
2 |
3 | This page provides a high level overview of Workflow Core, the UI-agnostic Swift and Kotlin runtimes at the heart of the Workflow libraries.
4 | See [Workflow UI](../ui-concepts) to learn about the companion Android and iOS specific modules.
5 |
6 | ## What is a Workflow?
7 |
8 | A [Workflow](../../glossary#workflow-instance) defines the possible states and behaviors of components of a particular type.
9 | The overall state of a Workflow has two parts:
10 |
11 | * [Props](../../glossary#props), configuration information provided by whatever is running the Workflow
12 | * And the private [State](../../glossary#state) managed by the Workflow itself
13 |
14 | At any time, a Workflow can be asked to transform its current Props and State into a [Rendering](../../glossary#rendering) that is suitable for external consumption.
15 | A Rendering is typically a simple struct with display data, and event handler functions that can enqueue [Workflow Actions](../../glossary#action) — functions that update State, and which may at the same time emit [Output](../../glossary#output) events.
16 |
17 | 
18 |
19 | For example, a Workflow running a simple game might be configured with a description of the participating `Players` as its Props, build `GameScreen` structs when asked to render, and emit a `GameOver` event as Output to signal that is finished.
20 |
21 | 
22 |
23 | A workflow Rendering usually serves as a view model in iOS or Android apps, but that is not a requirement.
24 | Again, this page includes no details about how platform specific UI code is driven.
25 | See [Workflow UI](../ui-concepts) for that discussion.
26 |
27 | !!! note
28 | Readers with an Android background should note the lower case _v_ and _m_ of "view model" — this notion has nothing to do with Jetpack `ViewModel`.
29 |
30 | ## Composing Workflows
31 |
32 | Workflows run in a tree, with a single root Workflow declaring it has any number of children for a particular state, each of which can declare children of their own, and so on.
33 | The most common reason to compose Workflows this way is to build big view models (Renderings) out of small ones.
34 |
35 | For example, consider an overview / detail split screen, like an email app with a list of messages on the left, and the body of the selected message on the right.
36 | This could be modeled as a trio of Workflows:
37 |
38 | **InboxWorkflow**
39 |
40 | * Expects a `List` as its Props
41 | * Rendering is an `InboxScreen`, a struct with displayable information derived from its Props, and an `onMessageSelected()` function
42 | * When `onMessageSelected()` is called, a WorkflowAction is executed which emits the given `MessageId` as Output
43 | * Has no private State
44 |
45 | 
46 |
47 | **MessageWorkflow**
48 |
49 | * Requires a `MessageId` Props value to produce a `MessageScreen` Rendering
50 | * Has no private State, and emits no Output
51 |
52 | 
53 |
54 | **EmailBrowserWorkflow**
55 |
56 | * State includes a `List`, and the selected `MessageId`
57 | * Rendering is a `SplitScreen` view model, to be assembled from the renderings of the other two Workflows
58 | * Accepts no Props, and emits no Output
59 |
60 | 
61 |
62 | When `EmailBrowserWorkflow` is asked to provide its Rendering, it in turn asks for Renderings from its two children.
63 |
64 | * It provides the `List` from its state as the Props for `EmailInboxWorkflow` and receives an `InBoxScreen` rendering in return. That `InboxScreen` becomes the left pane of a `SplitScreen` Rendering.
65 | * For the `SplitScreen`'s right pane, the browser Workflow provides the currently selected `MessageId` as input to `EmailMessageWorkflow`, to get a `MessageScreen` rendering.
66 |
67 | 
68 |
69 | !!! note
70 | Note that the two children, `EmailInboxWorkflow` and `EmailMessageWorkflow`, have no knowledge of each other, nor of the context in which they are run.
71 |
72 | The `InboxScreen` rendering includes an `onMessageSelected(MessageId)` function.
73 | When that is called, `EmailInboxWorkflow` enqueues an Action function that emits the given `MessageId` as Output.
74 | `EmailBrowserWorkflow` receives that Output, and enqueues another Action that updates the `selection: MessageId` of its State accordingly.
75 |
76 | 
77 |
78 | Whenever such a [Workflow Action cascade](../../glossary#action-cascade) fires, the root Workflow is asked for a new Rendering.
79 | Just as before, `EmailBrowserWorkflow` delegates to its two children for their Renderings, this time providing the new value of `selection` as the updated Props for `MessageWorkflow`.
80 |
81 |
88 |
89 | ## Why does Workflow work this way?
90 |
91 | Workflow was built to tame the composition and navigation challenges presented by Square's massive Android and iOS apps.
92 | It lets us write intricate, centralized, well tested code encapsulating the flow through literally hundreds of individual screens.
93 | These days we are able to see and shape the forest, despite all of the trees.
94 |
95 | We built it with two core design principals in mind:
96 |
97 | * Unidirectional data flow is the best way to stay sane when building UI
98 | * Declarative programming is the best way to define unidirectional data flows
99 |
100 | What does that actually mean?
101 |
102 | ### Unidirectional Data Flow
103 |
104 | There is a wealth of information on the web about [Unidirectional Data Flow](https://www.google.com/search?q=unidirectional+data+flow),
105 | but it very simply means that there is a single path along which data travel _from_ your business
106 | logic to your UI, and events travel _to_ your business logic from your UI, and they always and only
107 | travel in one direction along that path. For Workflow, this also implies that the UI is (almost)
108 | stateless, and that the interesting state for your app is centralized and not duplicated.
109 |
110 | In practice, this makes program flow much easier to reason about because anytime something happens
111 | in an app, it removes the questions of where the state came from that caused it, which components
112 | got which events, and which sequences of cause and effect actually occurred. It makes unit testing
113 | easier because state and events are explicit, and always live in the same place and flow through the
114 | same APIs, so unit tests only need to test state transitions, for the most part.
115 |
116 | ### Declarative vs Imperative
117 |
118 | Traditionally, most mobile code is [“imperative”](https://en.wikipedia.org/wiki/Imperative_programming)
119 | – it consists of instructions for how to build and display the UI. These instructions can include
120 | control flow like loops. Imperative code is usually stateful, state is usually sprinkled all over
121 | the place, and tends to care about instances and identity. When reading imperative code, you almost
122 | have to run an interpreter and keep all the pieces of state in your head to figure out what it does.
123 |
124 | Web UI is traditionally [declarative](https://en.wikipedia.org/wiki/Declarative_programming) – it
125 | describes what to render, and some aspects of how to render it (style), but doesn’t say how to
126 | actually draw it. Declarative code is usually easier to read than imperative code. It
127 | describes what it produces, not how to generate it. Declarative code usually cares more about pure
128 | values than instance identities. However, since computers still need actual instructions at some
129 | point, declarative code requires something else, usually imperative, either a compiler or
130 | interpreter, to actually do something with it.
131 |
132 | Workflow code is written in regular Kotlin or Swift, which are both imperative languages, but the
133 | library encourages you to write your logic in a declarative and functional style. The library
134 | manages state and wiring up event handling for you, so the only code you need to write is code that
135 | is actually interesting for your particular problem.
136 |
137 | !!! note "A note about functional programming"
138 | Kotlin and Swift are not strictly functional programming languages, but both have features that allow you to write [functional](https://en.wikipedia.org/wiki/Functional_programming)-style code.
139 | Functional code discourages side effects and is generally much easier to test than object-oriented code.
140 | Functional and declarative programming go very well together, and Workflow encourages you to write such code.
141 |
--------------------------------------------------------------------------------
/docs/userguide/implementation.md:
--------------------------------------------------------------------------------
1 | # Implementation Notes
2 |
3 | !!! info "Work in progress…"
4 | So far we only have notes on the implementation of the Swift runtime.
5 | They're actually pretty close to what goes on in Kotlin, the `WorkflowNode` and `SubtreeManager` classes in particular.
6 |
7 | ## Swift
8 |
9 | ### The Render loop
10 |
11 | #### Initial pass
12 |
13 | 
14 |
15 | The root of your workflow hierarchy gets put into a `WorkflowHost` (if you're using
16 | `ContainerViewController` this is created for you). As part of its initializer, `WorkflowHost`
17 | creates a `WorkflowNode` that wraps the given root `Workflow` (and keeps track of the `Workflow`'s
18 | `State`). It then calls `render()` on the node:
19 |
20 | ```swift
21 | // WorkflowHost
22 | public init(workflow: WorkflowType, debugger: WorkflowDebugger? = nil) {
23 | self.debugger = debugger
24 |
25 | self.rootNode = WorkflowNode(workflow: workflow) // 1. Create the node
26 |
27 | self.mutableRendering = MutableProperty(self.rootNode.render()) // 2. Call render()
28 | ```
29 |
30 | `WorkflowNode` contains a `SubtreeManager`, whose primary purpose is to manage child workflows
31 | (more on this later). When `render()` gets invoked on the node, it calls `render` on the
32 | `SubtreeManager` and passes a closure that takes a `RenderContext` and returns a `Rendering` for
33 | the `Workflow` associated with the node.
34 |
35 | ```swift
36 | // WorkflowNode
37 | func render() -> WorkflowType.Rendering {
38 | return subtreeManager.render { context in
39 | return workflow.render(
40 | state: state,
41 | context: context
42 | )
43 | }
44 | }
45 | ```
46 |
47 | The `SubtreeManager` instantiates a `RenderContext` and invokes the closure that was passed in.
48 | This last step generates the `Rendering`. This `Rendering` then gets passed back up the call stack
49 | until it reaches the `WorkflowHost`.
50 |
51 | #### Composition
52 |
53 | In cases where a `Workflow` has child `Workflow`s, the render sequence is similar. The [tutorial]
54 | (../tutorial/building-a-workflow/#the-render-context) goes through this in more detail.
55 |
56 | 
57 |
58 | Essentially, a `Workflow` containing child `Workflow`s calls `render(context:key:outputMap:)` on
59 | each child `Workflow` and passes in the `RenderContext`. The context does some bookkeeping for the
60 | child `Workflow` (creating or updating a `ChildWorkflow`) and then calls `render()`.
61 | `ChildWorkflow.render()` calls `render()` on its `WorkflowNode` and we recurse back to step 2.
62 |
--------------------------------------------------------------------------------
/docs/userguide/testing-concepts.md:
--------------------------------------------------------------------------------
1 | # Workflow Testing
2 |
3 | _Coming soon!_
4 |
--------------------------------------------------------------------------------
/docs/userguide/ui-concepts.md:
--------------------------------------------------------------------------------
1 | # Workflow UI
2 |
3 | This page provides a high level overview of Workflow UI, the companion that allows [Workflow Core](../concepts) to drive Android and iOS apps.
4 | To see how these ideas are realized in code, move on to [Coding Workflow UI](../ui-in-code).
5 |
6 | !!! warning Kotlin WIP
7 | The `Screen` interface that is so central to this discussion has reached Kotlin very recently, via `v1.8.0-beta01`.
8 | Thus, if you are working against the most recent non-beta release, you will find the code blocks here don't match what you're seeing.
9 |
10 | Square is using the `Screen` machinery introduced with the beta at the heart of our Android app suite, and we expect the beta period to be a short one.
11 | The Swift `Screen` protocol _et al._ have been in steady use for years.
12 |
13 | ## What's a Screen?
14 |
15 | Most Workflow implementations produce `struct` / `data class` [renderings](../../glossary#rendering) that can serve as view models.
16 | Such a rendering provides enough data to paint a complete UI, including functions to be called in response to UI events.
17 |
18 | These view model renderings implement the `Screen` [protocol](https://github.com/square/workflow-swift/blob/main/WorkflowUI/Sources/Screen/Screen.swift) / [interface](https://github.com/square/workflow-kotlin/blob/main/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/Screen.kt) to advertise that this is their intended use.
19 | The core service provided by Workflow UI is to transform `Screen` types into platform-specific view objects, and to keep those views updated as new `Screen` renderings are emitted.
20 |
21 | `Screen` is the lynch pin that ties the Workflow Core and Workflow UI worlds together, the basic UI building block for Workflow-driven apps.
22 | A `Screen` is an object that can be presented as a basic 2D UI box, like an `android.view.View` or a `UIViewController`.
23 | And Workflow UI provides the glue that allows you to declare (at compile time!) that instances of `FooScreen : Screen` are used to drive `FooViewController`, `layout/foo_screen.xml`, or `@Composable fun Content(FooScreen, ViewEnvironment)`.
24 |
25 | !!! faq "Why \"Screen\"?"
26 | We chose the name "Screen" because "View" would invite confusion with the like-named Android and iOS classes, and because "Box" didn't occur to us.
27 | (No one seems to have been bothered by the fact that `Screen` and iOS's `UIScreen` are unrelated.)
28 |
29 | And really, we went with "Screen" because it's the nebulous term that we and our users have always used to discuss our apps:
30 | "Go to the Settings screen."
31 | "How do I get to the Tipping screen?"
32 | "The Cart screen is shown in a modal over the Home screen on tablets."
33 | It's a safe bet you understood each of those sentences.
34 |
35 | ## Workflow Tree, Rendering Tree, View Tree
36 |
37 | In the Workflow Core page we discussed how Workflows can be [composed as trees](../concepts#composing-workflows), like this email app driven by a trio of Workflows that assemble a composite `SplitScreen` rendering.
38 |
39 | 
40 |
41 | Let's take a look at how Workflow UI transforms such a [container screen](../../glossary#container-screen) into a [container view](../../glossary#container-view).
42 |
43 | The main connection between the Workflow Core runtime and a native view system is the stream of Rendering objects from the root Workflow, `EmailBrowserWorkflow` in this discussion.
44 | From that point on, the flow of control is entirely in view-land.
45 |
46 | The precise details of that journey vary between Android and iOS in terms of naming, subclassing v. delegating, and so on, mainly to ensure that the API is idiomatic for each audience.
47 | None the less, the broad strokes are the same.
48 | (Move on to [Coding Workflow UI](../ui-in-code) to drill into the platform-specific details.)
49 |
50 | Each flavor of Workflow UI provides two core container helpers, both pictured below:
51 |
52 | * A "workflow container", able to instantiate and update a view that can display Screen instances of the given type
53 | * In iOS this is `DescribedViewController`
54 | * For Android Classic we provide `WorkflowViewStub`, very similar to `android.view.ViewStub`.
55 | * Android Jetpack Compose code can call `@Compose fun WorkflowRendering()`.
56 | * A "workflow root container", able to field a stream of renderings from the Workflow Core runtime, and pass them on to a workflow container
57 | * `ContainerViewController` for iOS
58 | * `WorkflowLayout` for Android Classic
59 | * `@Compose fun Workflow.renderAsState()` for Android Jetpack Compose
60 |
61 | 
62 |
63 | When the runtime in our example is started, the flow is something like this:
64 |
65 | * `EmailBrowserWorkflow` is asked for its first Rendering, a `SplitScreen` wrapping an `InboxScreen` and a `MessageScreen`.
66 | * The _Workflow root container_ receives that, and hands it off to its _Workflow container_.
67 | * The container is able to resolve that `SplitScreen` instances can be displayed by views of the associated type _Custom split view_.
68 | * The container builds that view, and passes it the `SplitScreen`.
69 | * _Custom split view_ is written with two _Workflow containers_ of its own, one for the left side and for the right.
70 | * The left hand container resolves `InboxScreen` to _Custom inbox view_, builds one, and hands the rendering that new view.
71 | * The right hand container does the same for the `MessageScreen`, creating a _Custom message view_ to display it.
72 |
73 | Sooner or later the state of `EmailBrowserWorkflow` or one of its children will change.
74 | Perhaps a new message has been received.
75 | Perhaps an event handler function on `InboxScreen` has been called because the user wants to read something else now.
76 | Regardless of where in the Workflow hierarchy the update happens, the entire tree will be re-rendered: `EmailBrowserWorkflow` will be asked for a new Rendering, it will ask its children for the same, and so on.
77 |
78 | !!! tip "Yes, everything renders when anything changes"
79 | New Workflow developers generally freak out when they hear that the entire tree is re-rendered when any state anywhere updates.
80 | Remember that `render()` implementations are expected to be idempotent, and that their job is strictly declarative: `render()` effectively means "I assume these children are running, and that I am subscribed to these work streams. Please make sure that stays the case, or fire up some new ones if needed."
81 | Another way is to think of them as declaring how to adapt the internal State into the external Rendering.
82 | These calls should be cheap, with all real work happening outside of the `render()` call.
83 |
84 | Optimizations may prevent rendering calls that are clearly redundant from being made, but semantically one should assume that the whole world is rendered when any part of the world changes.
85 |
86 | Once the runtime's Workflow tree finishes re-rendering, the new `SplitScreen` is passed through the native view system like so:
87 |
88 | * The _Workflow root container_ once again passes the new `SplitScreen` to its _Workflow container_, because that is the only trick it knows.
89 | * That container recognizes that `SplitScreen` can be accepted by the _Custom split view_ it created last time, and so there is no work to be done.
90 | * The existing _Custom split view_ receives the new `SplitScreen`.
91 | * Just like last time, _Custom split view_ passes `InboxScreen` to the _Workflow container_ on its left, and `MessageScreen` to that on its right.
92 | * The left hand _Workflow container_ sees that it is already showing a _Custom inbox view_ and passes `InboxScreen` rendering through.
93 | * The same things happens with `MessageScreen`, and the _Custom message view_ previously built by the right hand _Workflow container_.
94 |
95 | As is always the case with view code, _Custom inbox view_ and _Custom message view_ should be written with care to avoid redundant work, comparing what they are already showing with what they are being asked to show now.
96 | (A simple way to do this is to keep a Screen type's display data in a separate object from its event handlers, as an Equatable Swift struct, or as a Kotlin data class.
97 | Always hold on to the latest Screen in a `var`, and write UI click handlers and to reference it.)
98 |
99 | The update scenario would be different if the types of any of the `Screen` Renderings changed.
100 | Suppose our email app is able to host both email and voice mail in its inbox, and that the `MessageScreen` from the previous update is replaced with a `VoicemailScreen` this time.
101 | In that case, _Custom message view_ would refuse the new Rendering, and the right hand _Workflow container_ that created it would destroy it.
102 | A _Custom voicemail view_ would be created in its stead, and that new view would paint itself with the information from the `VoicemailScreen`.
103 |
104 | So just how do these containers know what views to create for what Screen types?
105 | Those details are very language and platform specific, and are covered in the next page, under [Building views from Screens](../ui-in-code#view-binding).
106 |
107 | ## ViewEnvironment
108 |
109 | TK
110 |
--------------------------------------------------------------------------------
/docs/userguide/ui-in-code.md:
--------------------------------------------------------------------------------
1 | # Coding Workflow UI
2 |
3 | This page translates the high level discussion of [Workflow UI](../ui-concepts) into Android and iOS code.
4 |
5 | ## Separation of Concerns
6 |
7 | Workflow maintains a rigid separation between its core runtime and its UI support.
8 | The [Workflow Core](../concepts) modules are strictly Swift and Kotlin, with no dependencies on any UI framework.
9 | Dependencies on Android and iOS are restricted to the Workflow UI modules, as you would expect.
10 | This innate separation naturally puts developers on a path to avoid entangling view concerns with their app logic.
11 |
12 | And note that we say "app logic" rather than "business logic."
13 | In any interesting app, the code that manages navigation and other UI-releated behavior is likely to dwarf that for what we typically think of as model concerns, in both size and complexity.
14 |
15 | We're all pretty good at capturing business concerns in tidy object-oriented models of items for sale, shopping carts, payment cards and the like, nicely decoupled from the UI world.
16 | But the rest of the app, and in particular the bits about how our users navigate it?
17 | Traditionally it's hard to keep that app-specific logic centralized, so that you can see what's going on; and even harder to keep it decoupled from your view system, so that it's easy to test.
18 | The strict divide between Workflow UI and Workflow Core leads you to maintain that separation by accident.
19 |
20 | ## Bootstrapping
21 |
22 | The following snippets demonstrate using Workflow to drive the root views of iOS and Android apps.
23 | But really, you can host a Workflow driven UI anywhere you can show a view, whatever "view" means on your platform.
24 |
25 | === "iOS"
26 | ```Swift
27 | @UIApplicationMain
28 | class AppDelegate: UIResponder, UIApplicationDelegate {
29 | var window: UIWindow?
30 |
31 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
32 | window = UIWindow(frame: UIScreen.main.bounds)
33 |
34 | window?.rootViewController = ContainerViewController(workflow: RootWorkflow())
35 |
36 | window?.makeKeyAndVisible()
37 |
38 | return true
39 | }
40 | }
41 | ```
42 |
43 | === "Android Classic"
44 | Android classic makes things a little complicated (naturally), as your Workflow runtime has to survive configuration changes.
45 | Our habit is use a Jetpack `ViewModel` to solve that problem, on what is typically the only line of code in a Workflow app that deals with the Jetpack Lifecycle at all.
46 |
47 | ```kotlin title="HelloWorkflowActivity.kt"
48 | class HelloWorkflowActivity : AppCompatActivity() {
49 | override fun onCreate(savedInstanceState: Bundle?) {
50 | super.onCreate(savedInstanceState)
51 |
52 | // This ViewModel will survive configuration changes. It's instantiated
53 | // by the first call to androidx.activity.viewModels(), and that
54 | // original instance is returned by succeeding calls.
55 | val model: HelloViewModel by viewModels()
56 | setContentView(
57 | WorkflowLayout(this).apply { take(lifecycle, model.renderings) }
58 | )
59 | }
60 | }
61 |
62 | class HelloViewModel(savedState: SavedStateHandle) : ViewModel() {
63 | val renderings: StateFlow by lazy {
64 | renderWorkflowIn(
65 | workflow = HelloWorkflow,
66 | scope = viewModelScope,
67 | savedStateHandle = savedState
68 | )
69 | }
70 | }
71 | ```
72 |
73 | === "Android Jetpack Compose"
74 | ```kotlin title="HelloComposeActivity.kt"
75 | class HelloComposeActivity : AppCompatActivity() {
76 | override fun onCreate(savedInstanceState: Bundle?) {
77 | super.onCreate(savedInstanceState)
78 | setContent {
79 | val rendering by HelloWorkflow.renderAsState(props = Unit, onOutput = {})
80 | WorkflowRendering(rendering, ViewEnvironment.EMPTY)
81 | }
82 | }
83 | }
84 | ```
85 |
86 | Android developers should note that classic and Compose bootstrapping are completely interchangeable.
87 | Each style is able to display Screens of any type, regardless of whether they are set up to inflate `View` instances or to run `@Composeable` functions.
88 |
89 | ## Building views from Screens
90 |
91 | Hello, Screen world.
92 |
93 | === "iOS"
94 | ```swift title="WelcomeScreen.swift"
95 | struct WelcomeScreen: Screen {
96 | var name: String
97 | var onNameChanged: (String) -> Void
98 | var onLoginTapped: () -> Void
99 |
100 | func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription {
101 | return WelcomeViewController.description(for: self, environment: environment)
102 | }
103 | }
104 |
105 | private final class WelcomeViewController: ScreenViewController {
106 | override func viewDidLoad() { … }
107 | override func viewDidLayoutSubviews() { … }
108 |
109 | override func screenDidChange(from previousScreen: WelcomeScreen, previousEnvironment: ViewEnvironment) {
110 | super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment)
111 |
112 | nameField.text = screen.name
113 | }
114 | }
115 | ```
116 |
117 | iOS `Screen` classes are expected to provide matching `ViewControllerDescription` instances.
118 | A `ViewControllerDescription` can build a `UIViewController` on demand, or update an existing one if it's recognized by `ViewControllerDescription.canUpdate(UIViewController)`.
119 |
120 | These duties are all fullfilled by the provided `open class ScreenViewController`.
121 | It's like any other `ViewController`, with the addition of:
122 |
123 | * an open `screenDidChange()` method that the Workflow UI runtime calls with a series of `Screen` instances of the specified type
124 | * a `description()` class method, perfect for calling from `Screen.viewcontrollerDescription()`
125 |
126 | === "Android Classic"
127 | ```kotlin title="HelloScreen.kt"
128 | data class HelloScreen(
129 | val message: String,
130 | val onClick: () -> Unit
131 | ) : AndroidScreen {
132 | override val viewFactory: ScreenViewFactory =
133 | fromViewBinding(HelloViewBinding::inflate) { helloScreen, viewEnvironment ->
134 | helloMessage.text = helloScreen.message
135 | helloMessage.setOnClickListener { helloScreen.onClick() }
136 | }
137 | }
138 | ```
139 |
140 | The Android `Screen` interface is purely a marker type.
141 | It defines no Android-specific methods to ensure you have the option of keeping your app logic pure.
142 | If you don't need that rigor, life is simpler (and safer, no runtime errors) if your `Screen` renderings implement `AndroidScreen` instead.
143 |
144 | An `AndroidScreen` is required to provide a matching `ScreenViewFactory`.
145 | `ScreenViewFactory` returns `View` instances wrapped in `ScreenViewHolder` objects.
146 | `ScreenViewHolder.show` is called by the Workflow UI runtime to update the view with `Screen` instances that are deemed acceptible by `ScreenViewHolder.canShow`.
147 |
148 | In this example the `fromViewBinding` function creates a `ScreenViewFactory` that builds `View` instances using a [Jetpack View Binding](https://developer.android.com/topic/libraries/view-binding), `HelloViewBinding`, presumably derived from `hello_view_binding.xml`.
149 | The lamda argument to the `fromViewBinding` provides the implementation for `ScreenViewHolder.show`, and is guaranteed that the given `helloScreen` parameter is of the appropriate type.
150 |
151 | Other factory functions are provided to work with layout resources directly, or to build views entirely from code.
152 |
153 | === "Android Jetpack Compose"
154 | ```kotlin title="HelloScreen.kt"
155 | data class HelloScreen(
156 | val message: String,
157 | val onClick: () -> Unit
158 | ) : ComposeScreen {
159 | @Composable override fun Content(viewEnvironment: ViewEnvironment) {
160 | Button(onClick) {
161 | Text(message)
162 | }
163 | }
164 | }
165 | ```
166 |
167 | Here, `HelloScreen` is implementing `ComposeScreen`.
168 | `ComposeScreen` extends the same `AndroidScreen` class used for classic Android, defining `@Composable fun Content()` to get its work done.
169 | `Content` is always called from a `@Composable Box()` context.
170 |
171 | !!! tip "It's context aware"
172 | Even though `AndroidScreen` provides a thing called `ScreenViewFactory` to do its work, the factories built by `ComposeScreen` are able to recognize whether they're being called from a classic `View` or from a `@Composeable` function, and do the right thing.
173 | Workflow UI only creates `ComposeView` instances as needed: when a `@Composeable` needs to be shown in a `View`.
174 | If the factory is to be used in a `@Composable` context, `Content()` is called directly.
175 |
176 | ## Where is that "separation of concerns" you promised?
177 |
178 | After all the chest-thumping above about [Separation of Concerns](#separation-of-concerns), the code samples above probably look pretty entangled.
179 | That's because, while the Workflow libraries themselves are completely decoupled, they don't force that strict rigor on your app code.
180 |
181 | If you aren't building, say, a core Workflow module that you want to ship separately from its Android and command line interfaces, you'd probably gain nothing from enforced separation but boilerplate and runtime errors.
182 | And in practice, your Workflow unit tests won't call `viewFactory` and will build and run just fine against the JVM.
183 | Likewise, at this point we've been building apps this way for hundreds of engineering years, and so far no one has called `viewControllerDescription()` and stashed a `UIViewController` in their workflow state.
184 | (This is not a challenge.)
185 |
186 | If you are one of the few who truly do need impermeable boundaries between your core and UI modules, they aren't hard to get.
187 | Your `Screen` implementations can be defined completely separately from their view code and bound later.
188 |
189 | === "iOS"
190 | ```Swift title="WelcomeScreen.swift"
191 | struct WelcomeScreen {
192 | var name: String
193 | var onNameChanged: (String) -> Void
194 | var onLoginTapped: () -> Void
195 | }
196 | ```
197 | ```Swift title="WelcomeViewController.swift"
198 | extension WelcomeScreen: Screen {
199 | func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription {
200 | return WelcomeViewController.description(for: self, environment: environment)
201 | }
202 | }
203 |
204 | private final class WelcomeViewController: ScreenViewController {
205 | // ...
206 | ```
207 |
208 | === "Android"
209 | ```Kotlin title="HelloScreen.kt"
210 | data class HelloScreen(
211 | val message: String,
212 | val onClick: () -> Unit
213 | ) : Screen
214 | ```
215 | ```kotlin title="HelloWorkflowGreenTheme.kt"
216 | private object HelloScreenGreenThemeViewFactory: ScreenViewFactory
217 | by ScreenViewFactory.fromViewBinding(GreenHelloViewBinding::inflate) { r, _ ->
218 | helloMessage.text = r.message
219 | helloMessage.setOnClickListener { r.onClick() }
220 | }
221 | }
222 | private val viewRegistry = ViewRegistry(HelloScreenGreenThemeViewFactory)
223 |
224 | val HelloWorkflowGreenTheme =
225 | HelloWorkflow.mapRenderings { it.withRegistry(viewRegistry) }
226 | ```
227 |
228 | ## Container screens make container views
229 |
230 | A [container screen](../../Glossary#container-screen) is one that is built out of other Screens.
231 | And naturally enough, the thing that a container screen drives is a [container view](../../Glossary#container-view): one that is able to host child views that are driven by Screen instances of arbitrary type.
232 |
233 | Workflow UI provides two root container views out of the box, the `ContainerViewController` and `WorkflowLayout` classes discussed above, under [Bootstrapping](#bootstrapping).
234 | They do most of their work by delegating to another pair of support view classes: `ScreenViewController` for iOS and `WorkflowViewStub` for Android.
235 | Android also provides `@Composable fun WorkflowRendering()` for use with Jetpack Compose.
236 | For something like a `SplitScreen` rendering, you'll write your own view code that does the same.
237 |
238 | === "iOS"
239 | ```swift title="SplitScreen.swift"
240 | public struct SplitScreen: Screen {
241 | public let leadingScreen: LeadingScreenType
242 |
243 | public let trailingScreen: TrailingScreenType
244 |
245 | public func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription {
246 | return SplitScreenViewController.description(for: self, environment: environment)
247 | }
248 | ```
249 | ```swift title="SplitScreenViewController.swift"
250 | internal final class SplitScreenViewController: ScreenViewController {
251 | internal typealias ContainerScreen = SplitScreen
252 |
253 | private var leadingContentViewController: DescribedViewController
254 | private lazy var leadingContainerView: ContainerView = .init()
255 |
256 | private lazy var separatorView: UIView = .init()
257 |
258 | private var trailingContentViewController: DescribedViewController
259 | private lazy var trailingContainerView: ContainerView = .init()
260 |
261 | required init(screen: ContainerScreen, environment: ViewEnvironment) {
262 | self.leadingContentViewController = DescribedViewController(
263 | screen: screen.leadingScreen,
264 | environment: environment
265 | )
266 | self.trailingContentViewController = DescribedViewController(
267 | screen: screen.trailingScreen,
268 | environment: environment
269 | )
270 | super.init(screen: screen, environment: environment)
271 | }
272 |
273 | override internal func screenDidChange(from previousScreen: ContainerScreen, previousEnvironment: ViewEnvironment) {
274 | super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment)
275 |
276 | update(with: screen)
277 | }
278 |
279 | private func update(with screen: ContainerScreen) {
280 | leadingContentViewController.update(
281 | screen: screen.leadingScreen,
282 | environment: environment
283 | )
284 | trailingContentViewController.update(
285 | screen: screen.trailingScreen,
286 | environment: environment
287 | )
288 |
289 | // Intentional force of layout pass after updating the child view controllers
290 | view.layoutIfNeeded()
291 | }
292 |
293 | override internal func viewDidLoad() {
294 | /** Lay out the two children horizontally, nothing workflow specific here. */
295 |
296 | update(with: screen)
297 | }
298 |
299 | override internal func viewDidLayoutSubviews() {
300 | /** Calculate the layout, nothing workflow specific here. */
301 | }
302 | ```
303 |
304 | The interesting thing here is the use of `DescribedViewController` to display the nested `leadingContent` and `trailingContent` Screens.
305 | `DescribedViewController` uses `Screen.viewControllerDescription` to build a new `UIViewController` if it needs to, or update an existing one if it can.
306 | Everything else is just run of the mill iOS view code.
307 |
308 | === "Android Classic"
309 |
310 | ```kotlin title="SplitScreen.kt"
311 | data class SplitScreen(
312 | val leadingScreen: L,
313 | val trailingScreen: T
314 | ): AndroidScreen> {
315 | override val viewFactory: ScreenViewFactory> =
316 | fromViewBinding(SplitScreenBinding::inflate) { screen, _ ->
317 | leadingStub.show(leadingScreen)
318 | trailingStub.show(trailingScreen)
319 | }
320 | }
321 | ```
322 | ```xml title="split_screen.xml"
323 |
324 |
329 |
330 |
336 |
337 |
342 |
343 |
349 |
350 |
351 | ```
352 |
353 | === "Android Jetpack Compose"
354 | ```kotlin title="SplitScreen.kt"
355 | data class SplitScreen(
356 | val leadingScreen: L,
357 | val trailingScreen: T
358 | ): ComposeScreen> {
359 | @Composable override fun Content(viewEnvironment: ViewEnvironment) {
360 | Row {
361 | WorkflowRendering(
362 | rendering = leadingScreen,
363 | modifier = Modifier
364 | .weight(1 / 3f)
365 | .fillMaxHeight()
366 | )
367 | WorkflowRendering(
368 | rendering = trailingScreen,
369 | modifier = Modifier
370 | .weight(2 / 3f)
371 | .fillMaxHeight()
372 | )
373 | }
374 | }
375 | }
376 | ```
377 |
--------------------------------------------------------------------------------
/docs/userguide/whyworkflow.md:
--------------------------------------------------------------------------------
1 | Why Workflow?
2 | ============
3 |
4 | So you want me to take the application feature I have to develop and break it down into separate components? And then enumerate every possible state for each of those components? As well as writing classes or structs that represent each of these states in addition to the collection of objects that each component might pass to another? That sounds like a lot of work just to help the seller order a set of gift cards! Why make something simple so complicated? Why should I use Workflow?
5 |
6 | I think even those of us who use Workflow all the time end up asking this question. It’s a very reasonable question that we try to answer here. At the heart of the matter, there are two complementary justifications for Workflow, which we will expand on below:
7 |
8 | 1. Software clarity, correctness, and testability (especially at scale).
9 | 2. Encouraging programming paradigms that are best practices for the mobile domain.
10 |
11 | ## Software clarity, correctness, and testability (at scale)
12 |
13 | I like to think that most of us have been there: It's our second straight day staring at the logs from over 200 customers. **We know what the problem is**: the user gets to screen Y and object foo’s state is bar, but foo _should not_ be bar while in screen Y.
14 |
15 | *Why is foo bar?*
16 |
17 | Unfortunately we don’t have any debug log for foo’s state at the time we start screen Y. We only have one when the user tries to click on button Z, and at that point the state is already bar even though it should only ever be baz or buz.
18 |
19 | What happened? How did foo get to state bar on screen Y? Looking at the code, foo is shared state with 15 other screens, and it is mutable in all of them. The logic to update foo’s state in screen Y happens in code that is coupled to interaction with button Z, so we cannot simply add a unit test for this, we need a complex UI test to reproduce screen Y. **We don’t know _how_ the problem happened and it almost seems like we _can’t_ know how without significant effort!**
20 |
21 | The story above is a little dramatic but I hope the feeling it invokes is familiar. It is a daunting task to reason through application code and build up a sufficient mental model of all possible side effects in any one feature area.
22 |
23 | Now scale up the numbers a bit — foo is shared by 150 other screens — and the once daunting task seems almost impossible.
24 |
25 | All mobile developers face some form of the above problem, and at Square within our Point of Sale applications we face the scaled up version every day.
26 |
27 | ### What do we want?
28 |
29 | * Clear boundaries _between_ each feature’s software components that can be instrumented with logs and that have contracts that can be tested.
30 | * Clear expectations for outcomes _within_ a particular feature’s software component that can be verified for correctness with tests.
31 | * Immutable State within any particular scope (e.g. Screen Y in the context above) so that the code handling mutations to provide a new State as a result of some event is in a “protected area” that can be instrumented and tested.
32 | * A clear separation of the State updates from the presentation of the UI.
33 |
34 | We want the conditions above because we want:
35 |
36 | 1. Not to have bugs like the one we started this discussion with. In other words, we want our tests to give us confidence in our application logic.
37 | 2. In the inevitable case that we do have a bug, we want to be able to isolate the scenario, reproduce the exact conditions, fix the bug and write a test so that it doesn’t happen again.
38 |
39 | Workflow facilitates these goals for native mobile applications by providing a pattern (and a supporting application Runtime) similar to React, Elm, or any number of other web application JavaScript frameworks (not to mention forthcoming native mobile frameworks such as Jetpack Compose and SwiftUI).
40 |
41 | Each logical component area is separated into a Workflow with a finite set of states and the logic to transition between them. Workflows can be composed together for a full feature with each Workflow’s signature specifying a clear contract. The Workflow Runtime’s event loop handles the production of new immutable states for each Workflow so that within the Workflow render logic it is immutable. Workflows can be executed and instrumented in a testable way with extra hooks for simple verification of outcomes in unit tests.
42 |
43 | On an even simpler level Workflow improves clarity by giving a large team of developers a shared idiom of software components with which to discuss business logic across feature areas, and across mobile platforms (Android, iOS). Further, as the application is composed with multiple Workflows, the framework enables loose coupling between features to focus the impact of code changes.
44 |
45 | ## Encouraging programming paradigms that are best practices for the mobile domain
46 |
47 | Mobile applications receive and display a lot of data! Our applications at Square certainly do. As a result of this, there is a growing trend towards **reactive programming** for mobile applications. In this paradigm, the application logic subscribes to a stream of data which is then pushed to the logic rather than having to be periodically pulled and operated on. This has the profound effect of ensuring that the data shown to the application user is never stale. This style of programming also makes clear that most mobile applications are a series of mapping operations on a stream of data that is eventually mapped into some UI.
48 |
49 | Another mobile programming best practice (arising out of a long tradition) is to favor **declarative programming** over **imperative programming**. With this style choice, the code for a feature declares what should be occurring for a particular state, rather than consisting of a series of statements that are essentially _how_ to make that occur. This is a best practice because when a program’s logic is defined in this way, it is very simple to test (so more likely to be tested!): “For state Y we expect Rendering Z;” “From state Y given input A we expect Rendering Z+.” Possibly more important, it is easier to read, comprehend quickly, and to reason about than a series of complex commands for the computer.
50 |
51 | Workflows encourage a declarative style because each state of a particular component must be enumerated and then the Rendering (representation that gets passed to the UI framework) is _declared_ for that particular state, alongside a declaration of what children and side effects _should be_ running in that state. The well-tested and reliable Workflow runtime loop itself handles _how_ to start and stop the children and side effects, reducing resource leaks. By requiring these formal definitions of each State, Rendering, and the Actions that will change the current state, Workflow naturally encourages declarative programming.
52 |
53 | While reactive and declarative programming may be current best practices, there is one Software Engineering principle that has proven over and over again to be the most universal and the most important for systems of scale: **Separation of Concerns**. Any system of scale requires multiple separate components that can be worked on, tested, improved, and refactored independently by multiple teams of people. A system of multiple components requires communication and any good communication begs explicit structure and contracts.
54 |
55 | ---
56 |
57 | For mobile applications at Square we have settled on the Model-View-ViewModel (MVVM) architecture as the structure for the topical separation of concerns of the layers of the application. MVVM’s unidirectional layered communication is the same as that of Model-View-Presenter (MVP), as opposed to the ‘circular’ communication of Model-View-Controller (MVC). MVVM’s use of a strict binding between the ViewModel and the View is the same as MVC, as opposed to the imperative interpretation of the Model in MVP. MVVM provides the reasoning and comprehension benefits of unidirectional data flow while also eliminating as much business logic as possible from the view layer and encouraging declarative ViewModels. At Square this works well because we have UI design frameworks that change infrequently (so keeping bindings up-to-date is not much overhead), but business logic that is constantly being updated (so emphasizing low coupling is important).
58 |
59 | Workflows embrace MVVM because the Rendering produced by a Workflow tree is the ViewModel, which can then be bound to any native mobile UI framework.
60 |
61 | For feature based separation of concerns we lean on Workflow’s facility for composition at scale via strong parent-child contracts and a hierarchical tree organization.
62 |
63 | ---
64 |
65 | While building a Hello World Workflow may seem like overkill (although [it's](https://github.com/square/workflow-kotlin/blob/main/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloWorkflow.kt) really not that bad!), the explicitness and contracts that Workflows require of the developer lay the structure for good communication. The composability of Workflows encourages **reuse** and encourages separation of concerns into the most appropriate reusable components.
66 |
67 | There are even more platform-specific best practices that Workflow dovetails well with, such as structured concurrency with Kotlin coroutines, as each Worker or side effect can define a specific coroutine scope for the operations.
68 |
69 | What about the next 10 years? Jetpack Compose UI and SwiftUI are establishing themselves as the native mobile UI toolkits of the future. They both embrace the same MVVM approach that Workflow does, and encourage thinking about the “composability” of _separate_ components of your application. With this resonance, Workflows help you to prepare your mental model to adapt to these new UI toolkits, and shapes our codebase in a way that will ease our adoption of them. To learn more about Compose and Workflow see [this post](https://developer.squareup.com/blog/jetpack-compose-support-in-workflow).
--------------------------------------------------------------------------------
/docs/userguide/worker-in-code.md:
--------------------------------------------------------------------------------
1 | # Coding a Worker
2 |
3 | `Worker` is a protocol (in Swift) and interface (in Kotlin) that defines an asynchronous task that
4 | can be performed by a `Workflow`. `Worker`s only emit outputs, they do not have a `Rendering` type.
5 | They are similar to child workflows with `Void`/`Unit` rendering types.
6 |
7 | A workflow can ask the infrastructure to await the result of a worker by passing that worker to the
8 | `RenderContext.runningWorker` method within a call to the `render` method. A workflow can handle
9 | outputs from a `Worker`.
10 |
11 | ## Workers provide a declarative window into the imperative world
12 |
13 | As nice as it is to write declarative code, real apps need to interact with imperative APIs. Workers
14 | allow wrapping imperative APIs so that Workflows can interact with them in a declarative fashion.
15 | Instead of making imperative "start this, do that, now stop" calls, a Workflow can say "I declare
16 | that this task should now be running" and let the infrastructure worry about ensuring the task is
17 | actually started when necessary, continues running if it was already in flight, and torn down when
18 | it's not needed anymore.
19 |
20 | ## Workers can perform side effects
21 |
22 | Unlike workflows' `render` method, which can be called many times and must be idempotent, workers
23 | are started and then ran until completion (or cancellation) – independently of how many times the
24 | workflow running them is actually rendered. This means that side effects that should be performed
25 | only once when a workflow enters a particular state, for example, should be placed into a `Worker`
26 | that the workflow runs while in that state.
27 |
28 | ## Workers are cold reactive streams
29 |
30 | Workers are effectively simple wrappers around asynchronous streams with explicit equivalence. In
31 | Swift, workers are backed by ReactiveSwift [`SignalProducer`s](http://reactivecocoa.io/reactiveswift/docs/latest/SignalProducer.html#/s:13ReactiveSwift14SignalProducerV).
32 | In Kotlin, they're backed by Kotlin [`Flow`s](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/).
33 | They are also easily derived from [Reactive Streams Publishers](https://www.reactive-streams.org),
34 | including RxJava `Observable`, `Flowable`, or `Single` instances.
35 |
36 | ## Worker subscriptions are managed automatically
37 |
38 | While Workers are _backed_ by reactive streams with library-specific subscription APIs, you never
39 | actually subscribe directly to a worker yourself. Instead, a Workflow asks the infrastructure to
40 | run a worker, and the infrastructure will take care of initializing and tearing down the
41 | subscription as appropriate – much like how child workflows' lifetimes are automatically managed by
42 | the runtime. This makes it impossible to accidentally leak a subscription to a worker.
43 |
44 | ## Workers manage their _own_ internal state
45 |
46 | Unlike Workflows, which are effectively collections of functions defining state transitions, Workers
47 | represent long-running tasks. For example, Workers commonly execute network requests. The worker's
48 | stream will open a socket and, either blocking on a background thread or asynchronously, read from
49 | that socket and eventually emit data to the workflow that is running it.
50 |
51 | ## Workers define their own equivalence
52 |
53 | Since Workers represent ongoing tasks, the infrastructure needs to be able to tell when two workers
54 | represent the same task (so it doesn't perform the task twice), or when a worker has changed between
55 | render passes such that it needs to be torn down and re-started for the new work.
56 |
57 | For these reasons, any time a workflow requests that a worker be run in sequential render passes, it
58 | is asked to compare itself with its last instance and determine if they are equivalent. In Swift,
59 | this is determined by the `Worker` `isEquivalent:to:` method. `Worker`s that conform to `Equatable`
60 | will automatically get an `isEquivalent:to:` method based on the `Equatable` implementation. In
61 | Kotlin, the `Worker` interface defines the `doesSameWorkAs` method which is passed the previous worker.
62 |
63 | !!! faq "Kotlin: Why don't Workers use `equals`?"
64 | Worker equivalence is a key part of the Worker API. The default implementation of `equals`,
65 | which just compares object identity, is almost always incorrect for workers. Defining a separate
66 | method forces implementers to think about how equivalence is defined.
67 |
68 | ## Workers are lifecycle-aware
69 |
70 | Workers are aware of when they're started (just like Workflows), but they are also aware of when
71 | they are torn down. This makes them handy for managing resources as well.
72 |
--------------------------------------------------------------------------------
/docs/userguide/workflow-in-code.md:
--------------------------------------------------------------------------------
1 | # Coding a Workflow
2 |
3 | In code, `Workflow` is a Swift protocol or Kotlin interface with State, Rendering and Output parameter types.
4 | The Kotlin interface also defines a Props type.
5 | In Swift, props are implicit as properties of the struct implementing Workflow.
6 |
7 | === "Swift"
8 | ```Swift
9 | public protocol Workflow: AnyWorkflowConvertible {
10 |
11 | associatedtype State
12 |
13 | associatedtype Output = Never
14 |
15 | associatedtype Rendering
16 |
17 | func makeInitialState() -> State
18 |
19 | func workflowDidChange(from previousWorkflow: Self, state: inout State)
20 |
21 | func render(state: State, context: RenderContext) -> Rendering
22 |
23 | }
24 |
25 | ```
26 |
27 | === "Kotlin"
28 | ```Kotlin
29 | abstract class StatefulWorkflow :
30 | Workflow {
31 |
32 | abstract fun initialState(
33 | props: PropsT,
34 | initialSnapshot: Snapshot?
35 | ): StateT
36 |
37 | open fun onPropsChanged(
38 | old: PropsT,
39 | new: PropsT,
40 | state: StateT
41 | ): StateT = state
42 |
43 | abstract fun render(
44 | props: PropsT,
45 | state: StateT,
46 | context: RenderContext
47 | ): RenderingT
48 |
49 | abstract fun snapshotState(state: StateT): Snapshot
50 | }
51 | ```
52 |
53 | ??? faq "Swift: What is `AnyWorkflowConvertible`?"
54 | When a protocol has an associated `Self` type, Swift requires the use of a [type-erasing wrapper](https://medium.com/swiftworld/swift-world-type-erasure-5b720bc0318a)
55 | to store references to instances of that protocol.
56 | [`AnyWorkflow`](/workflow/swift/api/Workflow/Structs/AnyWorkflow.html) is such a wrapper for
57 | `Workflow`. [`AnyWorkflowConvertible`](/workflow/swift/api/Workflow/Protocols/AnyWorkflowConvertible.html)
58 | is a protocol with a single method that returns an `AnyWorkflow`. It is useful as a base type
59 | because it allows instances of `Workflow` to be used directly by any code that requires the
60 | type-erased `AnyWorkflow`.
61 |
62 | ??? faq "Kotlin: `StatefulWorkflow` vs `Workflow`"
63 | It is a common practice in Kotlin to divide types into two parts: an interface for public API,
64 | and a class for private implementation. The Workflow library defines a [`Workflow`](/workflow/kotlin/api/htmlMultiModule/workflow-core/com.squareup.workflow1/-workflow/index.html)
65 | interface, which should be used as the type of properties and parameters by code that needs to
66 | refer to a particular `Workflow` interface. The `Workflow` interface contains a single method,
67 | which simply returns a `StatefulWorkflow` – a `Workflow` can be described as “anything that can
68 | be expressed as a `StatefulWorkflow`.”
69 |
70 | The library also defines two abstract classes which define the contract for workflows and should
71 | be subclassed to implement your workflows:
72 |
73 | - [**`StatefulWorkflow`**](/workflow/kotlin/api/htmlMultiModule/workflow-core/com.squareup.workflow1/-stateful-workflow/index.html)
74 | should be subclassed to implement Workflows that have [private state](#private-state).
75 | - [**`StatelessWorkflow`**](/workflow/kotlin/api/htmlMultiModule/workflow-core/com.squareup.workflow1/-stateless-workflow/index.html)
76 | should be subclassed to implement Workflows that _don't_ have any private state. See [Stateless Workflows](#stateless-workflows).
77 |
78 | Workflows have several responsibilities:
79 |
80 | ## Workflows have state
81 |
82 | Once a Workflow has been started, it always operates in the context of some state. This state is
83 | divided into two parts: private state, which only the Workflow implementation itself knows about,
84 | which is defined by the `State` type, and properties (or "props"), which is passed to the Workflow
85 | from its parent (more on hierarchical workflows below).
86 |
87 | ### Private state
88 |
89 | Every Workflow implementation defines a `State` type to maintain any necessary state while the
90 | workflow is running.
91 |
92 | For example, a tic-tac-toe game might have a state like this:
93 |
94 | === "Swift"
95 | ```Swift
96 | struct State {
97 |
98 | enum Player {
99 | case x
100 | case o
101 | }
102 |
103 | enum Space {
104 | case unfilled
105 | filled(Player)
106 | }
107 |
108 | // 3 rows * 3 columns = 9 spaces
109 | var spaces: [Space] = Array(repeating: .unfilled, count: 9)
110 | var currentTurn: Player = .x
111 | }
112 | ```
113 |
114 | === "Kotlin"
115 | ```Kotlin
116 | data class State(
117 | // 3 rows * 3 columns = 9 spaces
118 | val spaces: List = List(9) { Unfilled },
119 | val currentTurn: Player = X
120 | ) {
121 |
122 | enum class Player {
123 | X, O
124 | }
125 |
126 | sealed class Space {
127 | object Unfilled : Space()
128 | data class Filled(val player: Player) : Space()
129 | }
130 | }
131 | ```
132 |
133 | When the workflow is first started, it is queried for an initial state value. From that point
134 | forward, the workflow may advance to a new state as the result of events occurring from various
135 | sources (which will be covered below).
136 |
137 | !!! info "Stateless Workflows"
138 | If a workflow does not have any private state, it is often referred to as a
139 | "stateless workflow". A stateless Workflow is simply a Workflow that has a `Void` or `Unit`
140 | `State` type. See [more](/workflow/kotlin/api/workflow/com.squareup.workflow1/-workflow/#stateless-workflows).
141 |
142 | ### Public Props
143 |
144 | Every Workflow implementation also defines data that is passed into it. The Workflow is not able to
145 | modify this state itself, but it may change between render passes. This public state is called
146 | `Props`.
147 |
148 | In Swift, the props are simply defined as properties of the struct implementing Workflow itself. In
149 | Kotlin, the `Workflow` interface defines a separate `PropsT` type parameter. (This additional type
150 | parameter is necessary due to Kotlin’s lack of the `Self` type that Swift workflow’s
151 | `workflowDidChange` method relies upon.)
152 |
153 | === "Swift"
154 | ```Swift
155 | TK
156 | ```
157 |
158 | === "Kotlin"
159 | ```Kotlin
160 | data class Props(
161 | val playerXName: String
162 | val playerOName: String
163 | )
164 | ```
165 |
166 | ## Workflows are advanced by `WorkflowAction`s
167 |
168 | Any time something happens that should advance a workflow – a UI event, a network response, a
169 | child's output event – actions are used to perform the update. For example, a workflow may respond
170 | to UI events by mapping those events into a type conforming to/implementing `WorkflowAction`. These
171 | types implement the logic to advance a workflow by:
172 |
173 | - Advancing to a new state
174 | - (Optionally) emitting an output event up the tree.
175 |
176 | `WorkflowAction`s are typically defined as enums with associated types (Swift) or sealed classes
177 | (Kotlin), and can include data from the event – for example, the ID of the item in the list that was
178 | clicked.
179 |
180 | Side effects such as logging button clicks to an analytics framework are also typically performed in
181 | actions.
182 |
183 | If you're familiar with React/Redux, `WorkflowAction`s are essentially reducers.
184 |
185 | ## Workflows can emit output events up the hierarchy to their parent
186 |
187 | When a workflow is advanced by an action, an optional output event can be sent up the workflow
188 | hierarchy. This is the opportunity for a workflow to notify its parent that something has happened
189 | (and the parent's opportunity to respond to that event by dispatching its own action, continuing up
190 | the tree as long as output events are emitted).
191 |
192 | ## Workflows produce an external representation of their state via `Rendering`
193 |
194 | Immediately after starting up, or after a state transition occurs, a workflow will have its `render`
195 | method called. This method is responsible for creating and returning a value of type `Rendering`.
196 | You can think of `Rendering` as the "external published state" of the workflow, and the `render`
197 | function as a map of (`Props` + `State` + childrens' `Rendering`s) -> `Rendering`. While a
198 | workflow's internal state may contain more detailed or comprehensive state, the `Rendering`
199 | (external state) is a type that is useful outside of the workflow. Because a workflow’s render
200 | method may be called by infrastructure for a variety of reasons, it’s important to not perform side
201 | effects when rendering — render methods must be idempotent. Event-based side effects should use
202 | Actions and state-based side effects should use Workers.
203 |
204 | When building an interactive application, the `Rendering` type is commonly (but not always) a view
205 | model that will drive the UI layer.
206 |
207 | ## Workflows can respond to UI events
208 |
209 | The `RenderContext` that is passed into `render` as the last parameter provides some useful tools to
210 | assist in creating the `Rendering` value.
211 |
212 | If a workflow is producing a view model, it is common to need an event handler to respond to UI
213 | events. The `RenderContext` has API to create an event handler, called a `Sink`, that when called
214 | will advance the workflow by dispatching an action back to the workflow (for more on actions, see
215 | [above](#workflows-are-advanced-by-workflowactions)).
216 |
217 | === "Swift"
218 | ```Swift
219 | func render(state: State, context: RenderContext) -> DemoScreen {
220 | // Create a sink of our Action type so we can send actions back to the workflow.
221 | let sink = context.makeSink(of: Action.self)
222 |
223 | return DemoScreen(
224 | title: "A nice title",
225 | onTap: { sink.send(Action.refreshButtonTapped) }
226 | }
227 | ```
228 |
229 | === "Kotlin"
230 | ```Kotlin
231 | TK
232 | ```
233 |
234 | ## Workflows form a hierarchy (they may have children)
235 |
236 | As they produce a `Rendering` value, it is common for workflows to delegate some portion of that
237 | work to a _child workflow_. This is done via the `RenderContext` that is passed into the `render`
238 | method. In order to delegate to a child, the parent calls `renderChild` on the context, with the
239 | child workflow as the single argument. The infrastructure will spin up the child workflow (including
240 | initializing its initial state) if this is the first time this child has been used, or, if the child
241 | was also used on the previous `render` pass, the existing child will be updated. Either way,
242 | `render` will immediately be called on the child (by the Workflow infrastructure), and the resulting
243 | child's `Rendering` value will be returned to the parent.
244 |
245 | This allows a parent to return complex `Rendering` types (such as a view model representing the
246 | entire UI state of an application) without needing to model all of that complexity within a single
247 | workflow.
248 |
249 | !!! info "Workflow Identity"
250 | The Workflow infrastructure automatically detects the first time and the last subsequent time
251 | you've asked to render a child workflow, and will automatically initialize the child and clean
252 | it up. In both Swift and Kotlin, this is done using the workflow's concrete type. Both languages
253 | use reflection to do this comparison (e.g. in Kotlin, the workflows' `KClass`es are compared).
254 |
255 | It is an error to render workflows of the same type more than once in the same render pass.
256 | Since type is used for workflow identity, the child rendering APIs take an optional string key
257 | to differentiate between multiple child workflows of the same type.
258 |
259 | ## Workflows can subscribe to external event sources
260 |
261 | If a workflow needs to respond to some external event source (e.g. push notifications), the workflow
262 | can ask the context to listen to those events from within the `render` method.
263 |
264 | !!! info "Swift vs Kotlin"
265 | In the Swift library, there is a special API for subscribing to hot streams (`Signal` in
266 | ReactiveSwift). The Kotlin library does not have any special API for subscribing to hot streams
267 | (channels), though it does have extension methods to convert [`ReceiveChannel`s](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/),
268 | and RxJava `Flowable`s and `Observables`, to [`Worker`s](/workflow/userguide/worker-in-code/). The reason for this
269 | discrepancy is simply that we don't have any uses of channels yet in production, and so we've
270 | decided to keep the API simpler. If we start using channels in the future, it may make sense to
271 | make subscribing to them a first-class API like in Swift.
272 |
273 | ## Workflows can perform asynchronous tasks (Workers)
274 |
275 | `Workers` are very similar in concept to child workflows. Unlike child workflows, however, workers
276 | do not have a `Rendering` type; they only exist to perform a single asynchronous task before sending
277 | zero or more output events back up the tree to their parent.
278 |
279 | For more information about workers, see the [Worker](/workflow/userguide/worker-in-code/) section below.
280 |
281 | ## Workflows can be saved to and restored from a snapshot (Kotlin only)
282 |
283 | On every render pass, each workflow is asked to create a "snapshot" of its state – a lazily-produced
284 | serialization of the workflow's `State` as a binary blob. These `Snapshot`s are aggregated into a
285 | single `Snapshot` for the entire workflow tree and emitted along with the root workflow's
286 | `Rendering`. When the workflow runtime is started, it can be passed an optional `Snapshot` to
287 | restore the tree from. When non-null, the root workflow's snapshot is extracted and passed to the
288 | root workflow's `initialState`. The workflow can choose to either ignore the snapshot or use it to
289 | restore its `State`. On the first render pass, if the root workflow renders any children that were
290 | also being rendered when the snapshot was taken, those children's snapshots are also extracted from
291 | the aggregate and used to initialize their states.
292 |
293 | !!! faq "Why don't Swift Workflows support snapshotting?"
294 | Snapshotting was built into Kotlin workflows specifically to support Android's app lifecycle,
295 | which requires apps to serialize their current state before being backgrounded so that they can
296 | be restored in case the system needs to kill the hosting process. iOS apps don't have this
297 | requirement, so the Swift library doesn't need to support it.
298 |
--------------------------------------------------------------------------------
/lint_docs.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Copyright 2019 Square Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | # This script uses markdownlint.
19 | # https://github.com/markdownlint/markdownlint
20 | # To install, run:
21 | # gem install mdl
22 |
23 | set -e
24 |
25 | STYLE=.markdownlint.rb
26 | DIR=docs/
27 |
28 | # CHANGELOG is an mkdocs redirect pointer, not valid markdown.
29 | find $DIR \
30 | -name '*.md' \
31 | -not -name 'CHANGELOG.md' \
32 | -not -name 'whyworkflow.md' \
33 | | xargs mdl --style $STYLE --ignore-front-matter \
34 | && echo "Success."
35 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2019 Square Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | #
16 |
17 | site_name: Square Workflow
18 | repo_name: Workflow
19 | repo_url: https://github.com/square/workflow
20 | site_description: "A library for making composable state machines, and UIs driven by those state machines."
21 | site_author: Square, Inc.
22 | site_url: https://square.github.io/workflow/
23 | remote_branch: gh-pages
24 | edit_uri: edit/main/docs/
25 |
26 | copyright: 'Copyright © 2019 Square, Inc.'
27 |
28 | theme:
29 | name: 'material'
30 | logo: images/icon-square.png
31 | favicon: images/icon-square.png
32 | icon:
33 | repo: fontawesome/brands/github
34 | palette:
35 | primary: 'red'
36 | accent: 'pink'
37 | features:
38 | - tabs
39 | - instant
40 |
41 | extra_css:
42 | - 'css/app.css'
43 |
44 | markdown_extensions:
45 | - admonition
46 | - smarty
47 | - codehilite:
48 | guess_lang: false
49 | linenums: True
50 | - footnotes
51 | - meta
52 | - toc:
53 | permalink: true
54 | - pymdownx.betterem:
55 | smart_enable: all
56 | - pymdownx.caret
57 | - pymdownx.details
58 | - pymdownx.inlinehilite
59 | - pymdownx.magiclink
60 | - pymdownx.smartsymbols
61 | - pymdownx.superfences
62 | - pymdownx.tabbed
63 | - tables
64 |
65 | plugins:
66 | - search
67 | - redirects:
68 | redirect_maps:
69 | # Redirect some of the most-visited pages from their old locations in case there are links
70 | # to these pages somewhere.
71 | 'kotlin/api/workflow-core/com.squareup.workflow/index.md': 'kotlin/api/htmlMultiModule/index.html'
72 | 'kotlin/api/workflow-core/com.squareup.workflow/-worker/index.md': 'kotlin/api/htmlMultiModule/workflow-core/com.squareup.workflow1/-worker/index.html'
73 | 'kotlin/api/workflow-testing/com.squareup.workflow.testing/index.md': 'kotlin/api/htmlMultiModule/workflow-testing/com.squareup.workflow1.testing/index.html'
74 | 'kotlin/api/workflow-testing/com.squareup.workflow.testing/-render-tester/index.md': 'kotlin/api/htmlMultiModule/workflow-testing/com.squareup.workflow1.testing/-render-tester/index.html'
75 |
76 | extra:
77 | # type is the name of the FontAwesome icon without the fa- prefix.
78 | social:
79 | - icon: fontawesome/brands/github-alt
80 | link: https://github.com/square
81 | - icon: fontawesome/brands/twitter
82 | link: https://twitter.com/squareeng
83 | - icon: fontawesome/brands/linkedin
84 | link: https://www.linkedin.com/company/joinsquare/
85 |
86 | nav:
87 | - 'Overview': index.md
88 | - 'Why Workflow?': 'userguide/whyworkflow.md'
89 | - 'User Guide':
90 | - 'Workflow Core': 'userguide/concepts.md'
91 | - 'Coding a Workflow (stale)': 'userguide/workflow-in-code.md'
92 | - 'Coding a Worker (stale)': 'userguide/worker-in-code.md'
93 | - 'Workflow UI (in progress)': 'userguide/ui-concepts.md'
94 | - 'Coding Workflow UI (in progress)': 'userguide/ui-in-code.md'
95 | - 'Testing (TBD)': 'userguide/testing-concepts.md'
96 | - 'Common Patterns': 'userguide/common-patterns.md'
97 | - 'Implementation Notes': 'userguide/implementation.md'
98 | - 'Tutorials and Samples':
99 | - 'Swift Tutorial 🔗': https://github.com/square/workflow-swift/tree/main/Samples/Tutorial
100 | - 'Swift Samples 🔗': https://github.com/square/workflow-swift/tree/main/Samples
101 | - 'Kotlin Tutorial 🔗': https://github.com/square/workflow-kotlin/tree/main/samples/tutorial#readme
102 | - 'Kotlin Samples 🔗': https://github.com/square/workflow-kotlin/tree/main/samples
103 | - 'API Reference':
104 | - 'Kotlin 🔗': 'kotlin/api/htmlMultiModule'
105 | - 'Swift 🔗': https://square.github.io/workflow-swift/documentation
106 | - 'Glossary': 'glossary.md'
107 | - 'FAQ': faq.md
108 | - 'Pre-1.0 Resources': historical.md
109 | - 'Changelog': CHANGELOG.md
110 | - 'Contributing': CONTRIBUTING.md
111 | - 'Code of Conduct': CODE_OF_CONDUCT.md
112 |
113 | # Google Analytics. Add export WORKFLOW_GOOGLE_ANALYTICS_KEY="UA-XXXXXXXXX-X" to your ~/.bashrc
114 | google_analytics:
115 | - !!python/object/apply:os.getenv ["WORKFLOW_GOOGLE_ANALYTICS_KEY"]
116 | - auto
117 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | mkdocs==1.3.0
2 | mkdocs-material==8.2.11
3 | mkdocs-redirects==1.0.4
4 |
--------------------------------------------------------------------------------
/wolfcrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/square/workflow/a4a77a3a2dc07a5bf38f1b6489be75f4e15d686d/wolfcrow.png
--------------------------------------------------------------------------------