├── .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 | [![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](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 | ![wolfcrow](wolfcrow.png) 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 +0000Canvas 1Layer 1Native view systemMessageScreenInboxScreenRuntimeWorkflow root containerEmailBrowserWorkflowSplitScreen( InboxScreen, MessageScreen)Workflow containerCustom split viewWorkflow containerCustom inbox viewWorkflow containerCustom 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 +0000Canvas 1Layer 1EmailBrowserWorkflowState { messages: List<MessageId>, selection: MessageId}SplitScreen 4 | -------------------------------------------------------------------------------- /docs/images/email_inbox_workflow_schematic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Produced by OmniGraffle 6.6.2 2022-05-25 18:58:42 +0000Canvas 1Layer 1InboxWorkflowMessageIdList<MessageId>onMessageSelected()InboxScreen 4 | -------------------------------------------------------------------------------- /docs/images/email_message_workflow_schematic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Produced by OmniGraffle 6.6.2 2022-05-25 18:58:42 +0000Canvas 1Layer 1MessageWorkflowMessageIdMessageScreen 4 | -------------------------------------------------------------------------------- /docs/images/email_schematic_renderings_only.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Produced by OmniGraffle 6.6.2 2022-05-27 17:33:43 +0000Canvas 1Layer 1EmailBrowserWorkflowInboxWorkflowInboxScreenMessageWorkflowMessageScreenSplitScreen( InboxScreen, MessageScreen) 4 | -------------------------------------------------------------------------------- /docs/images/game_workflow_schematic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Produced by OmniGraffle 6.6.2 2022-05-25 18:58:42 +0000Canvas 1Layer 1GameWorkflowGameState {}GameOverPlayersonClick()GameScreen 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 +0000Canvas 1Layer 1EmailBrowserWorkflowState { messages: List<MessageId>, selection: MessageId}SplitScreen( InboxScreen, MessageScreen)InboxWorkflowList<MessageId>InboxScreenMessageWorkflowMessageIdMessageScreen 4 | -------------------------------------------------------------------------------- /docs/images/split_screen_update.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Produced by OmniGraffle 6.6.2 2022-05-27 16:16:23 +0000Canvas 1Layer 1onMessageSelected()EmailBrowserWorkflowState { messages: List<MessageId>, selection: MessageId}SplitScreen( InboxScreen, MessageScreen)InboxWorkflowList<MessageId>InboxScreenMessageWorkflowMessageIdMessageScreenMessageId 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 +0000Canvas 1Layer 1StateOutputPropseventsRendering 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 | ![Workflow schematic showing State as a box with Props entering from the top, Output exiting from the top, and Rendering exiting from the left side, with events returning to the workflow via Rendering](../images/workflow_schematic.svg) 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 | ![Workflow schematic with State type GameState, Props type Players, Rendering type GameScreen, and Output type GameOver. The workflow is receiving an onClick() event.](../images/game_workflow_schematic.svg) 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 | ![Workflow schematic showing InboxWorkflow](../images/email_inbox_workflow_schematic.svg) 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 | ![Workflow schematic showing MessageWorkflow](../images/email_message_workflow_schematic.svg) 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 | ![Workflow schematic showing MessageWorkflow](../images/email_browser_workflow_schematic.svg) 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 | ![Workflow schematic showing a parent EmailBrowserWorkflow providing Props to its children, InboxWorkflow and MessageWorkflow, and assembling their renderings into a SplitScreen(InboxScreen, MessageScreen)](../images/split_screen_schematic.svg) 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 | ![Workflow schematic showing EmailBrowserWorkflow rendering by delegating to two children, InboxWorkflow and MessageWorkflow, and assembling their renderings into its own.](../images/split_screen_update.svg) 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 | ![workflow rendering sequence diagram](../images/swift/workflow_rendering.png) 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 | ![nested workflow rendering sequence diagram](../images/swift/nested_workflow_rendering.png) 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 | ![Workflow schematic showing a parent EmailBrowserWorkflow assembling the renderings of its children, InboxWorkflow and MessageWorkflow, into a SplitScreen(InboxScreen, MessageScreen)](../images/email_schematic_renderings_only.svg) 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 | ![A box labeled Runtime contains the EmailBrowserWorkflow. It slightly overlaps a larger box labeled Native view system. A line from the EmailBrowserWorkflow's Rendering port connects to a box at the top of the Native View System, labeled Workflow root container. That Rendering, a SplitScreen(InboxScreen, MessageScreen), is passed from the Workflow root container down to a bi-part box labeled Workflow container / Custom split view. From there, InboxScreen is passed down to a similar bi-part box labeled Workflow container / Custom inbox view, and MessageScreen to Workflow container / Custom message view](../images/down_the_view_tree.svg) 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 --------------------------------------------------------------------------------