Repository: alibaba/fish-redux Branch: master Commit: 40b853bfa084 Files: 314 Total size: 554.9 KB Directory structure: gitextract_al9_2a3r/ ├── .github/ │ └── ISSUE_TEMPLATE/ │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md ├── .gitignore ├── .metadata ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── doc/ │ ├── README-cn.md │ ├── README.md │ ├── concept/ │ │ ├── action-cn.md │ │ ├── action.md │ │ ├── adapter-cn.md │ │ ├── adapter.md │ │ ├── auto-dispose-cn.md │ │ ├── auto-dispose.md │ │ ├── component-cn.md │ │ ├── component.md │ │ ├── connector-cn.md │ │ ├── connector.md │ │ ├── custom-adapter-cn.md │ │ ├── custom-adapter.md │ │ ├── dependencies-cn.md │ │ ├── dependencies.md │ │ ├── dependent-cn.md │ │ ├── dependent.md │ │ ├── directory-cn.md │ │ ├── directory.md │ │ ├── dynamic-flow-adapter-cn.md │ │ ├── dynamic-flow-adapter.md │ │ ├── effect-cn.md │ │ ├── effect.md │ │ ├── evolution-of-fish-redux.md │ │ ├── features.md │ │ ├── filter-cn.md │ │ ├── filter.md │ │ ├── higher-effect-cn.md │ │ ├── higher-effect.md │ │ ├── lifecycle-cn.md │ │ ├── lifecycle.md │ │ ├── mechanism-cn.md │ │ ├── mechanism.md │ │ ├── middleware-cn.md │ │ ├── middleware.md │ │ ├── on-error-cn.md │ │ ├── on-error.md │ │ ├── oop-cn.md │ │ ├── oop.md │ │ ├── page-cn.md │ │ ├── page.md │ │ ├── reducer-cn.md │ │ ├── reducer.md │ │ ├── redux-cn.md │ │ ├── redux.md │ │ ├── should-update-cn.md │ │ ├── should-update.md │ │ ├── static-flow-adapter-cn.md │ │ ├── static-flow-adapter.md │ │ ├── view-cn.md │ │ ├── view.md │ │ ├── what's-adapter.md │ │ ├── what's-connector.md │ │ ├── what's-the-diiference-cn.md │ │ ├── what's-the-diiference.md │ │ ├── widget-wrapper-cn.md │ │ └── widget-wrapper.md │ └── introduction/ │ ├── README-cn.md │ └── README.md ├── docs/ │ ├── .nojekyll │ ├── README.md │ ├── _navbar.md │ ├── _sidebar.md │ ├── concept/ │ │ ├── _navbar.md │ │ ├── _sidebar.md │ │ ├── action.md │ │ ├── adapter.md │ │ ├── auto-dispose.md │ │ ├── component.md │ │ ├── connector.md │ │ ├── custom-adapter.md │ │ ├── dependencies.md │ │ ├── dependent.md │ │ ├── directory.md │ │ ├── dynamic-flow-adapter.md │ │ ├── effect.md │ │ ├── evolution-of-fish-redux.md │ │ ├── features.md │ │ ├── filter.md │ │ ├── higher-effect.md │ │ ├── lifecycle.md │ │ ├── mechanism.md │ │ ├── middleware-cn.md │ │ ├── middleware.md │ │ ├── on-error.md │ │ ├── oop.md │ │ ├── page.md │ │ ├── reducer.md │ │ ├── redux.md │ │ ├── should-update.md │ │ ├── static-flow-adapter.md │ │ ├── view.md │ │ ├── what's-adapter.md │ │ ├── what's-connector.md │ │ ├── what's-the-diiference.md │ │ └── widget-wrapper.md │ ├── index.html │ └── zh-cn/ │ ├── README.md │ ├── _sidebar.md │ └── concept/ │ ├── _navbar.md │ ├── _sidebar.md │ ├── action.md │ ├── adapter.md │ ├── auto-dispose.md │ ├── component.md │ ├── connector.md │ ├── custom-adapter.md │ ├── dependencies.md │ ├── dependent.md │ ├── directory.md │ ├── dynamic-flow-adapter.md │ ├── effect.md │ ├── filter.md │ ├── higher-effect.md │ ├── lifecycle.md │ ├── mechanism.md │ ├── middleware.md │ ├── on-error.md │ ├── oop.md │ ├── page.md │ ├── reducer.md │ ├── redux.md │ ├── should-update.md │ ├── static-flow-adapter.md │ ├── view.md │ ├── what's-adapter.md │ ├── what's-connector.md │ ├── what's-the-diiference.md │ └── widget-wrapper.md ├── example/ │ ├── .flutter-plugins-dependencies │ ├── .gitignore │ ├── .metadata │ ├── README.md │ ├── lib/ │ │ ├── app.dart │ │ ├── global_store/ │ │ │ ├── action.dart │ │ │ ├── reducer.dart │ │ │ ├── state.dart │ │ │ └── store.dart │ │ ├── main.dart │ │ ├── todo_edit_page/ │ │ │ ├── action.dart │ │ │ ├── effect.dart │ │ │ ├── page.dart │ │ │ ├── state.dart │ │ │ └── view.dart │ │ └── todo_list_page/ │ │ ├── action.dart │ │ ├── effect.dart │ │ ├── flow_adapter/ │ │ │ ├── adapter.dart │ │ │ ├── connector.dart │ │ │ └── reducer.dart │ │ ├── list_adapter/ │ │ │ ├── action.dart │ │ │ ├── adapter.dart │ │ │ └── reducer.dart │ │ ├── page.dart │ │ ├── reducer.dart │ │ ├── report_component/ │ │ │ ├── component.dart │ │ │ ├── state.dart │ │ │ └── view.dart │ │ ├── state.dart │ │ ├── todo_component/ │ │ │ ├── action.dart │ │ │ ├── component.dart │ │ │ ├── effect.dart │ │ │ ├── reducer.dart │ │ │ ├── state.dart │ │ │ └── view.dart │ │ └── view.dart │ ├── pubspec.yaml │ ├── test/ │ │ └── widget_test.dart │ └── web/ │ ├── index.html │ └── manifest.json ├── fish_redux.iml ├── lib/ │ ├── fish_redux.dart │ └── src/ │ ├── extensions/ │ │ ├── adapter_extensions.dart │ │ ├── component_extensions.dart │ │ ├── connector_extensions.dart │ │ └── extendsions.dart │ ├── redux/ │ │ ├── apply_middleware.dart │ │ ├── basic.dart │ │ ├── combine_reducers.dart │ │ ├── connector.dart │ │ ├── create_store.dart │ │ └── redux.dart │ ├── redux_adapter/ │ │ ├── adapter.dart │ │ ├── dynamic_flow_adapter.dart │ │ ├── flow_adapter.dart │ │ ├── recycle_context.dart │ │ ├── redux_adapter.dart │ │ ├── source_flow_adapter.dart │ │ └── static_flow_adapter.dart │ ├── redux_aop/ │ │ ├── aop.dart │ │ ├── common_aop/ │ │ │ ├── common_aop.dart │ │ │ ├── debounce.dart │ │ │ ├── debug.dart │ │ │ ├── delay.dart │ │ │ ├── log.dart │ │ │ ├── memoize.dart │ │ │ ├── performance.dart │ │ │ ├── throttle.dart │ │ │ └── wait_until.dart │ │ └── redux_aop.dart │ ├── redux_component/ │ │ ├── auto_dispose.dart │ │ ├── basic.dart │ │ ├── batch_store.dart │ │ ├── component.dart │ │ ├── context.dart │ │ ├── dependencies.dart │ │ ├── dependent.dart │ │ ├── dispatch_bus.dart │ │ ├── enhancer.dart │ │ ├── helper.dart │ │ ├── lifecycle.dart │ │ ├── local.dart │ │ ├── logic.dart │ │ ├── page.dart │ │ └── redux_component.dart │ ├── redux_component_mixin/ │ │ ├── keep_alive_mixin.dart │ │ ├── private_reducer_mixin.dart │ │ ├── redux_component_mixin.dart │ │ ├── single_ticker_provider_mixin.dart │ │ ├── ticker_provider_mixin.dart │ │ ├── visible_change_mixin.dart │ │ └── widgets_binding_observer_mixin.dart │ ├── redux_connector/ │ │ ├── connector.dart │ │ ├── generator.dart │ │ ├── helper.dart │ │ ├── map_like.dart │ │ ├── none.dart │ │ ├── op_mixin.dart │ │ ├── redux_connector.dart │ │ └── reselect.dart │ ├── redux_middleware/ │ │ ├── adapter_middleware/ │ │ │ ├── adapter_middleware.dart │ │ │ └── safety_adapter.dart │ │ ├── middleware/ │ │ │ ├── log.dart │ │ │ ├── middleware.dart │ │ │ └── performance.dart │ │ ├── redux_middleware.dart │ │ └── view_middleware/ │ │ ├── safety_view.dart │ │ └── view_middleware.dart │ ├── redux_routes/ │ │ ├── page_routes.dart │ │ └── redux_routes.dart │ └── utils/ │ ├── collections.dart │ ├── debug.dart │ ├── hash.dart │ ├── tuple.dart │ └── utils.dart ├── pubspec.yaml └── test/ ├── lib/ │ ├── all_test.dart │ ├── instrument.dart │ ├── redux/ │ │ ├── redux_test.dart │ │ └── store_test.dart │ ├── redux_adapter/ │ │ ├── adapter_test.dart │ │ ├── dynamic_adapter_test.dart │ │ ├── redux_adapter_test.dart │ │ ├── source_adapter_test.dart │ │ └── static_flow_adapter_test.dart │ ├── redux_aop/ │ │ ├── memoize_test.dart │ │ └── redux_aop_test.dart │ ├── redux_component/ │ │ ├── auto_dispose_test.dart │ │ ├── component_test.dart │ │ ├── lifecycle_test.dart │ │ ├── page_test.dart │ │ └── redux_component_test.dart │ ├── redux_connector/ │ │ ├── map_like_test.dart │ │ ├── redux_connector_test.dart │ │ └── reselect_test.dart │ ├── redux_middleware/ │ │ └── redux_middleware_test.dart │ ├── redux_routes/ │ │ └── redux_routes_test.dart │ ├── track.dart │ └── utils/ │ ├── collections_test.dart │ └── utils_test.dart ├── pubspec.yaml └── test_widgets/ ├── .gitignore ├── lib/ │ ├── adapter/ │ │ ├── action.dart │ │ ├── adapter.dart │ │ ├── page.dart │ │ └── state.dart │ ├── component/ │ │ ├── action.dart │ │ ├── component.dart │ │ ├── page.dart │ │ └── state.dart │ ├── dynamic_flow_adapter/ │ │ ├── action.dart │ │ ├── adapter.dart │ │ ├── component.dart │ │ ├── dynamic_flow_adapter.dart │ │ ├── page.dart │ │ └── state.dart │ ├── main.dart │ ├── page/ │ │ ├── action.dart │ │ ├── exception.dart │ │ ├── page.dart │ │ └── state.dart │ ├── source_flow_adapter/ │ │ ├── action.dart │ │ ├── adapter.dart │ │ ├── component.dart │ │ ├── page.dart │ │ ├── source_flow_adapter.dart │ │ └── state.dart │ ├── static_flow_adapter/ │ │ ├── action.dart │ │ ├── component.dart │ │ ├── page.dart │ │ ├── state.dart │ │ └── static_flow_adapter.dart │ └── test_base.dart └── pubspec.yaml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. Show the code you wrote as completely as possible. ```dart /// your code here ``` **To Reproduce** Steps to reproduce the behavior. **Expected behavior** A clear and concise description of what you expected to happen. **Additional context** 1. The version of fish-redux which you are using. 2. The information from flutter doctor. ================================================ FILE: .github/ISSUE_TEMPLATE/custom.md ================================================ --- name: Custom issue template about: Describe this issue template's purpose here. title: '' labels: '' assignees: '' --- ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .gitignore ================================================ .DS_Store .atom/ .idea/ .vscode/ .packages .pub/ .dart_tool/ build/ pubspec.lock android/ ios/ .gradle/ build.gradle gradle/ gradlew gradlew.bat local.properties redux.iml pubspec.lock android/local.properties # docs node_modules/ ================================================ FILE: .metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: 5391447fae6209bb21a89e6a5a6583cac1af9b4b channel: stable project_type: package ================================================ FILE: .travis.yml ================================================ os: - linux sudo: false addons: apt: sources: - ubuntu-toolchain-r-test packages: - libstdc++6 # - fonts-droid before_script: - git clone https://github.com/flutter/flutter.git -b stable --depth 1 - ./flutter/bin/flutter doctor script: - ./flutter/bin/flutter test --coverage --coverage-path=lcov.info after_success: - bash <(curl -s https://codecov.io/bash) cache: directories: - $HOME/.pub-cache ================================================ FILE: CHANGELOG.md ================================================ ## [0.3.7] - fix test . - revert issue 613 ## [0.3.6] - fix some dos . ## [0.3.5] - fix deprecated using of api - inheritFromWidgetOfExactType . ## [0.3.4] - fix PureViewComponent's dispatch bug ## [0.3.3] - fix Page name duplication - easy to use reselect - add keep-alive-client wrapper - fix spell error on pure-view-widget - add default widget for component.buildComponent api - add docsify to view docs - add unit test for collection utils and maplike connector ## [0.3.2] - fix Page name duplication ## [0.3.1] - rename AdapterSource’s api - add PureViewComponent - deprecate broadcastEffect - ## [0.3.0] - add SourceFlowAdapter - Use SourceFlowAdapter instead of DynamicFlowAdapter in example - deprecate DynamicFlowAdapter ## [0.2.8] - fix item-bean clone bug #493 ## [0.2.7] - add StateKey #461 - reselect optimization #482 ## [0.2.6] - add TickerProviderMixin - let dispatch return whatever result in effect. #462 - fix Reselect's _listEquals bug #457 - fix SingleTickerProviderMixin & TickerProviderMixin’s dispose bug #461 - add ClearOnDependenciesChanged ## [0.2.5] - add ctx.listen api - rename LocalState to LocalProps - correct some comments ## [0.2.4] - fix Context.broadcast bug #375 - fix PrivateReducerMixin bug #380 - add LocalState ## [0.2.3] - Reconstruct Context - Breaking-change - Reconstruct dependencies - Remove OOP style - Remove higherEffect - Remove deprecated api(Connector, createMixedStore, AutoDispose:follow, AutoDispose:follower) - Remove unused DisposedException - Hide widgets.dart's Action to compate with flutter1.7 - Compate with flutter_web ## [0.2.2] - add congruent conn - fields mainCtx & viewUpdater in ComponentState become public - fix bug when a store recived action after teardown ## [0.2.1] - add forceUpdate api on context - fix bug in adapter’s appear & disappear if items are recycled - fix bug in connectStores api if page has no reducer ## [0.2.0] - force update if driven by outside observable - fix inverter bugs & optimization connectStores api - modify the use of global state in example ## [0.1.9] - add mixed-store's batch notification feature ## [0.1.8] - add api to subscribe app-store for page-store - add api to subscribe app-store for component - add viewMiddleware - add adapterMiddleware - add effectMiddleware - add protected attribute method, more friendly to OOP - remove debug_report ## [0.1.7] - reconstruct mixed-store - reconstruct communication - rename appBroadcast to broadcast - rename pageBroadcast to broadcastEffect - add dispatch-bus - enhance dispatch-api - add some docs - move test to dev_dependencies ## [0.1.6] - fix bug if component has no reducer in app-routes - reconstruct createStore - app-routes's store to be visible ## [0.1.5] - fix bug if notified on building - reconstruct test - add mergeMiddleware\$ ## [0.1.4] - add support for AppStore - add routes - move middleware/aop to the top dir - add PrivateReducerMixin - add reselect - add docs ## [0.1.3] - add support for immutable-state #111 - fix the same type of state component reuse in listview #107 - remove warnings in logMiddleware for debug-actions #98 - correct spelling - modify bindAction #73 ## [0.1.2] - add stfState field in Context #58 - add batchedNotify feature in page-store - add some docs - correct spelling ## [0.1.1] - fix hot-reload bug - add excluedSelf in broadcast - rename sample to example - rename docs to doc ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2019 Alibaba, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================

Fish Redux

[![Build Status](https://travis-ci.org/alibaba/fish-redux.svg?branch=master)](https://travis-ci.org/alibaba/fish-redux) [![pub package](https://img.shields.io/pub/v/fish_redux.svg)](https://pub.dartlang.org/packages/fish_redux) [![codecov](https://codecov.io/gh/alibaba/fish-redux/branch/master/graph/badge.svg)](https://codecov.io/gh/alibaba/fish-redux) ## What is Fish Redux ? Fish Redux is an assembled flutter application framework based on Redux state management. It is suitable for building medium and large applications. It has four characteristics: > 1. Functional Programming > 2. Predictable state container > 3. Pluggable componentization > 4. Non-destructive performance ## Architecture diagram ## Installation [Go](https://pub.dartlang.org/packages/fish_redux#-installing-tab-) ## Documentation Language: [English](doc/README.md) | [中文简体](doc/README-cn.md) ## Examples - [todo list](example) - a simple todo list demo. - run it: ``` cd ./example flutter create . flutter run ``` ## What's the difference between 'Fish Redux' and 'Redux' ? - [answers](doc/concept/what's-the-diiference.md) ## Plugins ### Code Template - [Fish Redux Template For Android Studio](https://github.com/BakerJQ/FishReduxTemplateForAS), by BakerJQ. - [Fish Redux Template For VSCode](https://github.com/huangjianke/fish-redux-template), by huangjianke. ### Dev-Tools - Redux Inspector (using [Flutter Debugger](https://github.com/blankapp/flutter-debugger) and [flipperkit_fish_redux_middleware](https://pub.dartlang.org/packages/flipperkit_fish_redux_middleware)) for Fish Redux apps, by [JianyingLi](https://github.com/lijy91) ## License - Fish Redux is released under the Apache 2.0 license. See [LICENSE](LICENSE) for details. ## 关于我们 阿里巴巴-闲鱼技术是国内最早也是最大规模线上运行Flutter的团队。 我们在公众号中为你精选了Flutter独家干货,全面而深入。 内容包括:Flutter的接入、规模化应用、引擎探秘、工程体系、创新技术等教程和开源信息。 **架构/服务端/客户端/前端/质量工程师 在公众号中投递简历,名额不限哦** 欢迎来闲鱼做一个好奇、幸福、有影响力的程序员,简历投递:tino.wjf@alibaba-inc.com 订阅地址 [For English](https://twitter.com/xianyutech "For English") ================================================ FILE: analysis_options.yaml ================================================ # Specify analysis options. # # Until there are meta linter rules, each desired lint must be explicitly enabled. # See: https://github.com/dart-lang/linter/issues/288 # # For a list of lints, see: http://dart-lang.github.io/linter/lints/ # See the configuration guide for more # https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer # # There are four similar analysis options files in the flutter repos: # - analysis_options.yaml (this file) # - packages/flutter/lib/analysis_options_user.yaml # - https://github.com/flutter/plugins/blob/master/analysis_options.yaml # - https://github.com/flutter/engine/blob/master/analysis_options.yaml # # This file contains the analysis options used by Flutter tools, such as IntelliJ, # Android Studio, and the `flutter analyze` command. # # The flutter/plugins repo contains a copy of this file, which should be kept # in sync with this file. analyzer: language: enableSuperMixins: true strong-mode: implicit-dynamic: false errors: # treat missing required parameters as a warning (not a hint) missing_required_param: warning # treat missing returns as a warning (not a hint) missing_return: warning # allow having TODOs in the code todo: ignore exclude: - 'bin/cache/**' - 'flutter/**' # the following two are relative to the stocks example and the flutter package respectively # see https://github.com/dart-lang/sdk/issues/28463 - 'lib/i18n/stock_messages_*.dart' - 'lib/src/http/**' linter: rules: # these rules are documented on and in the same order as # the Dart Lint rules page to make maintenance easier # https://github.com/dart-lang/linter/blob/master/example/all.yaml - always_declare_return_types - always_put_control_body_on_new_line # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 - always_require_non_null_named_parameters - always_specify_types - annotate_overrides # - avoid_annotating_with_dynamic # conflicts with always_specify_types - avoid_as # - avoid_bool_literals_in_conditional_expressions # not yet tested # - avoid_catches_without_on_clauses # we do this commonly # - avoid_catching_errors # we do this commonly - avoid_classes_with_only_static_members - avoid_empty_else - avoid_function_literals_in_foreach_calls - avoid_init_to_null - avoid_null_checks_in_equality_operators # - avoid_positional_boolean_parameters # not yet tested # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) - avoid_relative_lib_imports - avoid_renaming_method_parameters - avoid_return_types_on_setters # - avoid_returning_null # we do this commonly # - avoid_returning_this # https://github.com/dart-lang/linter/issues/842 # - avoid_setters_without_getters # not yet tested # - avoid_single_cascade_in_expression_statements # not yet tested - avoid_slow_async_io # - avoid_types_as_parameter_names # https://github.com/dart-lang/linter/pull/954/files # - avoid_types_on_closure_parameters # conflicts with always_specify_types # - avoid_unused_constructor_parameters # https://github.com/dart-lang/linter/pull/847 - await_only_futures - camel_case_types - cancel_subscriptions # - cascade_invocations # not yet tested # - close_sinks # https://github.com/flutter/flutter/issues/5789 # - comment_references # blocked on https://github.com/dart-lang/dartdoc/issues/1153 # - constant_identifier_names # https://github.com/dart-lang/linter/issues/204 - control_flow_in_finally - directives_ordering - empty_catches - empty_constructor_bodies - empty_statements - hash_and_equals - implementation_imports # - invariant_booleans # https://github.com/flutter/flutter/issues/5790 - iterable_contains_unrelated_type # - join_return_with_assignment # not yet tested - library_names - library_prefixes - list_remove_unrelated_type # - literal_only_boolean_expressions # https://github.com/flutter/flutter/issues/5791 - no_adjacent_strings_in_list - no_duplicate_case_values - non_constant_identifier_names # - omit_local_variable_types # opposite of always_specify_types # - one_member_abstracts # too many false positives # - only_throw_errors # https://github.com/flutter/flutter/issues/5792 - overridden_fields - package_api_docs - package_names - package_prefixed_library_names # - parameter_assignments # we do this commonly - prefer_adjacent_string_concatenation - prefer_asserts_in_initializer_lists - prefer_bool_in_asserts - prefer_collection_literals - prefer_conditional_assignment - prefer_const_constructors - prefer_const_constructors_in_immutables - prefer_const_declarations - prefer_const_literals_to_create_immutables # - prefer_constructors_over_static_methods # not yet tested - prefer_contains # - prefer_equal_for_default_values # not yet tested # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods - prefer_final_fields - prefer_final_locals - prefer_foreach # - prefer_function_declarations_over_variables # not yet tested - prefer_initializing_formals # - prefer_interpolation_to_compose_strings # not yet tested - prefer_is_empty - prefer_is_not_empty - prefer_single_quotes - prefer_typing_uninitialized_variables - recursive_getters - slash_for_doc_comments # - sort_constructors_first - sort_unnamed_constructors_first - super_goes_last - test_types_in_equals - throw_in_finally # - type_annotate_public_apis # subset of always_specify_types - type_init_formals # - unawaited_futures # https://github.com/flutter/flutter/issues/5793 - unnecessary_brace_in_string_interps - unnecessary_getters_setters # - unnecessary_lambdas # https://github.com/dart-lang/linter/issues/498 - unnecessary_null_aware_assignments - unnecessary_null_in_if_null_operators - unnecessary_overrides # - unnecessary_parenthesis # - unnecessary_statements # not yet tested - unnecessary_this - unnecessary_new - unnecessary_const - unrelated_type_equality_checks - use_rethrow_when_possible # - use_setters_to_change_properties # not yet tested # - use_string_buffers # https://github.com/dart-lang/linter/pull/664 # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review - valid_regexps ================================================ FILE: doc/README-cn.md ================================================ - **简介** - [关于](introduction/README-cn.md) - [演进](concept/evolution-of-fish-redux.md) - [特性](concept/features.md) - **核心概念** - [Redux](concept/redux-cn.md) - [Action](concept/action-cn.md) - [Connector](concept/connector-cn.md) - [Reducer](concept/reducer-cn.md) - [Middleware](concept/middleware-cn.md) - [Component](concept/component-cn.md) - [View](concept/view-cn.md) - [Reducer](concept/reducer-cn.md) - [Effect](concept/effect-cn.md) - [HigherEffect](concept/higher-effect-cn.md) - [Lifecycle](concept/lifecycle-cn.md) - [Dependencies](concept/dependencies-cn.md) - [Dependent](concept/dependent-cn.md) - [ShouldUpdate](concept/should-update-cn.md) - [OnError](concept/on-error-cn.md) - [Filter](concept/filter-cn.md) - [OOP](concept/oop-cn.md) - [WidgetWrapper](concept/widget-wrapper-cn.md) - [Page](concept/page-cn.md) - [Adapter](concept/adapter-cn.md) - [StaticFlowAdapter](concept/static-flow-adapter-cn.md) - [DynamicFlowAdapter](concept/dynamic-flow-adapter-cn.md) - [CustomAdapter](concept/custom-adapter-cn.md) - **其他** - [What's the difference between 'Fish Redux' and 'Redux' ?](concept/what's-the-diiference-cn.md) - [What's-adapter](concept/what's-adapter.md) - [What's-connector](concept/what's-connector.md) - [Mechanism](concept/mechanism-cn.md) - [Directory](concept/directory-cn.md) ================================================ FILE: doc/README.md ================================================ - **Introduction** - [About](introduction/README.md) - [Evolution](concept/evolution-of-fish-redux.md) - [Features](concept/features.md) - **Core Concepts** - [Redux](concept/redux.md) - [Action](concept/action.md) - [Connector](concept/connector.md) - [Reducer](concept/reducer.md) - [Middleware](concept/middleware.md) - [Component](concept/component.md) - [View](concept/view.md) - [Reducer](concept/reducer.md) - [Effect](concept/effect.md) - [HigherEffect](concept/higher-effect.md) - [Lifecycle](concept/lifecycle.md) - [Dependencies](concept/dependencies.md) - [Dependent](concept/dependent.md) - [ShouldUpdate](concept/should-update.md) - [OnError](concept/on-error.md) - [Filter](concept/filter.md) - [OOP](concept/oop.md) - [WidgetWrapper](concept/widget-wrapper.md) - [Page](concept/page.md) - [Adapter](concept/adapter.md) - [StaticFlowAdapter](concept/static-flow-adapter.md) - [DynamicFlowAdapter](concept/dynamic-flow-adapter.md) - [CustomAdapter](concept/custom-adapter.md) - **Others** - [What's the difference between 'Fish Redux' and 'Redux' ?](concept/what's-the-diiference.md) - [What is an adapter?](concept/what's-adapter.md) - [What is a connector?](concept/what's-connector.md) - [Communication mechanism of fish-redux](concept/mechanism.md) - [Recommended directory structure](concept/directory.md) ================================================ FILE: doc/concept/action-cn.md ================================================ # Action - Action 包含两个字段 - type - payload - 推荐的写法是 - 为一个组件|适配器创建一个 action.dart 文件,包含两个类 - 为 type 字段起一个枚举类 - 为 Action 的创建起一个 ActionCreator 类,这样利于约束 payload 的类型。 - Effect 接受处理的 Action,以 on{Verb} 命名 - Reducer 接受处理的 Action,以{verb} 命名 - 示例代码 ```dart enum MessageAction { onShare, shared, } class MessageActionCreator { static Action onShare(Map payload) { return Action(MessageAction.onShare, payload: payload); } static Action shared() { return const Action(MessageAction.shared); } } ``` ================================================ FILE: doc/concept/action.md ================================================ # Action - Action contains two fields - type - payload - Recommended way of writing action - Create an action.dart file for a component|adapter that contains two classes - An enumeration class for the type field - An ActionCreator class is created for the creator of the Action, which helps to constrain the type of payload. - Effect Accepted Action which's type is named after `on{verb}` - Reducer Accepted Action which's type is named after `{verb}` - Sample code ```dart enum MessageAction { onShare, shared, } class MessageActionCreator { static Action onShare(Map payload) { return Action(MessageAction.onShare, payload: payload); } static Action shared() { return const Action(MessageAction.shared); } } ``` ================================================ FILE: doc/concept/adapter-cn.md ================================================ # Adapter - 我们在基础 Component 的概念外,额外增加了一种组件化的抽象 Adapter。它的目标是解决 Component 模型在 ListView 的场景下的 3 个问题 - 1)将一个"Big-Cell"放在 ListView 里,无法享受 ListView 代码的性能优化。 - 2)Component 无法区分 appear|disappear 和 init|dispose 事件。 - 3)Effect 的生命周期和 View 的耦合,在 ListView 的有些场景下不符合直观的预期。 - 一个 Adapter 和 Component 几乎都是一致的,除了以下几点 - Component 生成一个 Widget,Adapter 生成一个 ListAdapter,ListAdapter 有能力生成一组 Widget。 - 不具体生成 Widget,而是一个 ListAdapter,能非常大的提升页面帧率和流畅度。 - Effect-Lifecycle-Promote - Component 的 Effect 是跟着 Widget 的生命周期走的,Adapter 的 Effect 是跟着上一级的 Widget 的生命周期走。 - Effect​ 提升,极大的解除了业务逻辑和视图生命的耦合,即使它的展示还未出现,的其他模块依然能通过 dispatch-api,调用它的能力。 - appear|disappear 的通知 - 由于 Effect 生命周期的提升,我们就能更加精细的区分 init|dispose 和 appear|disappear。而这在 Component 的模型中是无法区分的。 - Reducer is long-lived, Effect is medium-lived, View is short-lived. - Adapter 的三种实现 - [DynamicFlowAdapter](dynamic-flow-adapter-cn.md) - [StaticFlowAdapter](static-flow-adapter-cn.md) - [CustomAdapter](custom-adapter-cn.md) ================================================ FILE: doc/concept/adapter.md ================================================ # Adapter - In addition to the concept of the underlying Component, we have added a componentized abstract Adapter. Its goal is to solve the 3 problems of the Component model in the ListView scene. - 1)Putting a "Big-Cell" in the ListView does not enjoy the performance optimization of the ListView code. - 2)Component cannot distinguish between the appear|disappear and init|dispose events. - 3)The life cycle of the Effect and the coupling of the View do not meet the intuitive expectations in some scenes of the ListView. - An Adapter and a Component are almost identical except for the following points - Component generates a Widget, Adapter generates a ListAdapter, and ListAdapter has the ability to generate a list of Widgets.。 - Not specifically generating a Widget but a ListAdapter can greatly improve the page frame rate and fluency. - Effect-Lifecycle-Promote - The Effect of Component follows the life cycle of the Widget, and the Adapter's Effect follows the life cycle of the parent Widget. - The improvement of the life cycle of the effect greatly removes the coupling between the business logic and the view life. Even if its display has not yet appeared, other modules can still call its capabilities through dispatch-api. - Appearance|disappear event notification - As the Effect lifecycle improves, we can more closely distinguish between init|dispose and appear|disappear. This is indistinguishable from the Model's model. - Reducer is long-lived, Effect is medium-lived, View is short-lived. - Three implementations of Adapter - [DynamicFlowAdapter](dynamic-flow-adapter.md) - [StaticFlowAdapter](static-flow-adapter.md) - [CustomAdapter](custom-adapter.md) ================================================ FILE: doc/concept/auto-dispose-cn.md ================================================ # Auto-Dispose - 它是一个非常简易管理生命周期对象的方式。一个 auto-dispose 对象可以自我主动释放,或者在它 follow 的 托管对象释放的时候,释放。 - 在 Effect 中使用的 Context,以及 HigherEffect 中的 EffectPart,都是 auto-dispose 对象。所以我们可以方便的将自定义的需要做生命周期管理的对象托管给它们。 - 示例代码 ```dart class ItemWidgetBindingObserver extends WidgetsBindingObserver with AutoDispose { ItemWidgetBindingObserver() : super() { WidgetsBinding.instance.addObserver(this); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (AppConfig.flutterBinding.framesEnabled && state == AppLifecycleState.resumed) { AppConfig.flutterBinding.performReassemble(); } } @override void dispose() { super.dispose(); WidgetsBinding.instance.removeObserver(this); } } void _init(Action action, Context ctx) { final ItemWidgetBindingObserver observer = ItemWidgetBindingObserver(); observer.follow(ctx); } ``` ================================================ FILE: doc/concept/auto-dispose.md ================================================ # Auto-Dispose - AutoDispose is a very simple way to manage lifecycle objects. An auto-dispose object can be released on its own initiative or released when the managed object it follows is released. - The Context used in Effect and the EffectPart in HigherEffect are auto-dispose objects. So we can easily host custom objects that need to be managed for lifecycle management. - Sample Code ```dart class ItemWidgetBindingObserver extends WidgetsBindingObserver with AutoDispose { ItemWidgetBindingObserver() : super() { WidgetsBinding.instance.addObserver(this); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (AppConfig.flutterBinding.framesEnabled && state == AppLifecycleState.resumed) { AppConfig.flutterBinding.performReassemble(); } } @override void dispose() { super.dispose(); WidgetsBinding.instance.removeObserver(this); } } void _init(Action action, Context ctx) { final ItemWidgetBindingObserver observer = ItemWidgetBindingObserver(); observer.follow(ctx); } ``` ================================================ FILE: doc/concept/component-cn.md ================================================ # Component 组件是对视图展现和逻辑功能的封装。 面向当下,从 Redux 的视角看,我们对组件分为状态修改的功能(Reducer)和其他。 面向未来,从 UI-Automation 的视角看,我们对组件分为展现表达和其他。 结合上面两个视角,于是我们得到了,View、 Effect、Reducer 三部分,称之为组件的三要素,分别负责了组件的展示、非修改数据的行为、修改数据的操作。 我们以显式配置的方式来完成大组件所依赖的小组件、适配器的注册,这份依赖配置称之为 Dependencies。 所以有了这个公式 Component = View + Effect(可选) + Reducer(可选) + Dependencies(可选) 分治:从组件的角度 集中:从 Store 的角度 ================================================ FILE: doc/concept/component.md ================================================ # Component Component is the encapsulation of view presentation and logic functions. For the moment, from the perspective of Redux, we divide the component into state-manage functions (Reducers) and others. Looking to the future, from the perspective of UI-Automation, we divide the component into presentations and others. Combining the above two perspectives, we got the three parts of View, SideEffect, and Reducer, which are called the three factors of the component. We use explicit configuration to complete the registration of components and adapters on which large component depend. This dependency configuration is called Dependencies. So with this formula: Component = View + Effect(Optional) + Reducer(Optional) + Dependencies(Optional) Division: From the perspective of the component Concentration: From the perspective of the Store ================================================ FILE: doc/concept/connector-cn.md ================================================ # Connector - 它表达了如何从一个大数据中读取小数据,同时对小数据的修改如何同步给大数据,这样的数据连接关系。 - 它是将一个集中式的 Reducer,可以由多层次多模块的小 Reducer 自动拼装的关键。 - 它大大降低了我们使用 Redux 的复杂度。我们不再关心组装过程,我们关心的核心是什么动作促使数据怎么变化。 - 它使用在配置 Dependencies 中,在配置中我们就固化了大组件和小组件之间的连接关系(数据管道),所以在我们使用小组件的时候是不需要传入任何动态参数的。 - ![image.png | left | 719x375](https://cdn.nlark.com/lark/0/2018/png/82574/1545365202743-01074be7-f067-45c7-aae0-91b12cd50ae6.png) - Sample Code ```dart class DetialState { Profile profile; String message; } ConnOp messageConnector() { return ConnOp( get: (DetialState state) => state.message, set: (DetialState state, String message) => state.message = message, ); } ``` ================================================ FILE: doc/concept/connector.md ================================================ # Connector - It expresses a data connection relationship of how to read small data from a big data, and how to synchronize to big data when the small data is modified。 - It is the key to a centralized Reducer that can be assembled automatically by a multi-level, multi-module, small Reducer - It greatly reduces the difficulty of using Redux. We no longer care about the assembly process, we care about what specific actions cause the state to change. - It is used in the configuration Dependencies, in the configuration we have solidified the connection between the large component and the small component, so we do not need to pass in any dynamic parameters when we use the small component. - ![image.png | left | 719x375](https://cdn.nlark.com/lark/0/2018/png/82574/1545365202743-01074be7-f067-45c7-aae0-91b12cd50ae6.png) - Sample Code ```dart class DetialState { Profile profile; String message; } ConnOp messageConnector() { return ConnOp( get: (DetialState state) => state.message, set: (DetialState state, String message) => state.message = message, ); } ``` ================================================ FILE: doc/concept/custom-adapter-cn.md ================================================ # CustomAdapter - 对大 Cell 的自定义实现 - 要素和 Component 类似,不一样的地方是 Adapter 的视图部分返回的是一个 ListAdapter - 示例代码 ```dart class CommentAdapter extends Adapter { CommentAdapter() : super( adapter: buildCommentAdapter, effect: buildCommentEffect(), reducer: buildCommentReducer(), ); } ListAdapter buildCommentAdapter(CommentState state, Dispatch dispatch, ViewService service) { final List builders = Collections.compact([] ..add((BuildContext buildContext, int index) => _buildDetailCommentHeader(state, dispatch, service)) ..addAll(_buildCommentViewList(state, dispatch, service)) ..add(isEmpty(state.commentListRes?.items) ? (BuildContext buildContext, int index) => _buildDetailCommentEmpty(state.itemInfo, dispatch) : null) ..add(state.commentListRes?.getHasMore() == true ? (BuildContext buildContext, int index) => _buildLoadMore(dispatch) : null)); return ListAdapter( (BuildContext buildContext, int index) => builders[index](buildContext, index), builders.length, ); } ///builds ``` ================================================ FILE: doc/concept/custom-adapter.md ================================================ # CustomAdapter - Custom implementation of large Cell in LisView. - The Factors of the Adapter are similar to the Component's. The difference is that the view part of the Adapter returns a ListAdapter. - Sample Code ```dart class CommentAdapter extends Adapter { CommentAdapter() : super( adapter: buildCommentAdapter, effect: buildCommentEffect(), reducer: buildCommentReducer(), ); } ListAdapter buildCommentAdapter(CommentState state, Dispatch dispatch, ViewService service) { final List builders = Collections.compact([] ..add((BuildContext buildContext, int index) => _buildDetailCommentHeader(state, dispatch, service)) ..addAll(_buildCommentViewList(state, dispatch, service)) ..add(isEmpty(state.commentListRes?.items) ? (BuildContext buildContext, int index) => _buildDetailCommentEmpty(state.itemInfo, dispatch) : null) ..add(state.commentListRes?.getHasMore() == true ? (BuildContext buildContext, int index) => _buildLoadMore(dispatch) : null)); return ListAdapter( (BuildContext buildContext, int index) => builders[index](buildContext, index), builders.length, ); } ///builds ``` ================================================ FILE: doc/concept/dependencies-cn.md ================================================ # Dependencies - Dependencies 是一个表达组件之间依赖关系的结构。它接收两个字段 - slots - {} - [adapter](adapter-cn.md) - 它主要包含三方面的信息 - slots,组件依赖的插槽。 - adapter,组件依赖的具体适配器(用来构建高性能的 ListView)。 - [Dependent](dependent-cn.md) 是 subComponent | subAdapter + [connector](connector-cn.md) 的组合。 - 一个 组件的 [Reducer](reducer-cn.md) 由 Component 自身配置的 Reducer 和它的 Dependencies 下的所有子 Reducers 自动复合而成。 - 示例代码 ```dart ///register in component class ItemComponent extends ItemComponent { ItemComponent() : super( view: buildItemView, reducer: buildItemReducer(), dependencies: Dependencies( slots: >{ 'appBar': AppBarComponent().asDependent(AppBarConnector()), 'body': ItemBodyComponent().asDependent(ItemBodyConnector()), 'ad_ball': ADBallComponent().asDependent(ADBallConnector()), 'bottomBar': BottomBarComponent().asDependent(BottomBarConnector()), }, ), ); } ///call in view Widget buildItemView(ItemState state, Dispatch dispatch, ViewService service) { return Scaffold( body: Stack( children: [ service.buildComponent('body'), service.buildComponent('ad_ball'), Positioned( child: service.buildComponent('bottomBar'), left: 0.0, bottom: 0.0, right: 0.0, height: 100.0, ), ], ), appBar: AppbarPreferSize(child: service.buildComponent('appBar'))); } ``` ================================================ FILE: doc/concept/dependencies.md ================================================ # Dependencies - Dependencies is a structure that expresses dependencies between components. It accepts two fields - slots - {} - [adapter](adapter.md) - It mainly contains three aspects of information - The slots that the component depends on. - The adapter that the component depends on (used to build a high-performance ListView). - [Dependent](dependent.md) Is a combination of subComponent | subAdapter + [connector](connector.md)。 - A component's [Reducer](reducer.md) is automatically compounded by the Reducer configured by the Component itself and all of the Reducers under its Dependencies. - Sample Code ```dart ///register in component class ItemComponent extends ItemComponent { ItemComponent() : super( view: buildItemView, reducer: buildItemReducer(), dependencies: Dependencies( slots: >{ 'appBar': AppBarComponent().asDependent(AppBarConnector()), 'body': ItemBodyComponent().asDependent(ItemBodyConnector()), 'ad_ball': ADBallComponent().asDependent(ADBallConnector()), 'bottomBar': BottomBarComponent().asDependent(BottomBarConnector()), }, ), ); } ///call in view Widget buildItemView(ItemState state, Dispatch dispatch, ViewService service) { return Scaffold( body: Stack( children: [ service.buildComponent('body'), service.buildComponent('ad_ball'), Positioned( child: service.buildComponent('bottomBar'), left: 0.0, bottom: 0.0, right: 0.0, height: 100.0, ), ], ), appBar: AppbarPreferSize(child: service.buildComponent('appBar'))); } ``` ================================================ FILE: doc/concept/dependent-cn.md ================================================ ### Dependent - Dependent = connector + subComponent | subAdapter 的组合,它表达了小组件|小适配器是如何连接到 Component 的。 - 示例代码 ```dart /// todo ``` ================================================ FILE: doc/concept/dependent.md ================================================ ### Dependent - Dependent = connector + subComponent | subAdapter - It expresses how the small component or adapter are connected to it's parent component. - Sample Code ```dart /// todo ``` ================================================ FILE: doc/concept/directory-cn.md ================================================ # Directory 推荐的目录结构会是这样 ``` sample_page -- action.dart /// define action types and action creator -- page.dart /// config a page or component -- view.dart /// define a function which expresses the presentation of user interface -- effect.dart /// define a function which handles the side-effect -- reducer.dart /// define a function which handles state-change -- state.dart /// define a state and some connector of substate components sample_component -- action.dart -- component.dart -- view.dart -- effect.dart -- reducer.dart -- state.dart ``` 上层负责组装,下层负责实现。 ================================================ FILE: doc/concept/directory.md ================================================ # Directory The recommended directory structure ``` sample_page -- action.dart /// define action types and action creator -- page.dart /// config a page or component -- view.dart /// define a function which expresses the presentation of user interface -- effect.dart /// define a function which handles the side-effect -- reducer.dart /// define a function which handles state-change -- state.dart /// define a state and some connector of substate components sample_component -- action.dart -- component.dart -- view.dart -- effect.dart -- reducer.dart -- state.dart ``` The upper layer is responsible for assembly and the lower layer is responsible for implementation. ================================================ FILE: doc/concept/dynamic-flow-adapter-cn.md ================================================ # DynamicFlowAdapter - 模版是一个 Map,接受一个数组类型的数据驱动 - 示例代码 ```dart class RecommendAdapter extends DynamicFlowAdapter { RecommendAdapter() : super( pool: >{ 'card_0': RecommendTitleComponent(), 'card_1': RecommendRowComponent(), }, connector: RecommendCardListConnector(), ); } ``` ================================================ FILE: doc/concept/dynamic-flow-adapter.md ================================================ # DynamicFlowAdapter - The template is a Map that accepts an array-like data driven - Sample Code ```dart class RecommendAdapter extends DynamicFlowAdapter { RecommendAdapter() : super( pool: >{ 'card_0': RecommendTitleComponent(), 'card_1': RecommendRowComponent(), }, connector: RecommendCardListConnector(), ); } ``` ================================================ FILE: doc/concept/effect-cn.md ================================================ # Effect Effect顾名思义,用于处理Action的副作用。 我估摸着有人就要问我了,副作用是啥玩意? 打个比方吧,假如我拥有一个函数 `f()` ```text fn f(x): return x * 1 ``` 此时此刻,另一个函数 `g()` ```text fn g(x): changeSystemEntropy() return ax ^ 2 + bx + c ``` 我们可以发现,`g()`里边有个改变系统熵的行为。这在函数式编程思想中,就叫做副作用,因为它可能影响到除了这个函数内部自身状态以外的其他状态。 在Fish-Redux中同样,我们通过 `dispatch()` 一些action实现状态修改,但是相对于状态来说,对外部的操作,类似于 `SystemChrome.setSystemUIOverlayStyle()`这样的操作,都是副作用。 现在介绍完了副作用,也没啥可介绍的了。 Effect用法跟Reducer差不太多,但是作用完全不同。 除了上面介绍的场景之外,异步请求也是一个经常会有的情况,这时候Effect可以帮你方便的解决这些问题。 你可以通过控制effect的返回值来达到某些目的,默认情况下,effect会在reducer之前被执行。 当前effect返回 `true` 的时候,就会停止后续的effect和reducer的操作 当前effect返回 `false` 的时候,后续effect和reducer继续执行 - Effect 是一个处理所有副作用的函数。它接收下面的参数 - Action action - Context context - BuildContext context - T state - dispatch - isDisposed Effect会接收来自 View 的“意图”,包括对应的生命周期的回调,然后做出具体的执行。 - 它的处理可能是一个异步函数,数据可能在过程中被修改,所以我们应该通过 context.state 获取最新数据。 - 如果它要修改数据,应该发一个 Action 到 Reducer 里去处理。它对数据是只读的,不能直接去修改数据。 - 如果它的返回值是一个非空值,则代表自己优先处理,不再做下一步的动作;否则广播给其他组件的 Effect 部分,同时发送给 Reducer。 > Self-First-Broadcast。 > ![image.png | left | 747x399](https://cdn.nlark.com/lark/0/2018/png/82574/1545365233153-4c8105b4-050c-49e6-be02-dbf28a861caa.png) - 示例代码 ```dart /// one style of writing FutureOr sideEffect(Action action, Context ctx) async { if (action.type == Lifecycle.initState) { //do something on initState return true; } else if (action.type == 'onShare') { //do something on onShare await Future.delayed(Duration(milliseconds: 1000)); ctx.dispatch(const Action('shared')); return true; } return null; } class MessageComponent extends Component { MessageComponent(): super( view: buildMessageView, effect: sideEffect, ); } ``` ```dart /// another style of writing Effect buildEffect() { return combineEffects(>{ Lifecycle.initState: _initState, 'onShare': _onShare, }); } void _initState(Action action, Context ctx) { //do something on initState } void _onShare(Action action, Context ctx) async { //do something on onShare await Future.delayed(Duration(milliseconds: 1000)); ctx.dispatch(const Action('shared')); } class MessageComponent extends Component { MessageComponent(): super( view: buildMessageView, effect: buildEffect(), ); } ``` ================================================ FILE: doc/concept/effect.md ================================================ # Effect - Effect is a function that handles all side effects. It receives the following parameters - Action action - Context context - BuildContext context - T state - dispatch - isDisposed - It mainly contains four aspects of information - Receive "intent" from the View, including the corresponding lifecycle callback, and then make specific execution. - Its processing may be an asynchronous function, the data may be changed in the process, so we should get the latest data through context.state. - If you want to modify the data, you should send an Action to the Reducer to handle. It is read-only for data and cannot be modified directly in a effect function. - If its return value is a non-null value, it will take precedence for itself and will not do the next step; otherwise it will broadcast to the Effect part of other components and sent the action to the Reducer. > Self-First-Broadcast。 > ![image.png | left | 747x399](https://cdn.nlark.com/lark/0/2018/png/82574/1545365233153-4c8105b4-050c-49e6-be02-dbf28a861caa.png) - Sample Code ```dart /// one style of writing FutureOr sideEffect(Action action, Context ctx) async { if (action.type == Lifecycle.initState) { //do something on initState return true; } else if (action.type == 'onShare') { //do something on onShare await Future.delayed(Duration(milliseconds: 1000)); ctx.dispatch(const Action('shared')); return true; } return null; } class MessageComponent extends Component { MessageComponent(): super( view: buildMessageView, effect: sideEffect, ); } ``` ```dart /// another style of writing Effect buildEffect() { return combineEffects(>{ Lifecycle.initState: _initState, 'onShare': _onShare, }); } void _initState(Action action, Context ctx) { //do something on initState } void _onShare(Action action, Context ctx) async { //do something on onShare await Future.delayed(Duration(milliseconds: 1000)); ctx.dispatch(const Action('shared')); } class MessageComponent extends Component { MessageComponent(): super( view: buildMessageView, effect: buildEffect(), ); } ``` ================================================ FILE: doc/concept/evolution-of-fish-redux.md ================================================ # fish-redux 的演进史 fish-redux 是一个不断演进的框架,甚至是在不断的回炉重造,在这个过程中 - 1. 第一个版本是基于社区内的 flutter_redux 进行的改造,核心是提供了 UI 代码的组件化,当然问题也非常明显,针对复杂的业务场景,往往业务逻辑很多,无法做到逻辑代码的分治和复用。 - 2. 第二个版本针对第一个版本的问题,做出了比较重大的修改,解决了 UI 代码和逻辑代码的分治问题,但设计上打破了 redux 的原则,丢失了 Redux 的精华。 - 3. 在第三个版本进行重构时,我们确立了整体的架构原则与分层要求,一方面按照 reduxjs 的代码进行了 flutter 侧的 redux 实现,将 redux 完整保留下来。另一方面针对组件化的问题,提供了 redux 之上的 component 的封装,并创新的通过这一层的架构设计提供了业务代码分治的能力。第三版 完成了 Redux, Component 两层的设计,其中包含了 Connector,Dependencies,Context 等重要概念。 - 3.1 解决集中和分治的矛盾的核心在于 [Connector](what's-connector.md) - 3.2 这一层的组件的分治是面向通用设计的。通过在 [Dependencies](dependencies-cn.md) 配置 slots,得到了可插拔的组件系统。 - 4. 在第三个版本 Redux & Component 之外,提供了面向 ListView 场景的分治设计 Adapter。 - 解决了在面向 ListView 场景下的逻辑的分治和性能降低的矛盾。 - [what's-adapter](what's-adapter.md) > 目前,fish redux 已经在闲鱼线上稳定运行,未来,期待 fish redux 给社区带来更多的输入。 ================================================ FILE: doc/concept/features.md ================================================ # Features ## 直接使用 flutter 会面临的问题? > [flutter](https://github.com/flutter/flutter) 是 google 推出的新一代跨平台渲染框架. > 它帮助开发者解决了跨平台,高性能,富有表现力和灵活的 UI 表达,快速开发等核心问题。 > 但是如果开发大应用,还需要解决以下问题。 > > > 1. 数据流问题 > > 2. 通信问题 > > 3. 可插拔的组件系统 > > 4. 展示和逻辑解耦 > > 5. 统一的编程模型和规范 > > 我们可以类比 flutter 和 React,事实上在中大型应用中 React 会面临的绝大多数问题,flutter 也同样面临考验。 ## 数据流问题 > 目前社区流行的数据流方案有: > 单向数据流方案,以 Redux 为代表 > 响应式数据流方案,以 Mobx 为代表 > 其他,以 rxjs 为代表 > 那么哪一种架构最合适 flutter ? > 我们追随了 javascript 栈绝大多数开发者的选择 - [ReduxJs](https://github.com/reduxjs/redux) > 感谢 ReduxJs,我们是几乎 100%的还原了它在 dart 上的实现。所以我们也继承了它的优点:[Predictable],[Centralized],[Debuggable],[Flexible]。 ## 通信问题 > 直接使用 flutter,在 Widgets 之间传递状态和回调,随着应用复杂度的上升,会变成是一件可怕而糟糕的事情。 > 通过 fish redux,依托于集中的 Redux 和分治的 Effect 模块,通过一个极简的 [dispatch-api](mechanism.md),完成所有的通信的诉求。 ## 可插拔的组件系统 > fish redux 通过一个配置式的 Dependencies,来完成灵活的可插拔的组件系统。同时有这一配置的存在,它解放了我们手动拼装 Reducer 的繁琐工作。 > 参考: > > > 1. [what's-connector](what's-connector.md) > > 2. [connector](connector.md) > > 3. [dependencies](dependencies.md) > > 4. [component](component.md) > > 5. [adapter](adapter.md) > > 6. [what's-adapter](what's-adapter.md) ## 展示和逻辑解耦 > fish redux 从 [elm](https://guide.elm-lang.org/) 中得到了非常多的设计灵感。 > 将一个组件,拆分为相互独立的 View,Effect,Reducer 三个函数,我们优雅的解决了展示和逻辑解耦的问题。 > 通过这样的拆分,我们将 UI 的表达隔离于一个函数内,它让我们更好的面向未来,一份 UI 表达它可能来自于开发者,可能来自于深度学习框架的 UI 代码生成,可能是面向移动终端,也可能是面向浏览器。它让我们有了更多的组合的可能。 > 同时函数式的编程模型带来了更容易编写,更容易扩展,更容易测试,更容易维护等特性。 ## 统一的编程模型和规范 > [directory](directory.md) ================================================ FILE: doc/concept/filter-cn.md ================================================ # Filter - Filter 是用来优化 Reducer 的性能的。因为 Reducer 是层层组装的,所以处理每一个 Action,理论上会遍历一遍所有的小 Reducer,在一些非常复杂的场景下,这样的一次深度遍历的耗时可能会到毫秒级别(一般情况下都应该小于 1 毫秒)。那么我们需要对 Reducer 做性能优化,提前决定要不要遍历这份 Reducer 子树,减少遍历的深度和次数。 - 示例代码 ```dart bool filter(Action action) { return action.type == 'some action'; } ``` ================================================ FILE: doc/concept/filter.md ================================================ # Filter - Filter is used to optimize the performance of the Reducer. Because the Reducer is layer-assembled, each Action is processed, and in theory, all the small Reducers are traversed. In some very complicated scenarios, such a deep traversal may take up to the millisecond level (generally Should be less than 1 millisecond). Then we need to optimize the performance of the Reducer, decide in advance whether to traverse this Reducer subtree, reduce the depth and number of traversal. - Sample Code ```dart bool filter(Action action) { return action.type == 'some action'; } ``` ================================================ FILE: doc/concept/higher-effect-cn.md ================================================ # HigherEffect - 由于 Effect 有可能有自己一些临时状态(尽管不建议这么做,但还是提供了支持),为了支持这个特性,我们将 Effect 提升为高阶函数,将它的状态放在闭包里。 - 框架支持 Effect|HigherEffect 的配置,但是不能对一个组件或适配器同时都配置,那样会带来困扰,一般情况下,都配置往往是个显式的疏忽大意。 - HigherEffect = (Context ctx) => (Action action) => FutureOr - 更详细的例子请参考 [OOP](oop-cn.md) - EffectPart ================================================ FILE: doc/concept/higher-effect.md ================================================ # HigherEffect - Since Effect may have some temporary state of its own (although it is not recommended, support is provided), in order to support this feature, we promote the Effect to a higher-order function and put its state in the closure. - The framework supports the configuration of Effect|HigherEffect on component or adapter, but it can't be configured for one component or adapter at the same time, which will cause trouble. In general, the configuration is often an explicit negligence. - HigherEffect = (Context ctx) => (Action action) => FutureOr - For more detailed examples, please refer to [OOP](oop.md) - EffectPart ================================================ FILE: doc/concept/lifecycle-cn.md ================================================ # Lifecycle - 默认的所有生命周期,本质上都来自于 flutter State 中的生命周期。 - initState - didChangeDependencies - build - didUpdateWidget - deactivate - dispose - 在组件内,Reducer 的生命周期是和页面一致的,Effect 和 View 的生命周期是和组件的 Widget 一致的。 - 在适配器中,Reducer 的生命周期是和页面一致的,Effect 的生命周期是和 ListView 的生命周期一致,View 的生命周期是短暂的(划入不可见区域即销毁)。同时增加了 appear 和 disappear 的生命周期, 代表这个 adapter 管理的视图数组,刚进入显示区和完全离开显示区的回调。 ================================================ FILE: doc/concept/lifecycle.md ================================================ # Lifecycle - The default all lifecycles are essentially derived from the lifecycle in flutter State. - initState - didChangeDependencies - build - didUpdateWidget - deactivate - dispose - Within the component, the Lifecycle of the Reducer is consistent with the page, and the lifecycle of Effect and View is consistent with the component's Widget. - In the adapter, the Lifecycle of the Reducer is consistent with the page. The life cycle of the Effect is the same as the life cycle of the ListView. The life cycle of the View is short-lived (destroyed in the invisible area). At the same time, the life cycle of appear and disappear is added, representing the view array managed by this adapter, the callback just entering the display area and completely leaving the display area. ================================================ FILE: doc/concept/mechanism-cn.md ================================================ # Communication Mechanism ## 页面内通信 - 组件|适配器内通信 - 组件|适配器间内通信 ![image.png | left | 747x399](https://cdn.nlark.com/lark/0/2018/png/82574/1545365233153-4c8105b4-050c-49e6-be02-dbf28a861caa.png) Self-First-Broadcast。 发出的 Action,自己优先处理,否则广播给其他组件和 Redux 处理。 最终我们通过一个简单而直观的 dispatch 完成了组件内,组件间(父到子,子到父,兄弟间等)的通信。 ## 页面间通信 - 页面间通信 - Context.appBroadcast - 每一个页面的 PageStore 都会收到消息,各自独立负责处理。 ![image.png | left | 691x519](https://cdn.nlark.com/lark/0/2018/png/82574/1545368705599-745c46a3-f5c6-41a7-a757-1bc6f9a389d4.png) # Refresh Mechanism ## 数据刷新 - 局部数据修改,自动层层触发上层数据的浅拷贝,对业务代码是透明的。 - 层层的数据的拷贝 - 一方面是对 Redux 数据修改的严格的 follow。 - 另一方面也是对数据驱动展示的严格的 follow。 - 数据的任何一个局部的变动,必须要让能看到这个局部的所有视图感知到。如果不拷贝,对应的视图通过新旧两份数据的比较(同一个引用),会错以为自己没有发生变化。 ![image.png | left | 747x361](https://cdn.nlark.com/lark/0/2018/png/82574/1545386668521-0081cb5f-8017-47d1-ad7c-8802bb0be8a0.png) ## 视图刷新 - 扁平化通知到所有组件,组件通过 shouldUpdate 确定自己是否需要刷新 ![image.png | left | 747x336](https://cdn.nlark.com/lark/0/2018/png/82574/1545386773247-2eddfa99-e6b9-4be9-ac43-d1944ff44e9b.png) ================================================ FILE: doc/concept/mechanism.md ================================================ # Communication Mechanism ## Page internal communication - Component internal communication - Inter-component communication ![image.png | left | 747x399](https://cdn.nlark.com/lark/0/2018/png/82574/1545365233153-4c8105b4-050c-49e6-be02-dbf28a861caa.png) Self-First-Broadcast。 The emitted Action will be processed first by its own Effect, otherwise it will be broadcast to other components and Redux. We completed the communication between the components (parent to child, child to parent, brother, etc.) through a simple and intuitive dispatch. ## Inter-page communication - Context.appBroadcast - Each page's PageStore receives an action which is handled independently. ![image.png | left | 691x519](https://cdn.nlark.com/lark/0/2018/png/82574/1545368705599-745c46a3-f5c6-41a7-a757-1bc6f9a389d4.png) # Refresh Mechanism ## 数据刷新 - Local data modification automatically triggers a shallow copy of the upper layer data and is transparent to the business code. ![image.png | left | 747x361](https://cdn.nlark.com/lark/0/2018/png/82574/1545386668521-0081cb5f-8017-47d1-ad7c-8802bb0be8a0.png) ## View refresh - When the state changes, the store flatly notifies all the components and the [ShouldUpdate](should-update.md) decide whether the view should be refreshed ![image.png | left | 747x336](https://cdn.nlark.com/lark/0/2018/png/82574/1545386773247-2eddfa99-e6b9-4be9-ac43-d1944ff44e9b.png) ================================================ FILE: doc/concept/middleware-cn.md ================================================ # Middleware 关于 Middleware 的定义、签名和 ReduxJS 社区是一致的。 示例代码 ```dart Middleware logMiddleware({ String tag = 'redux', String Function(T) monitor, }) { return ({Dispatch dispatch, Get getState}) { return (Dispatch next) { return isDebug() ? (Action action) { print('---------- [$tag] ----------'); print('[$tag] ${action.type} ${action.payload}'); final T prevState = getState(); if (monitor != null) { print('[$tag] prev-state: ${monitor(prevState)}'); } next(action); final T nextState = getState(); if (monitor != null) { print('[$tag] next-state: ${monitor(nextState)}'); } if (prevState == nextState) { print('[$tag] warning: ${action.type} has not been used.'); } print('========== [$tag] ================'); } : next; }; }; } ``` 更多的参考 src/utils/common_middleware ================================================ FILE: doc/concept/middleware.md ================================================ # Middleware - The definition and signature of Middleware is consistent with the ReduxJS community. - Sample Code ```dart Middleware logMiddleware({ String tag = 'redux', String Function(T) monitor, }) { return ({Dispatch dispatch, Get getState}) { return (Dispatch next) { return isDebug() ? (Action action) { print('---------- [$tag] ----------'); print('[$tag] ${action.type} ${action.payload}'); final T prevState = getState(); if (monitor != null) { print('[$tag] prev-state: ${monitor(prevState)}'); } next(action); final T nextState = getState(); if (monitor != null) { print('[$tag] next-state: ${monitor(nextState)}'); } if (prevState == nextState) { print('[$tag] warning: ${action.type} has not been used.'); } print('========== [$tag] ================'); } : next; }; }; } ``` 更多的参考 src/utils/common_middleware ================================================ FILE: doc/concept/on-error-cn.md ================================================ # OnError - 集中处理由 Effect 产生的业务异常,无论是同步函数还是异步函数。有了统一的异常处理机制,我们就能站在一个更高的抽象角度,对业务代码做出合理的简化。 - 示例代码 ```dart bool onMessageError(Exception e, Context ctx) { if(e is BizException) { ///do some toast return true; } return false; } class MessageComponent extends Component { MessageComponent(): super( view: buildMessageView, effect: buildEffect(), reducer: buildMessageReducer(), onError: onMessageError, ); } ``` ================================================ FILE: doc/concept/on-error.md ================================================ # OnError - Centralizes the business exceptions generated by Effect, whether it is a synchronous function or an asynchronous function. With a unified exception handling mechanism, we can stand on a higher level of abstraction and make reasonable simplifications of business code. - Sample Code ```dart bool onMessageError(Exception e, Context ctx) { if(e is BizException) { ///do some toast return true; } return false; } class MessageComponent extends Component { MessageComponent(): super( view: buildMessageView, effect: buildEffect(), reducer: buildMessageReducer(), onError: onMessageError, ); } ``` ================================================ FILE: doc/concept/oop-cn.md ================================================ # OOP - 虽然框架推荐使用的函数式的编程方式,也提供面向对象式的编程方式的支持。 - ViewPart - 需要复写 build 函数。 - 需要的 state,dispatch,viewService 的参数,已经成为了对象的字段可以直接使用。 - 它是@immutable 的,所以不应该也不需要在内部定义可变字段。 - EffectPart - 需要复写 createMap 函数。 - 需要的 Context 已经被打平,作为了对象的字段可以直接使用。 - 可以定义字段,它的可见性也仅限于自身。 - 它必须配合 higherEffect 一起使用。 - 示例代码 ```dart class MessageView extends ViewPart { @override Widget build() { return Column(children: [ viewService.buildComponent('profile'), InkWell( child: Text('$message'), onTap: () => dispatch(const Action('onShare')), ), ]); } } class MessageEffect extends EffectPart { ///we could put some Non-UI fields here. @override Map createMap() { return { Lifecycle.initState: _initState, 'onShare': _onShare, }; } void _initState(Action action) { //do something on initState } void _onShare(Action action) async { //do something on onShare await Future.delayed(Duration(milliseconds: 1000)); dispatch(const Action('shared')); } } class MessageComponent extends Component { MessageComponent(): super( view: MessageView().asView(), higherEffect: higherEffect(() => MessageEffect()), ); } ``` ================================================ FILE: doc/concept/oop.md ================================================ # OOP - Although the framework recommends the use of functional programming, it also provides object-oriented programming support. - ViewPart - Need to override the 'build' function. - The required state, dispatch, and viewService parameters have become fields of the object and can be used directly. - It is immutable, so there should be no need to define variable fields internally. - EffectPart - Need to override the 'createMap' function. - The required Context has been flattened as the fields which can be used directly. - Fields can be defined and their visibility is limited to themselves. - It must be used with higherEffect. - Sample Code ```dart class MessageView extends ViewPart { @override Widget build() { return Column(children: [ viewService.buildComponent('profile'), InkWell( child: Text('$message'), onTap: () => dispatch(const Action('onShare')), ), ]); } } class MessageEffect extends EffectPart { ///we could put some Non-UI fields here. @override Map createMap() { return { Lifecycle.initState: _initState, 'onShare': _onShare, }; } void _initState(Action action) { //do something on initState } void _onShare(Action action) async { //do something on onShare await Future.delayed(Duration(milliseconds: 1000)); dispatch(const Action('shared')); } } class MessageComponent extends Component { MessageComponent(): super( view: MessageView().asView(), higherEffect: higherEffect(() => MessageEffect()), ); } ``` ================================================ FILE: doc/concept/page-cn.md ================================================ # Page - 一个页面内都有且仅有一个 Store - Page 继承于 Component,所以它能配置所有 Component 的要素 - Page 能配置 Middleware,用于对 Redux 做 AOP 管理 - Page 必须配置一个初始化页面数据的初始化函数  initState - 示例代码 ```dart /// Hello World class HelloWordPage extends Page { HelloWordPage(): super( initState: (String msg) => msg, view:(String msg, _, __) => Text('Hello ${msg}'), ); } HelloWordPage().buildPage('world') ``` ================================================ FILE: doc/concept/page.md ================================================ # Page - One and only one store in one page - Page inherits from Component, so it can configure all the factors of Component. - Page can configure Middleware for AOP management of Redux. - Page must be configured with an initialization function that initializes page data initState. - Sample Code ```dart /// Hello World class HelloWordPage extends Page { HelloWordPage(): super( initState: (String msg) => msg, view:(String msg, _, __) => Text('Hello ${msg}'), ); } HelloWordPage().buildPage('world') ``` ================================================ FILE: doc/concept/reducer-cn.md ================================================ # Reducer - Reducer 是一个上下文无关的 pure function。它接收下面的参数 - T state - Action action - 它主要包含三方面的信息 - 接收一个“意图”, 做出数据修改。 - 如果要修改数据,需要创建一份新的拷贝,修改在拷贝上。 - 如果数据修改了,它会自动触发 State 的层层数据的拷贝,再以扁平化方式通知组件刷新。 - 示例代码 ```dart /// one style of writing String messageReducer(String msg, Action action) { if (action.type == 'shared') { return '$msg [shared]'; } return msg; } class MessageComponent extends Component { MessageComponent(): super( view: buildMessageView, effect: buildEffect(), reducer: messageReducer, ); } ``` ```dart /// another style of writing Reducer buildMessageReducer() { return asReducer(>{ 'shared': _shared, }); } String _shared(String msg, Action action) { return '$msg [shared]'; } class MessageComponent extends Component { MessageComponent(): super( view: buildMessageView, effect: buildEffect(), reducer: buildMessageReducer(), ); } ``` > 推荐的是第二种写法 ================================================ FILE: doc/concept/reducer.md ================================================ # Reducer - The Reducer is a context-independent pure function. It receives the following parameters - T state - Action action - It mainly contains three aspects of information - Receive an "intent" and make a state modification. - If you want to modify the state, you need to create a new copy and modify it on the copy. - If the small state is modified, it will automatically trigger the copy of the main state's layers data, and then notify the components to refresh in a flattened manner. - Sample Code ```dart /// one style of writing String messageReducer(String msg, Action action) { if (action.type == 'shared') { return '$msg [shared]'; } return msg; } class MessageComponent extends Component { MessageComponent(): super( view: buildMessageView, effect: buildEffect(), reducer: messageReducer, ); } ``` ```dart /// another style of writing Reducer buildMessageReducer() { return asReducer(>{ 'shared': _shared, }); } String _shared(String msg, Action action) { return '$msg [shared]'; } class MessageComponent extends Component { MessageComponent(): super( view: buildMessageView, effect: buildEffect(), reducer: buildMessageReducer(), ); } ``` > 推荐的是第二种写法 ================================================ FILE: doc/concept/redux-cn.md ================================================ ### Redux - State - [Action](action-cn.md) - [Reducer](reducer-cn.md) - Store - [Middleware](middleware-cn.md) 以上概念和社区的 Redux 是完全一致的。 Redux 是一个用来做[可预测][集中式][易调试][灵活性]的数据管理的框架。 如果想对 Redux 有更近一步的理解,请参考 [https://github.com/reduxjs/redux](https://github.com/reduxjs/redux) ================================================ FILE: doc/concept/redux.md ================================================ ### Redux - State - [Action](action.md) - [Reducer](reducer.md) - Store - [Middleware](middleware.md) The above concepts are exactly the same as the community's Redux. Redux is a framework for state management with [predictable][centralized] [easy to debug][flexibility]. If you want to have a closer understanding of Redux, please refer to [https://github.com/reduxjs/redux](https://github.com/reduxjs/redux) ================================================ FILE: doc/concept/should-update-cn.md ================================================ # ShouldUpdate - 当数据发生变更,Store 扁平化地通知所有组件 - 框架默认使用 identical 比较新旧两份数据来决定是否需要刷新。 - 如果我们对组件的刷新会有非常精确化的诉求, 那么我们可以自己定义一个 ShouldUpdate。 - 示例代码 ```dart bool shouldUpdate(DetailState old, DetailState now) { return old.message != now.message; } ``` ================================================ FILE: doc/concept/should-update.md ================================================ # ShouldUpdate - When the state changes, the store flatly notifies all the components. - By default, the framework uses identical to compare the old and new state to determine if a refresh is needed. - If we have a very precise request for component refresh, then we can define a ShouldUpdate ourselves. - Sample Code ```dart bool shouldUpdate(DetailState old, DetailState now) { return old.message != now.message; } ``` ================================================ FILE: doc/concept/static-flow-adapter-cn.md ================================================ # StaticFlowAdapter - 模版是一个 Array,接受 Object|Map 的数据驱动。 - 模版接收一个 Dependent 的数组,每一个 Dependent 可以是 Component 或者 Adapter + Connector 的组合。 - 抽象地看,它非常的像是一个 flatMap + compact 的操作。 - 示例代码 ```dart class ItemBodyComponent extends Component { ItemBodyComponent() : super( view: buildItemBody, dependencies: Dependencies( adapter: StaticFlowAdapter( slots: >[ VideoAdapter().asDependent(videoConnector()), UserInfoComponent().asDependent(userInfoConnector()), DescComponent().asDependent(descConnector()), ItemImageComponent().asDependent(itemImageConnector()), OriginDescComponent().asDependent(originDescConnector()), VisitComponent().asDependent(visitConnector()), SameMoreComponent().asDependent(sameMoreConnector()), PondComponent().asDependent(pondConnector()), CommentAdapter().asDependent(commentConnector()), RecommendAdapter().asDependent(recommendConnector()), PaddingComponent().asDependent(paddingConnector()), ]), ), ); } ``` ================================================ FILE: doc/concept/static-flow-adapter.md ================================================ # StaticFlowAdapter - The template is an Array that accepts map-like data driven. - The template receives an array of Dependents. - It's similar with a flatMap + compact operation abstractly. - Sample Code ```dart class ItemBodyComponent extends Component { ItemBodyComponent() : super( view: buildItemBody, dependencies: Dependencies( adapter: StaticFlowAdapter( slots: >[ VideoAdapter().asDependent(videoConnector()), UserInfoComponent().asDependent(userInfoConnector()), DescComponent().asDependent(descConnector()), ItemImageComponent().asDependent(itemImageConnector()), OriginDescComponent().asDependent(originDescConnector()), VisitComponent().asDependent(visitConnector()), SameMoreComponent().asDependent(sameMoreConnector()), PondComponent().asDependent(pondConnector()), CommentAdapter().asDependent(commentConnector()), RecommendAdapter().asDependent(recommendConnector()), PaddingComponent().asDependent(paddingConnector()), ]), ), ); } ``` ================================================ FILE: doc/concept/view-cn.md ================================================ # View - View 是一个输出 Widget 的上下文无关的函数。它接收下面的参数 - T state - Dispatch - ViewService - 它主要包含三方面的信息 - 视图完全由数据驱动。 - 视图产生的事件/回调,通过 Dispatch 发出“意图”,但绝不做具体的实现。 - 使用依赖的组件/适配器,通过在组件上显示配置,再通过 ViewService 标准化调用。 - 其中 ViewService 提供了三个能力 - BuildContext context,获取 flutter Build-Context 的能力 - Widget buildView(String name), 直接创建子组件的能力 - 这里传入的 name 即在 Dependencies 上配置的名称。 - 创建子组件不需要传入任何其他的参数,因为子组件需要的参数,已经通过 Dependencies 配置中,将它们的数据关系,通过 connector 确立。 - ListAdapter buildAdapter(), 直接创建适配器的能力 - 示例代码 ```dart Widget buildMessageView(String message, Dispatch dispatch, ViewService viewService) { return Column(children: [ viewService.buildComponent('profile'), InkWell( child: Text('$message'), onTap: () => dispatch(const Action('onShare')), ), ]); } class MessageComponent extends Component { MessageComponent(): super( view: buildMessageView, ); } ``` ================================================ FILE: doc/concept/view.md ================================================ # View - View is a context-independent function that outputs Widget. It receives the following parameters - T state - Dispatch - ViewService - It mainly contains three aspects of information - The view is completely driven by data. - The event/callback triggered by the view, use Dispatch to send "intent", but never do a specific implementation. - Use dependent component/adapter, by explicitly configuring it on the parent component, and then standardizing calls through the ViewService. - Where ViewService provides three capabilities - BuildContext context: Ability to get widget's BuildContext - Widget buildView(String name): Ability to create subcomponents directly - The name passed in here is the name configured on Dependencies. - Creating subcomponents does not require passing in any other parameters, since the parameters required by the subcomponents have been passed through the Dependencies configuration, and their data relationships are established via the connector. - ListAdapter buildAdapter(): Ability to create adapter directly - Sample Code ```dart Widget buildMessageView(String message, Dispatch dispatch, ViewService viewService) { return Column(children: [ viewService.buildComponent('profile'), InkWell( child: Text('$message'), onTap: () => dispatch(const Action('onShare')), ), ]); } class MessageComponent extends Component { MessageComponent(): super( view: buildMessageView, ); } ``` ================================================ FILE: doc/concept/what's-adapter.md ================================================ # What's adapter 面向 ListView 场景的分治设计 Adapter。 > 在解答什么是 adapter 之前,我们来看下一般框架对 ListView 的分治是怎么做的。传统的手段,我们对 ListView 的分治更多的局限于它展现部分,而它的逻辑部分往往是集中的。而当我们试图将 ListView 下的某一局部的展现和逻辑封装在一起,我们就会遇到"Big-Cell"问题,面临性能的显著降低。 > 这里面存在一个分治和性能上的矛盾。这个矛盾带来了复用难,可维护差,难以协作等中大型场景下的问题。 > > 解决这个问题,有两种思路: > > 1. 下沉到 UI 表达层(Widgets),去实现一个高性能的 ScrollView。 > 2. 向上做模型抽象,得到一个逻辑上的 ScrollView,性能上的 ListView。 > > fish redux 选择了第二条更加通用的路径来解决 LisView 下的分治问题。 > > 一个 ListView 对应了一个 Adapter,这看上去非常的像 Android 里的设计,但事实上 fish-redux 里的 Adapter 概念走的更远。 > > 1. 一个 Adapter 是可以由多个 Component 和 Adapter 组合而成,它有点像 flatmap & compact 的 api 的叠加。 > 2. Adapter 以及它的子 Adapter 的生命周期是和 ListView 是等效的。它像跨斗一般附着于 ListView 的生命周期之上。同时由于 Adapter 生命周期的提升,我们额外收获了两个非常有用的事件消息(appear & disappear)。 > > > 注意 ⚠️ 在 Adapter 里配置的子 Component,它的生命周期和它所对应的 WidgetState 是一致的,所以它的是短暂的。 - Adapter 的容器有两类,用图来说明吧: ================================================ FILE: doc/concept/what's-connector.md ================================================ # What's connector 在解答 connector 是什么之前,我们来先看一个代码片段 ```javascript let hasChanged = false; const nextState = {}; for (let i = 0; i < finalReducerKeys.length; i++) { const key = finalReducerKeys[i]; const reducer = finalReducers[key]; const previousStateForKey = state[key]; const nextStateForKey = reducer(previousStateForKey, action); if (typeof nextStateForKey === 'undefined') { const errorMessage = getUndefinedStateErrorMessage(key, action); throw new Error(errorMessage); } nextState[key] = nextStateForKey; hasChanged = hasChanged || nextStateForKey !== previousStateForKey; } return hasChanged ? nextState : state; ``` 以上来自于 Reduxjs-[combineReducers](https://github.com/reduxjs/redux/blob/master/src/combineReducers.js)的核心实现。 combineReducers 是一个将 Reducer 分治的函数,让一个庞大数据的 Reducer 可以由多层的更小的 Reducer 组合而成。 这是 Redux 框架里的核心 API,但是他有缺点。他有非常明显的语言的局限性,如下 3 点: 1. 浅拷贝一个任意对象 ```javascript const nextState = {}; ``` 2. 读取字段 ```javascript const previousStateForKey = state[key]; ``` 3. 写入字段 ```javascript nextState[key] = nextStateForKey; ``` 将上面的 3 点抽象来看: 1. State 的 clone 的能力(浅拷贝) 2. Get & Set 的能力,即为 Connector 的概念。 有了 以上两点,我们才完全集成了 Redux 的所有精华,同时将它的设计更上一个通用的维度。 ================================================ FILE: doc/concept/what's-the-diiference-cn.md ================================================ # What's different with Redux ? ## 它们是解决不同层面问题的两个框架 > Redux 是一个专注于状态管理的框架;Fish Redux 是基于 Redux 做状态管理的应用框架。 > 应用框架不仅仅要解决状态管理的问题,还要解决分治,通信,数据驱动,解耦等等问题。 ## Fish Redux 解决了集中和分治的矛盾。 > Redux 通过使用者手动组织代码的形式来完成从小的 Reducer 到主 Reducer 的合并过程; > Fish Redux 通过显式的表达组件之间的依赖关系,由框架自动完成从细力度的 Reducer 到主 Reducer 的合并过程; ## Fish Redux 提供了一个简单的组件抽象模型 > 它通过简单的 3 个函数组合而成 ## Fish Redux 提供了一个 Adapter 的抽象组件模型 > 在基础的组件模型以外,Fish Redux 提供了一个 Adapter 抽象模型,用来解决在 ListView 上大 Cell 的性能问题。 > 通过上层抽象,我们得到了逻辑上的 ScrollView,性能上的 ListView。 ================================================ FILE: doc/concept/what's-the-diiference.md ================================================ # What's different with Redux ? ## They are two frameworks for solving problems at different layers. > Redux is a framework focused on state management; Fish Redux is an application framework based on Redux for state management. > The application framework not only solves the problem of state management, but also solves the problems of divide and conquer, communication, data drive, decoupling and so on. ## Fish Redux solves the contradiction between concentration and division. > Redux completes the merge process from the small Reducers to the main Reducer by the user manually organizing the code; > Fish Redux automatically completes the merge process from the small Reducers to the main Reducer by explicitly expressing the dependencies between components; ## Fish Redux provides a simple component abstract model > It is a combination of simple 3 functions ## Fish Redux provides an abstract component model of the Adapter > In addition to the underlying component model, Fish Redux provides an Adapter abstraction model to solve the performance problems of large cells on ListView. > Through the upper abstraction, we get the logical ScrollView, the performance of the ListView. ================================================ FILE: doc/concept/widget-wrapper-cn.md ================================================ ### WidgetWrapper - 它用来解决 flutter 的 ui 体系下,一些需要实现特色接口的 Widget,比如 KeepAlive,因为通过 Component 产生的 Widget 会被一个框架内部的 Stateful 的 Widget 所包裹。 - 示例代码 ```dart import 'package:flutter/material.dart' hide Action, Page; Widget repaintBoundaryWrapper(Widget widget) { return RepaintBoundary(child: widget); } ``` ================================================ FILE: doc/concept/widget-wrapper.md ================================================ ### WidgetWrapper - It is used to solve flutter's ui system, some Widgets that need to implement the featured interface, such as KeepAlive, because the Widget generated by Component will be wrapped by a Stateful Widget inside the fish redux framework. - Sample Code ```dart import 'package:flutter/material.dart' hide Action, Page; Widget repaintBoundaryWrapper(Widget widget) { return RepaintBoundary(child: widget); } ``` ================================================ FILE: doc/introduction/README-cn.md ================================================ # 简介 Fish Redux 是一个基于 Redux 数据管理的组装式 flutter 应用框架, 它特别适用于构建中大型的复杂应用。 它的特点是配置式组装。 一方面我们将一个大的页面,对视图和数据层层拆解为互相独立的 Component|Adapter,上层负责组装,下层负责实现; 另一方面将 Component|Adapter 拆分为 View,Reducer,Effect 等相互独立的上下文无关函数。 所以它会非常干净,易维护,易协作。 Fish Redux 的灵感主要来自于 Redux, Elm, Dva 这样的优秀框架。而 Fish Redux 站在巨人的肩膀上,将集中,分治,复用,隔离做的更进一步。 ================================================ FILE: doc/introduction/README.md ================================================ # Introduction Fish Redux is an assembled flutter application framework based on Redux state management, it is especially suitable for building medium to large complex applications. It features a profiled assembly. On the one hand, we disassemble a large page into separate Component|Adapters. The upper layer is responsible for assembly, and the lower layer is responsible for implementation. On the other hand, the Component|Adapter is split into separate context-independent functions such as View, Reducer, and Effect. So it will be very clean, easy to maintain, and easy to collaborate. Fish Redux is inspired by the excellent frameworks such as Redux, Elm, Dva. Standing on the shoulders of giants, Fish Redux will do it further on the characteristics of concentration, division, reuse and isolation. ================================================ FILE: docs/.nojekyll ================================================ ================================================ FILE: docs/README.md ================================================

Fish Redux

[![Build Status](https://travis-ci.org/alibaba/fish-redux.svg?branch=master)](https://travis-ci.org/alibaba/fish-redux) [![pub package](https://img.shields.io/pub/v/fish_redux.svg)](https://pub.dartlang.org/packages/fish_redux) [![codecov](https://codecov.io/gh/alibaba/fish-redux/branch/master/graph/badge.svg)](https://codecov.io/gh/alibaba/fish-redux) ## What is Fish Redux ? Fish Redux is an assembled flutter application framework based on Redux state management. It is suitable for building medium and large applications. It has four characteristics: > 1. Functional Programming > 2. Predictable state container > 3. Pluggable componentization > 4. Non-destructive performance ## Architecture diagram ## Installation [Go](https://pub.dartlang.org/packages/fish_redux#-installing-tab-) ## Documentation Language: [English](/) | [中文简体](/zh-cn/) ## Examples - [todo list](example) - a simple todo list demo. - run it: ``` cd ./example flutter create . flutter run ``` ## What's the difference between 'Fish Redux' and 'Redux' ? - [answers](doc/concept/what's-the-diiference.md) ## Plugins ### Code Template - [Fish Redux Template For Android Studio](https://github.com/BakerJQ/FishReduxTemplateForAS), by BakerJQ. - [Fish Redux Template For VSCode](https://github.com/huangjianke/fish-redux-template), by huangjianke. ### Dev-Tools - Redux Inspector (using [Flutter Debugger](https://github.com/blankapp/flutter-debugger) and [flipperkit_fish_redux_middleware](https://pub.dartlang.org/packages/flipperkit_fish_redux_middleware)) for Fish Redux apps, by [JianyingLi](https://github.com/lijy91) ## License - Fish Redux is released under the Apache 2.0 license. See [LICENSE](LICENSE) for details. ## 关于我们 阿里巴巴-闲鱼技术是国内最早也是最大规模线上运行Flutter的团队。 我们在公众号中为你精选了Flutter独家干货,全面而深入。 内容包括:Flutter的接入、规模化应用、引擎探秘、工程体系、创新技术等教程和开源信息。 **架构/服务端/客户端/前端/质量工程师 在公众号中投递简历,名额不限哦** 欢迎来闲鱼做一个好奇、幸福、有影响力的程序员,简历投递:tino.wjf@alibaba-inc.com 订阅地址 [For English](https://twitter.com/xianyutech "For English") ================================================ FILE: docs/_navbar.md ================================================ * [En](/) * [中文](/zh-cn/) ================================================ FILE: docs/_sidebar.md ================================================ * [Redux](concept/redux.md) * [Action](concept/action.md) * [Connector](concept/connector.md) * [Reducer](concept/reducer.md) * [Middleware](concept/middleware.md) * [Component](concept/component.md) * [View](concept/view.md) * [Reducer](concept/reducer.md) * [Effect](concept/effect.md) * [HigherEffect](concept/higher-effect.md) * [Lifecycle](concept/lifecycle.md) * [Dependencies](concept/dependencies.md) * [Dependent](concept/dependent.md) * [ShouldUpdate](concept/should-update.md) * [OnError](concept/on-error.md) * [Filter](concept/filter.md) * [OOP](concept/oop.md) * [WidgetWrapper](concept/widget-wrapper.md) * [Page](concept/page.md) * [Adapter](concept/adapter.md) * [StaticFlowAdapter](concept/static-flow-adapter.md) * [DynamicFlowAdapter](concept/dynamic-flow-adapter.md) * [CustomAdapter](concept/custom-adapter.md) * [What's the difference between 'Fish Redux' and 'Redux' ?](concept/what's-the-diiference.md) * [What's-adapter](concept/what's-adapter.md) * [What's-connector](concept/what's-connector.md) * [Mechanism](concept/mechanism.md) * [Directory](concept/directory.md) ================================================ FILE: docs/concept/_navbar.md ================================================ * [En](/) * [中文](/zh-cn/) ================================================ FILE: docs/concept/_sidebar.md ================================================ * [Redux](concept/redux.md) * [Action](concept/action.md) * [Connector](concept/connector.md) * [Reducer](concept/reducer.md) * [Middleware](concept/middleware.md) * [Component](concept/component.md) * [View](concept/view.md) * [Reducer](concept/reducer.md) * [Effect](concept/effect.md) * [HigherEffect](concept/higher-effect.md) * [Lifecycle](concept/lifecycle.md) * [Dependencies](concept/dependencies.md) * [Dependent](concept/dependent.md) * [ShouldUpdate](concept/should-update.md) * [OnError](concept/on-error.md) * [Filter](concept/filter.md) * [OOP](concept/oop.md) * [WidgetWrapper](concept/widget-wrapper.md) * [Page](concept/page.md) * [Adapter](concept/adapter.md) * [StaticFlowAdapter](concept/static-flow-adapter.md) * [DynamicFlowAdapter](concept/dynamic-flow-adapter.md) * [CustomAdapter](concept/custom-adapter.md) * [What's the difference between 'Fish Redux' and 'Redux' ?](concept/what's-the-diiference.md) * [What's-adapter](concept/what's-adapter.md) * [What's-connector](concept/what's-connector.md) * [Mechanism](concept/mechanism.md) * [Directory](concept/directory.md) ================================================ FILE: docs/concept/action.md ================================================ # Action - Action contains two fields - type - payload - Recommended way of writing action - Create an action.dart file for a component|adapter that contains two classes - An enumeration class for the type field - An ActionCreator class is created for the creator of the Action, which helps to constrain the type of payload. - Effect Accepted Action which's type is named after `on{verb}` - Reducer Accepted Action which's type is named after `{verb}` - Sample code ```dart enum MessageAction { onShare, shared, } class MessageActionCreator { static Action onShare(Map payload) { return Action(MessageAction.onShare, payload: payload); } static Action shared() { return const Action(MessageAction.shared); } } ``` ================================================ FILE: docs/concept/adapter.md ================================================ # Adapter - In addition to the concept of the underlying Component, we have added a componentized abstract Adapter. Its goal is to solve the 3 problems of the Component model in the ListView scene. - 1)Putting a "Big-Cell" in the ListView does not enjoy the performance optimization of the ListView code. - 2)Component cannot distinguish between the appear|disappear and init|dispose events. - 3)The life cycle of the Effect and the coupling of the View do not meet the intuitive expectations in some scenes of the ListView. - An Adapter and a Component are almost identical except for the following points - Component generates a Widget, Adapter generates a ListAdapter, and ListAdapter has the ability to generate a list of Widgets.。 - Not specifically generating a Widget but a ListAdapter can greatly improve the page frame rate and fluency. - Effect-Lifecycle-Promote - The Effect of Component follows the life cycle of the Widget, and the Adapter's Effect follows the life cycle of the parent Widget. - The improvement of the life cycle of the effect greatly removes the coupling between the business logic and the view life. Even if its display has not yet appeared, other modules can still call its capabilities through dispatch-api. - Appearance|disappear event notification - As the Effect lifecycle improves, we can more closely distinguish between init|dispose and appear|disappear. This is indistinguishable from the Model's model. - Reducer is long-lived, Effect is medium-lived, View is short-lived. - Three implementations of Adapter - [DynamicFlowAdapter](dynamic-flow-adapter.md) - [StaticFlowAdapter](static-flow-adapter.md) - [CustomAdapter](custom-adapter.md) ================================================ FILE: docs/concept/auto-dispose.md ================================================ # Auto-Dispose - AutoDispose is a very simple way to manage lifecycle objects. An auto-dispose object can be released on its own initiative or released when the managed object it follows is released. - The Context used in Effect and the EffectPart in HigherEffect are auto-dispose objects. So we can easily host custom objects that need to be managed for lifecycle management. - Sample Code ```dart class ItemWidgetBindingObserver extends WidgetsBindingObserver with AutoDispose { ItemWidgetBindingObserver() : super() { WidgetsBinding.instance.addObserver(this); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (AppConfig.flutterBinding.framesEnabled && state == AppLifecycleState.resumed) { AppConfig.flutterBinding.performReassemble(); } } @override void dispose() { super.dispose(); WidgetsBinding.instance.removeObserver(this); } } void _init(Action action, Context ctx) { final ItemWidgetBindingObserver observer = ItemWidgetBindingObserver(); observer.follow(ctx); } ``` ================================================ FILE: docs/concept/component.md ================================================ # Component Component is the encapsulation of view presentation and logic functions. For the moment, from the perspective of Redux, we divide the component into state-manage functions (Reducers) and others. Looking to the future, from the perspective of UI-Automation, we divide the component into presentations and others. Combining the above two perspectives, we got the three parts of View, SideEffect, and Reducer, which are called the three factors of the component. We use explicit configuration to complete the registration of components and adapters on which large component depend. This dependency configuration is called Dependencies. So with this formula: Component = View + Effect(Optional) + Reducer(Optional) + Dependencies(Optional) Division: From the perspective of the component Concentration: From the perspective of the Store ================================================ FILE: docs/concept/connector.md ================================================ # Connector - It expresses a data connection relationship of how to read small data from a big data, and how to synchronize to big data when the small data is modified。 - It is the key to a centralized Reducer that can be assembled automatically by a multi-level, multi-module, small Reducer - It greatly reduces the difficulty of using Redux. We no longer care about the assembly process, we care about what specific actions cause the state to change. - It is used in the configuration Dependencies, in the configuration we have solidified the connection between the large component and the small component, so we do not need to pass in any dynamic parameters when we use the small component. - ![image.png | left | 719x375](https://cdn.nlark.com/lark/0/2018/png/82574/1545365202743-01074be7-f067-45c7-aae0-91b12cd50ae6.png) - Sample Code ```dart class DetialState { Profile profile; String message; } ConnOp messageConnector() { return ConnOp( get: (DetialState state) => state.message, set: (DetialState state, String message) => state.message = message, ); } ``` ================================================ FILE: docs/concept/custom-adapter.md ================================================ # CustomAdapter - Custom implementation of large Cell in LisView. - The Factors of the Adapter are similar to the Component's. The difference is that the view part of the Adapter returns a ListAdapter. - Sample Code ```dart class CommentAdapter extends Adapter { CommentAdapter() : super( adapter: buildCommentAdapter, effect: buildCommentEffect(), reducer: buildCommentReducer(), ); } ListAdapter buildCommentAdapter(CommentState state, Dispatch dispatch, ViewService service) { final List builders = Collections.compact([] ..add((BuildContext buildContext, int index) => _buildDetailCommentHeader(state, dispatch, service)) ..addAll(_buildCommentViewList(state, dispatch, service)) ..add(isEmpty(state.commentListRes?.items) ? (BuildContext buildContext, int index) => _buildDetailCommentEmpty(state.itemInfo, dispatch) : null) ..add(state.commentListRes?.getHasMore() == true ? (BuildContext buildContext, int index) => _buildLoadMore(dispatch) : null)); return ListAdapter( (BuildContext buildContext, int index) => builders[index](buildContext, index), builders.length, ); } ///builds ``` ================================================ FILE: docs/concept/dependencies.md ================================================ # Dependencies - Dependencies is a structure that expresses dependencies between components. It accepts two fields - slots - {} - [adapter](adapter.md) - It mainly contains three aspects of information - The slots that the component depends on. - The adapter that the component depends on (used to build a high-performance ListView). - [Dependent](dependent.md) Is a combination of subComponent | subAdapter + [connector](connector.md)。 - A component's [Reducer](reducer.md) is automatically compounded by the Reducer configured by the Component itself and all of the Reducers under its Dependencies. - Sample Code ```dart ///register in component class ItemComponent extends ItemComponent { ItemComponent() : super( view: buildItemView, reducer: buildItemReducer(), dependencies: Dependencies( slots: >{ 'appBar': AppBarComponent().asDependent(AppBarConnector()), 'body': ItemBodyComponent().asDependent(ItemBodyConnector()), 'ad_ball': ADBallComponent().asDependent(ADBallConnector()), 'bottomBar': BottomBarComponent().asDependent(BottomBarConnector()), }, ), ); } ///call in view Widget buildItemView(ItemState state, Dispatch dispatch, ViewService service) { return Scaffold( body: Stack( children: [ service.buildComponent('body'), service.buildComponent('ad_ball'), Positioned( child: service.buildComponent('bottomBar'), left: 0.0, bottom: 0.0, right: 0.0, height: 100.0, ), ], ), appBar: AppbarPreferSize(child: service.buildComponent('appBar'))); } ``` ================================================ FILE: docs/concept/dependent.md ================================================ ### Dependent - Dependent = connector + subComponent | subAdapter - It expresses how the small component or adapter are connected to it's parent component. - Sample Code ```dart /// todo ``` ================================================ FILE: docs/concept/directory.md ================================================ # Directory The recommended directory structure ``` sample_page -- action.dart /// define action types and action creator -- page.dart /// config a page or component -- view.dart /// define a function which expresses the presentation of user interface -- effect.dart /// define a function which handles the side-effect -- reducer.dart /// define a function which handles state-change -- state.dart /// define a state and some connector of substate components sample_component -- action.dart -- component.dart -- view.dart -- effect.dart -- reducer.dart -- state.dart ``` The upper layer is responsible for assembly and the lower layer is responsible for implementation. ================================================ FILE: docs/concept/dynamic-flow-adapter.md ================================================ # DynamicFlowAdapter - The template is a Map that accepts an array-like data driven - Sample Code ```dart class RecommendAdapter extends DynamicFlowAdapter { RecommendAdapter() : super( pool: >{ 'card_0': RecommendTitleComponent(), 'card_1': RecommendRowComponent(), }, connector: RecommendCardListConnector(), ); } ``` ================================================ FILE: docs/concept/effect.md ================================================ # Effect - Effect is a function that handles all side effects. It receives the following parameters - Action action - Context context - BuildContext context - T state - dispatch - isDisposed - It mainly contains four aspects of information - Receive "intent" from the View, including the corresponding lifecycle callback, and then make specific execution. - Its processing may be an asynchronous function, the data may be changed in the process, so we should get the latest data through context.state. - If you want to modify the data, you should send an Action to the Reducer to handle. It is read-only for data and cannot be modified directly in a effect function. - If its return value is a non-null value, it will take precedence for itself and will not do the next step; otherwise it will broadcast to the Effect part of other components and sent the action to the Reducer. > Self-First-Broadcast。 > ![image.png | left | 747x399](https://cdn.nlark.com/lark/0/2018/png/82574/1545365233153-4c8105b4-050c-49e6-be02-dbf28a861caa.png) - Sample Code ```dart /// one style of writing FutureOr sideEffect(Action action, Context ctx) async { if (action.type == Lifecycle.initState) { //do something on initState return true; } else if (action.type == 'onShare') { //do something on onShare await Future.delayed(Duration(milliseconds: 1000)); ctx.dispatch(const Action('shared')); return true; } return null; } class MessageComponent extends Component { MessageComponent(): super( view: buildMessageView, effect: sideEffect, ); } ``` ```dart /// another style of writing Effect buildEffect() { return combineEffects(>{ Lifecycle.initState: _initState, 'onShare': _onShare, }); } void _initState(Action action, Context ctx) { //do something on initState } void _onShare(Action action, Context ctx) async { //do something on onShare await Future.delayed(Duration(milliseconds: 1000)); ctx.dispatch(const Action('shared')); } class MessageComponent extends Component { MessageComponent(): super( view: buildMessageView, effect: buildEffect(), ); } ``` ================================================ FILE: docs/concept/evolution-of-fish-redux.md ================================================ # fish-redux 的演进史 fish-redux 是一个不断演进的框架,甚至是在不断的回炉重造,在这个过程中 - 1. 第一个版本是基于社区内的 flutter_redux 进行的改造,核心是提供了 UI 代码的组件化,当然问题也非常明显,针对复杂的业务场景,往往业务逻辑很多,无法做到逻辑代码的分治和复用。 - 2. 第二个版本针对第一个版本的问题,做出了比较重大的修改,解决了 UI 代码和逻辑代码的分治问题,但设计上打破了 redux 的原则,丢失了 Redux 的精华。 - 3. 在第三个版本进行重构时,我们确立了整体的架构原则与分层要求,一方面按照 reduxjs 的代码进行了 flutter 侧的 redux 实现,将 redux 完整保留下来。另一方面针对组件化的问题,提供了 redux 之上的 component 的封装,并创新的通过这一层的架构设计提供了业务代码分治的能力。第三版 完成了 Redux, Component 两层的设计,其中包含了 Connector,Dependencies,Context 等重要概念。 - 3.1 解决集中和分治的矛盾的核心在于 [Connector](what's-connector.md) - 3.2 这一层的组件的分治是面向通用设计的。通过在 [Dependencies](dependencies-cn.md) 配置 slots,得到了可插拔的组件系统。 - 4. 在第三个版本 Redux & Component 之外,提供了面向 ListView 场景的分治设计 Adapter。 - 解决了在面向 ListView 场景下的逻辑的分治和性能降低的矛盾。 - [what's-adapter](what's-adapter.md) > 目前,fish redux 已经在闲鱼线上稳定运行,未来,期待 fish redux 给社区带来更多的输入。 ================================================ FILE: docs/concept/features.md ================================================ # Features ## 直接使用 flutter 会面临的问题? > [flutter](https://github.com/flutter/flutter) 是 google 推出的新一代跨平台渲染框架. > 它帮助开发者解决了跨平台,高性能,富有表现力和灵活的 UI 表达,快速开发等核心问题。 > 但是如果开发大应用,还需要解决以下问题。 > > > 1. 数据流问题 > > 2. 通信问题 > > 3. 可插拔的组件系统 > > 4. 展示和逻辑解耦 > > 5. 统一的编程模型和规范 > > 我们可以类比 flutter 和 React,事实上在中大型应用中 React 会面临的绝大多数问题,flutter 也同样面临考验。 ## 数据流问题 > 目前社区流行的数据流方案有: > 单向数据流方案,以 Redux 为代表 > 响应式数据流方案,以 Mobx 为代表 > 其他,以 rxjs 为代表 > 那么哪一种架构最合适 flutter ? > 我们追随了 javascript 栈绝大多数开发者的选择 - [ReduxJs](https://github.com/reduxjs/redux) > 感谢 ReduxJs,我们是几乎 100%的还原了它在 dart 上的实现。所以我们也继承了它的优点:[Predictable],[Centralized],[Debuggable],[Flexible]。 ## 通信问题 > 直接使用 flutter,在 Widgets 之间传递状态和回调,随着应用复杂度的上升,会变成是一件可怕而糟糕的事情。 > 通过 fish redux,依托于集中的 Redux 和分治的 Effect 模块,通过一个极简的 [dispatch-api](mechanism.md),完成所有的通信的诉求。 ## 可插拔的组件系统 > fish redux 通过一个配置式的 Dependencies,来完成灵活的可插拔的组件系统。同时有这一配置的存在,它解放了我们手动拼装 Reducer 的繁琐工作。 > 参考: > > > 1. [what's-connector](what's-connector.md) > > 2. [connector](connector.md) > > 3. [dependencies](dependencies.md) > > 4. [component](component.md) > > 5. [adapter](adapter.md) > > 6. [what's-adapter](what's-adapter.md) ## 展示和逻辑解耦 > fish redux 从 [elm](https://guide.elm-lang.org/) 中得到了非常多的设计灵感。 > 将一个组件,拆分为相互独立的 View,Effect,Reducer 三个函数,我们优雅的解决了展示和逻辑解耦的问题。 > 通过这样的拆分,我们将 UI 的表达隔离于一个函数内,它让我们更好的面向未来,一份 UI 表达它可能来自于开发者,可能来自于深度学习框架的 UI 代码生成,可能是面向移动终端,也可能是面向浏览器。它让我们有了更多的组合的可能。 > 同时函数式的编程模型带来了更容易编写,更容易扩展,更容易测试,更容易维护等特性。 ## 统一的编程模型和规范 > [directory](directory.md) ================================================ FILE: docs/concept/filter.md ================================================ # Filter - Filter is used to optimize the performance of the Reducer. Because the Reducer is layer-assembled, each Action is processed, and in theory, all the small Reducers are traversed. In some very complicated scenarios, such a deep traversal may take up to the millisecond level (generally Should be less than 1 millisecond). Then we need to optimize the performance of the Reducer, decide in advance whether to traverse this Reducer subtree, reduce the depth and number of traversal. - Sample Code ```dart bool filter(Action action) { return action.type == 'some action'; } ``` ================================================ FILE: docs/concept/higher-effect.md ================================================ # HigherEffect - Since Effect may have some temporary state of its own (although it is not recommended, support is provided), in order to support this feature, we promote the Effect to a higher-order function and put its state in the closure. - The framework supports the configuration of Effect|HigherEffect on component or adapter, but it can't be configured for one component or adapter at the same time, which will cause trouble. In general, the configuration is often an explicit negligence. - HigherEffect = (Context ctx) => (Action action) => FutureOr - For more detailed examples, please refer to [OOP](oop.md) - EffectPart ================================================ FILE: docs/concept/lifecycle.md ================================================ # Lifecycle - The default all lifecycles are essentially derived from the lifecycle in flutter State. - initState - didChangeDependencies - build - didUpdateWidget - deactivate - dispose - Within the component, the Lifecycle of the Reducer is consistent with the page, and the lifecycle of Effect and View is consistent with the component's Widget. - In the adapter, the Lifecycle of the Reducer is consistent with the page. The life cycle of the Effect is the same as the life cycle of the ListView. The life cycle of the View is short-lived (destroyed in the invisible area). At the same time, the life cycle of appear and disappear is added, representing the view array managed by this adapter, the callback just entering the display area and completely leaving the display area. ================================================ FILE: docs/concept/mechanism.md ================================================ # Communication Mechanism ## Page internal communication - Component internal communication - Inter-component communication ![image.png | left | 747x399](https://cdn.nlark.com/lark/0/2018/png/82574/1545365233153-4c8105b4-050c-49e6-be02-dbf28a861caa.png) Self-First-Broadcast。 The emitted Action will be processed first by its own Effect, otherwise it will be broadcast to other components and Redux. We completed the communication between the components (parent to child, child to parent, brother, etc.) through a simple and intuitive dispatch. ## Inter-page communication - Context.appBroadcast - Each page's PageStore receives an action which is handled independently. ![image.png | left | 691x519](https://cdn.nlark.com/lark/0/2018/png/82574/1545368705599-745c46a3-f5c6-41a7-a757-1bc6f9a389d4.png) # Refresh Mechanism ## 数据刷新 - Local data modification automatically triggers a shallow copy of the upper layer data and is transparent to the business code. ![image.png | left | 747x361](https://cdn.nlark.com/lark/0/2018/png/82574/1545386668521-0081cb5f-8017-47d1-ad7c-8802bb0be8a0.png) ## View refresh - When the state changes, the store flatly notifies all the components and the [ShouldUpdate](should-update.md) decide whether the view should be refreshed ![image.png | left | 747x336](https://cdn.nlark.com/lark/0/2018/png/82574/1545386773247-2eddfa99-e6b9-4be9-ac43-d1944ff44e9b.png) ================================================ FILE: docs/concept/middleware-cn.md ================================================ # Middleware 关于 Middleware 的定义、签名和 ReduxJS 社区是一致的。 示例代码 ```dart Middleware logMiddleware({ String tag = 'redux', String Function(T) monitor, }) { return ({Dispatch dispatch, Get getState}) { return (Dispatch next) { return isDebug() ? (Action action) { print('---------- [$tag] ----------'); print('[$tag] ${action.type} ${action.payload}'); final T prevState = getState(); if (monitor != null) { print('[$tag] prev-state: ${monitor(prevState)}'); } next(action); final T nextState = getState(); if (monitor != null) { print('[$tag] next-state: ${monitor(nextState)}'); } if (prevState == nextState) { print('[$tag] warning: ${action.type} has not been used.'); } print('========== [$tag] ================'); } : next; }; }; } ``` 更多的参考 src/utils/common_middleware ================================================ FILE: docs/concept/middleware.md ================================================ # Middleware - The definition and signature of Middleware is consistent with the ReduxJS community. - Sample Code ```dart Middleware logMiddleware({ String tag = 'redux', String Function(T) monitor, }) { return ({Dispatch dispatch, Get getState}) { return (Dispatch next) { return isDebug() ? (Action action) { print('---------- [$tag] ----------'); print('[$tag] ${action.type} ${action.payload}'); final T prevState = getState(); if (monitor != null) { print('[$tag] prev-state: ${monitor(prevState)}'); } next(action); final T nextState = getState(); if (monitor != null) { print('[$tag] next-state: ${monitor(nextState)}'); } if (prevState == nextState) { print('[$tag] warning: ${action.type} has not been used.'); } print('========== [$tag] ================'); } : next; }; }; } ``` 更多的参考 src/utils/common_middleware ================================================ FILE: docs/concept/on-error.md ================================================ # OnError - Centralizes the business exceptions generated by Effect, whether it is a synchronous function or an asynchronous function. With a unified exception handling mechanism, we can stand on a higher level of abstraction and make reasonable simplifications of business code. - Sample Code ```dart bool onMessageError(Exception e, Context ctx) { if(e is BizException) { ///do some toast return true; } return false; } class MessageComponent extends Component { MessageComponent(): super( view: buildMessageView, effect: buildEffect(), reducer: buildMessageReducer(), onError: onMessageError, ); } ``` ================================================ FILE: docs/concept/oop.md ================================================ # OOP - Although the framework recommends the use of functional programming, it also provides object-oriented programming support. - ViewPart - Need to override the 'build' function. - The required state, dispatch, and viewService parameters have become fields of the object and can be used directly. - It is immutable, so there should be no need to define variable fields internally. - EffectPart - Need to override the 'createMap' function. - The required Context has been flattened as the fields which can be used directly. - Fields can be defined and their visibility is limited to themselves. - It must be used with higherEffect. - Sample Code ```dart class MessageView extends ViewPart { @override Widget build() { return Column(children: [ viewService.buildComponent('profile'), InkWell( child: Text('$message'), onTap: () => dispatch(const Action('onShare')), ), ]); } } class MessageEffect extends EffectPart { ///we could put some Non-UI fields here. @override Map createMap() { return { Lifecycle.initState: _initState, 'onShare': _onShare, }; } void _initState(Action action) { //do something on initState } void _onShare(Action action) async { //do something on onShare await Future.delayed(Duration(milliseconds: 1000)); dispatch(const Action('shared')); } } class MessageComponent extends Component { MessageComponent(): super( view: MessageView().asView(), higherEffect: higherEffect(() => MessageEffect()), ); } ``` ================================================ FILE: docs/concept/page.md ================================================ # Page - One and only one store in one page - Page inherits from Component, so it can configure all the factors of Component. - Page can configure Middleware for AOP management of Redux. - Page must be configured with an initialization function that initializes page data initState. - Sample Code ```dart /// Hello World class HelloWordPage extends Page { HelloWordPage(): super( initState: (String msg) => msg, view:(String msg, _, __) => Text('Hello ${msg}'), ); } HelloWordPage().buildPage('world') ``` ================================================ FILE: docs/concept/reducer.md ================================================ # Reducer - The Reducer is a context-independent pure function. It receives the following parameters - T state - Action action - It mainly contains three aspects of information - Receive an "intent" and make a state modification. - If you want to modify the state, you need to create a new copy and modify it on the copy. - If the small state is modified, it will automatically trigger the copy of the main state's layers data, and then notify the components to refresh in a flattened manner. - Sample Code ```dart /// one style of writing String messageReducer(String msg, Action action) { if (action.type == 'shared') { return '$msg [shared]'; } return msg; } class MessageComponent extends Component { MessageComponent(): super( view: buildMessageView, effect: buildEffect(), reducer: messageReducer, ); } ``` ```dart /// another style of writing Reducer buildMessageReducer() { return asReducer(>{ 'shared': _shared, }); } String _shared(String msg, Action action) { return '$msg [shared]'; } class MessageComponent extends Component { MessageComponent(): super( view: buildMessageView, effect: buildEffect(), reducer: buildMessageReducer(), ); } ``` > 推荐的是第二种写法 ================================================ FILE: docs/concept/redux.md ================================================ ### Redux - State - [Action](action.md) - [Reducer](reducer.md) - Store - [Middleware](middleware.md) The above concepts are exactly the same as the community's Redux. Redux is a framework for state management with [predictable][centralized] [easy to debug][flexibility]. If you want to have a closer understanding of Redux, please refer to [https://github.com/reduxjs/redux](https://github.com/reduxjs/redux) ================================================ FILE: docs/concept/should-update.md ================================================ # ShouldUpdate - When the state changes, the store flatly notifies all the components. - By default, the framework uses identical to compare the old and new state to determine if a refresh is needed. - If we have a very precise request for component refresh, then we can define a ShouldUpdate ourselves. - Sample Code ```dart bool shouldUpdate(DetailState old, DetailState now) { return old.message != now.message; } ``` ================================================ FILE: docs/concept/static-flow-adapter.md ================================================ # StaticFlowAdapter - The template is an Array that accepts map-like data driven. - The template receives an array of Dependents. - It's similar with a flatMap + compact operation abstractly. - Sample Code ```dart class ItemBodyComponent extends Component { ItemBodyComponent() : super( view: buildItemBody, dependencies: Dependencies( adapter: StaticFlowAdapter( slots: >[ VideoAdapter().asDependent(videoConnector()), UserInfoComponent().asDependent(userInfoConnector()), DescComponent().asDependent(descConnector()), ItemImageComponent().asDependent(itemImageConnector()), OriginDescComponent().asDependent(originDescConnector()), VisitComponent().asDependent(visitConnector()), SameMoreComponent().asDependent(sameMoreConnector()), PondComponent().asDependent(pondConnector()), CommentAdapter().asDependent(commentConnector()), RecommendAdapter().asDependent(recommendConnector()), PaddingComponent().asDependent(paddingConnector()), ]), ), ); } ``` ================================================ FILE: docs/concept/view.md ================================================ # View - View is a context-independent function that outputs Widget. It receives the following parameters - T state - Dispatch - ViewService - It mainly contains three aspects of information - The view is completely driven by data. - The event/callback triggered by the view, use Dispatch to send "intent", but never do a specific implementation. - Use dependent component/adapter, by explicitly configuring it on the parent component, and then standardizing calls through the ViewService. - Where ViewService provides three capabilities - BuildContext context: Ability to get widget's BuildContext - Widget buildView(String name): Ability to create subcomponents directly - The name passed in here is the name configured on Dependencies. - Creating subcomponents does not require passing in any other parameters, since the parameters required by the subcomponents have been passed through the Dependencies configuration, and their data relationships are established via the connector. - ListAdapter buildAdapter(): Ability to create adapter directly - Sample Code ```dart Widget buildMessageView(String message, Dispatch dispatch, ViewService viewService) { return Column(children: [ viewService.buildComponent('profile'), InkWell( child: Text('$message'), onTap: () => dispatch(const Action('onShare')), ), ]); } class MessageComponent extends Component { MessageComponent(): super( view: buildMessageView, ); } ``` ================================================ FILE: docs/concept/what's-adapter.md ================================================ # What's adapter 面向 ListView 场景的分治设计 Adapter。 > 在解答什么是 adapter 之前,我们来看下一般框架对 ListView 的分治是怎么做的。传统的手段,我们对 ListView 的分治更多的局限于它展现部分,而它的逻辑部分往往是集中的。而当我们试图将 ListView 下的某一局部的展现和逻辑封装在一起,我们就会遇到"Big-Cell"问题,面临性能的显著降低。 > 这里面存在一个分治和性能上的矛盾。这个矛盾带来了复用难,可维护差,难以协作等中大型场景下的问题。 > > 解决这个问题,有两种思路: > > 1. 下沉到 UI 表达层(Widgets),去实现一个高性能的 ScrollView。 > 2. 向上做模型抽象,得到一个逻辑上的 ScrollView,性能上的 ListView。 > > fish redux 选择了第二条更加通用的路径来解决 LisView 下的分治问题。 > > 一个 ListView 对应了一个 Adapter,这看上去非常的像 Android 里的设计,但事实上 fish-redux 里的 Adapter 概念走的更远。 > > 1. 一个 Adapter 是可以由多个 Component 和 Adapter 组合而成,它有点像 flatmap & compact 的 api 的叠加。 > 2. Adapter 以及它的子 Adapter 的生命周期是和 ListView 是等效的。它像跨斗一般附着于 ListView 的生命周期之上。同时由于 Adapter 生命周期的提升,我们额外收获了两个非常有用的事件消息(appear & disappear)。 > > > 注意 ⚠️ 在 Adapter 里配置的子 Component,它的生命周期和它所对应的 WidgetState 是一致的,所以它的是短暂的。 - Adapter 的容器有两类,用图来说明吧: ================================================ FILE: docs/concept/what's-connector.md ================================================ # What's connector 在解答 connector 是什么之前,我们来先看一个代码片段 ```javascript let hasChanged = false; const nextState = {}; for (let i = 0; i < finalReducerKeys.length; i++) { const key = finalReducerKeys[i]; const reducer = finalReducers[key]; const previousStateForKey = state[key]; const nextStateForKey = reducer(previousStateForKey, action); if (typeof nextStateForKey === 'undefined') { const errorMessage = getUndefinedStateErrorMessage(key, action); throw new Error(errorMessage); } nextState[key] = nextStateForKey; hasChanged = hasChanged || nextStateForKey !== previousStateForKey; } return hasChanged ? nextState : state; ``` 以上来自于 Reduxjs-[combineReducers](https://github.com/reduxjs/redux/blob/master/src/combineReducers.js)的核心实现。 combineReducers 是一个将 Reducer 分治的函数,让一个庞大数据的 Reducer 可以由多层的更小的 Reducer 组合而成。 这是 Redux 框架里的核心 API,但是他有缺点。他有非常明显的语言的局限性,如下 3 点: 1. 浅拷贝一个任意对象 ```javascript const nextState = {}; ``` 2. 读取字段 ```javascript const previousStateForKey = state[key]; ``` 3. 写入字段 ```javascript nextState[key] = nextStateForKey; ``` 将上面的 3 点抽象来看: 1. State 的 clone 的能力(浅拷贝) 2. Get & Set 的能力,即为 Connector 的概念。 有了 以上两点,我们才完全集成了 Redux 的所有精华,同时将它的设计更上一个通用的维度。 ================================================ FILE: docs/concept/what's-the-diiference.md ================================================ # What's different with Redux ? ## They are two frameworks for solving problems at different layers. > Redux is a framework focused on state management; Fish Redux is an application framework based on Redux for state management. > The application framework not only solves the problem of state management, but also solves the problems of divide and conquer, communication, data drive, decoupling and so on. ## Fish Redux solves the contradiction between concentration and division. > Redux completes the merge process from the small Reducers to the main Reducer by the user manually organizing the code; > Fish Redux automatically completes the merge process from the small Reducers to the main Reducer by explicitly expressing the dependencies between components; ## Fish Redux provides a simple component abstract model > It is a combination of simple 3 functions ## Fish Redux provides an abstract component model of the Adapter > In addition to the underlying component model, Fish Redux provides an Adapter abstraction model to solve the performance problems of large cells on ListView. > Through the upper abstraction, we get the logical ScrollView, the performance of the ListView. ================================================ FILE: docs/concept/widget-wrapper.md ================================================ ### WidgetWrapper - It is used to solve flutter's ui system, some Widgets that need to implement the featured interface, such as KeepAlive, because the Widget generated by Component will be wrapped by a Stateful Widget inside the fish redux framework. - Sample Code ```dart import 'package:flutter/material.dart' hide Action; Widget repaintBoundaryWrapper(Widget widget) { return RepaintBoundary(child: widget); } ``` ================================================ FILE: docs/index.html ================================================ Document
================================================ FILE: docs/zh-cn/README.md ================================================ - **核心概念** - [Redux](/zh-cn/concept/redux.md) - [Action](/zh-cn/concept/action.md) - [Connector](/zh-cn/concept/connector.md) - [Reducer](/zh-cn/concept/reducer.md) - [Middleware](/zh-cn/concept/middleware.md) - [Component](/zh-cn/concept/component.md) - [View](/zh-cn/concept/view.md) - [Reducer](/zh-cn/concept/reducer.md) - [Effect](/zh-cn/concept/effect.md) - [HigherEffect](/zh-cn/concept/higher-effect.md) - [Lifecycle](/zh-cn/concept/lifecycle.md) - [Dependencies](/zh-cn/concept/dependencies.md) - [Dependent](/zh-cn/concept/dependent.md) - [ShouldUpdate](/zh-cn/concept/should-update.md) - [OnError](/zh-cn/concept/on-error.md) - [Filter](/zh-cn/concept/filter.md) - [OOP](/zh-cn/concept/oop.md) - [WidgetWrapper](/zh-cn/concept/widget-wrapper.md) - [Page](/zh-cn/concept/page.md) - [Adapter](/zh-cn/concept/adapter.md) - [StaticFlowAdapter](/zh-cn/concept/static-flow-adapter.md) - [DynamicFlowAdapter](/zh-cn/concept/dynamic-flow-adapter.md) - [CustomAdapter](/zh-cn/concept/custom-adapter.md) - **其他** - [What's the difference between 'Fish Redux' and 'Redux' ?](/zh-cn/concept/what's-the-diiference.md) - [What's-adapter](/zh-cn/concept/what's-adapter.md) - [What's-connector](/zh-cn/concept/what's-connector.md) - [Mechanism](/zh-cn/concept/mechanism.md) - [Directory](/zh-cn/concept/directory.md) ================================================ FILE: docs/zh-cn/_sidebar.md ================================================ * [Redux](zh-cn/concept/redux.md) * [Action](zh-cn/concept/action.md) * [Connector](zh-cn/concept/connector.md) * [Reducer](zh-cn/concept/reducer.md) * [Middleware](zh-cn/concept/middleware.md) * [Component](zh-cn/concept/component.md) * [View](zh-cn/concept/view.md) * [Reducer](zh-cn/concept/reducer.md) * [Effect](zh-cn/concept/effect.md) * [HigherEffect](zh-cn/concept/higher-effect.md) * [Lifecycle](zh-cn/concept/lifecycle.md) * [Dependencies](zh-cn/concept/dependencies.md) * [Dependent](zh-cn/concept/dependent.md) * [ShouldUpdate](zh-cn/concept/should-update.md) * [OnError](zh-cn/concept/on-error.md) * [Filter](zh-cn/concept/filter.md) * [OOP](zh-cn/concept/oop.md) * [WidgetWrapper](zh-cn/concept/widget-wrapper.md) * [Page](zh-cn/concept/page.md) * [Adapter](zh-cn/concept/adapter.md) * [StaticFlowAdapter](zh-cn/concept/static-flow-adapter.md) * [DynamicFlowAdapter](zh-cn/concept/dynamic-flow-adapter.md) * [CustomAdapter](zh-cn/concept/custom-adapter.md) * [What's the difference between 'Fish Redux' and 'Redux' ?](zh-cn/concept/what's-the-diiference.md) * [What's-adapter](zh-cn/concept/what's-adapter.md) * [What's-connector](zh-cn/concept/what's-connector.md) * [Mechanism](zh-cn/concept/mechanism.md) * [Directory](zh-cn/concept/directory.md) ================================================ FILE: docs/zh-cn/concept/_navbar.md ================================================ * [En](/) * [中文](/zh-cn/) ================================================ FILE: docs/zh-cn/concept/_sidebar.md ================================================ * [Redux](zh-cn/concept/redux.md) * [Action](zh-cn/concept/action.md) * [Connector](zh-cn/concept/connector.md) * [Reducer](zh-cn/concept/reducer.md) * [Middleware](zh-cn/concept/middleware.md) * [Component](zh-cn/concept/component.md) * [View](zh-cn/concept/view.md) * [Reducer](zh-cn/concept/reducer.md) * [Effect](zh-cn/concept/effect.md) * [HigherEffect](zh-cn/concept/higher-effect.md) * [Lifecycle](zh-cn/concept/lifecycle.md) * [Dependencies](zh-cn/concept/dependencies.md) * [Dependent](zh-cn/concept/dependent.md) * [ShouldUpdate](zh-cn/concept/should-update.md) * [OnError](zh-cn/concept/on-error.md) * [Filter](zh-cn/concept/filter.md) * [OOP](zh-cn/concept/oop.md) * [WidgetWrapper](zh-cn/concept/widget-wrapper.md) * [Page](zh-cn/concept/page.md) * [Adapter](zh-cn/concept/adapter.md) * [StaticFlowAdapter](zh-cn/concept/static-flow-adapter.md) * [DynamicFlowAdapter](zh-cn/concept/dynamic-flow-adapter.md) * [CustomAdapter](zh-cn/concept/custom-adapter.md) * [What's the difference between 'Fish Redux' and 'Redux' ?](zh-cn/concept/what's-the-diiference.md) * [What's-adapter](zh-cn/concept/what's-adapter.md) * [What's-connector](zh-cn/concept/what's-connector.md) * [Mechanism](zh-cn/concept/mechanism.md) * [Directory](zh-cn/concept/directory.md) ================================================ FILE: docs/zh-cn/concept/action.md ================================================ # Action - Action 包含两个字段 - type - payload - 推荐的写法是 - 为一个组件|适配器创建一个 action.dart 文件,包含两个类 - 为 type 字段起一个枚举类 - 为 Action 的创建起一个 ActionCreator 类,这样利于约束 payload 的类型。 - Effect 接受处理的 Action,以 on{Verb} 命名 - Reducer 接受处理的 Action,以{verb} 命名 - 示例代码 ```dart enum MessageAction { onShare, shared, } class MessageActionCreator { static Action onShare(Map payload) { return Action(MessageAction.onShare, payload: payload); } static Action shared() { return const Action(MessageAction.shared); } } ``` ================================================ FILE: docs/zh-cn/concept/adapter.md ================================================ # Adapter - 我们在基础 Component 的概念外,额外增加了一种组件化的抽象 Adapter。它的目标是解决 Component 模型在 ListView 的场景下的 3 个问题 - 1)将一个"Big-Cell"放在 ListView 里,无法享受 ListView 代码的性能优化。 - 2)Component 无法区分 appear|disappear 和 init|dispose 事件。 - 3)Effect 的生命周期和 View 的耦合,在 ListView 的有些场景下不符合直观的预期。 - 一个 Adapter 和 Component 几乎都是一致的,除了以下几点 - Component 生成一个 Widget,Adapter 生成一个 ListAdapter,ListAdapter 有能力生成一组 Widget。 - 不具体生成 Widget,而是一个 ListAdapter,能非常大的提升页面帧率和流畅度。 - Effect-Lifecycle-Promote - Component 的 Effect 是跟着 Widget 的生命周期走的,Adapter 的 Effect 是跟着上一级的 Widget 的生命周期走。 - Effect​ 提升,极大的解除了业务逻辑和视图生命的耦合,即使它的展示还未出现,的其他模块依然能通过 dispatch-api,调用它的能力。 - appear|disappear 的通知 - 由于 Effect 生命周期的提升,我们就能更加精细的区分 init|dispose 和 appear|disappear。而这在 Component 的模型中是无法区分的。 - Reducer is long-lived, Effect is medium-lived, View is short-lived. - Adapter 的三种实现 - [DynamicFlowAdapter](dynamic-flow-adapter-cn.md) - [StaticFlowAdapter](static-flow-adapter-cn.md) - [CustomAdapter](custom-adapter-cn.md) ================================================ FILE: docs/zh-cn/concept/auto-dispose.md ================================================ # Auto-Dispose - 它是一个非常简易管理生命周期对象的方式。一个 auto-dispose 对象可以自我主动释放,或者在它 follow 的 托管对象释放的时候,释放。 - 在 Effect 中使用的 Context,以及 HigherEffect 中的 EffectPart,都是 auto-dispose 对象。所以我们可以方便的将自定义的需要做生命周期管理的对象托管给它们。 - 示例代码 ```dart class ItemWidgetBindingObserver extends WidgetsBindingObserver with AutoDispose { ItemWidgetBindingObserver() : super() { WidgetsBinding.instance.addObserver(this); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (AppConfig.flutterBinding.framesEnabled && state == AppLifecycleState.resumed) { AppConfig.flutterBinding.performReassemble(); } } @override void dispose() { super.dispose(); WidgetsBinding.instance.removeObserver(this); } } void _init(Action action, Context ctx) { final ItemWidgetBindingObserver observer = ItemWidgetBindingObserver(); observer.follow(ctx); } ``` ================================================ FILE: docs/zh-cn/concept/component.md ================================================ # Component 组件是对视图展现和逻辑功能的封装。 面向当下,从 Redux 的视角看,我们对组件分为状态修改的功能(Reducer)和其他。 面向未来,从 UI-Automation 的视角看,我们对组件分为展现表达和其他。 结合上面两个视角,于是我们得到了,View、 Effect、Reducer 三部分,称之为组件的三要素,分别负责了组件的展示、非修改数据的行为、修改数据的操作。 我们以显式配置的方式来完成大组件所依赖的小组件、适配器的注册,这份依赖配置称之为 Dependencies。 所以有了这个公式 Component = View + Effect(可选) + Reducer(可选) + Dependencies(可选) 分治:从组件的角度 集中:从 Store 的角度 ================================================ FILE: docs/zh-cn/concept/connector.md ================================================ # Connector - 它表达了如何从一个大数据中读取小数据,同时对小数据的修改如何同步给大数据,这样的数据连接关系。 - 它是将一个集中式的 Reducer,可以由多层次多模块的小 Reducer 自动拼装的关键。 - 它大大降低了我们使用 Redux 的复杂度。我们不再关心组装过程,我们关心的核心是什么动作促使数据怎么变化。 - 它使用在配置 Dependencies 中,在配置中我们就固化了大组件和小组件之间的连接关系(数据管道),所以在我们使用小组件的时候是不需要传入任何动态参数的。 - ![image.png | left | 719x375](https://cdn.nlark.com/lark/0/2018/png/82574/1545365202743-01074be7-f067-45c7-aae0-91b12cd50ae6.png) - Sample Code ```dart class DetialState { Profile profile; String message; } ConnOp messageConnector() { return ConnOp( get: (DetialState state) => state.message, set: (DetialState state, String message) => state.message = message, ); } ``` ================================================ FILE: docs/zh-cn/concept/custom-adapter.md ================================================ # CustomAdapter - 对大 Cell 的自定义实现 - 要素和 Component 类似,不一样的地方是 Adapter 的视图部分返回的是一个 ListAdapter - 示例代码 ```dart class CommentAdapter extends Adapter { CommentAdapter() : super( adapter: buildCommentAdapter, effect: buildCommentEffect(), reducer: buildCommentReducer(), ); } ListAdapter buildCommentAdapter(CommentState state, Dispatch dispatch, ViewService service) { final List builders = Collections.compact([] ..add((BuildContext buildContext, int index) => _buildDetailCommentHeader(state, dispatch, service)) ..addAll(_buildCommentViewList(state, dispatch, service)) ..add(isEmpty(state.commentListRes?.items) ? (BuildContext buildContext, int index) => _buildDetailCommentEmpty(state.itemInfo, dispatch) : null) ..add(state.commentListRes?.getHasMore() == true ? (BuildContext buildContext, int index) => _buildLoadMore(dispatch) : null)); return ListAdapter( (BuildContext buildContext, int index) => builders[index](buildContext, index), builders.length, ); } ///builds ``` ================================================ FILE: docs/zh-cn/concept/dependencies.md ================================================ # Dependencies - Dependencies 是一个表达组件之间依赖关系的结构。它接收两个字段 - slots - {} - [adapter](adapter-cn.md) - 它主要包含三方面的信息 - slots,组件依赖的插槽。 - adapter,组件依赖的具体适配器(用来构建高性能的 ListView)。 - [Dependent](dependent-cn.md) 是 subComponent | subAdapter + [connector](connector-cn.md) 的组合。 - 一个 组件的 [Reducer](reducer-cn.md) 由 Component 自身配置的 Reducer 和它的 Dependencies 下的所有子 Reducers 自动复合而成。 - 示例代码 ```dart ///register in component class ItemComponent extends ItemComponent { ItemComponent() : super( view: buildItemView, reducer: buildItemReducer(), dependencies: Dependencies( slots: >{ 'appBar': AppBarComponent().asDependent(AppBarConnector()), 'body': ItemBodyComponent().asDependent(ItemBodyConnector()), 'ad_ball': ADBallComponent().asDependent(ADBallConnector()), 'bottomBar': BottomBarComponent().asDependent(BottomBarConnector()), }, ), ); } ///call in view Widget buildItemView(ItemState state, Dispatch dispatch, ViewService service) { return Scaffold( body: Stack( children: [ service.buildComponent('body'), service.buildComponent('ad_ball'), Positioned( child: service.buildComponent('bottomBar'), left: 0.0, bottom: 0.0, right: 0.0, height: 100.0, ), ], ), appBar: AppbarPreferSize(child: service.buildComponent('appBar'))); } ``` ================================================ FILE: docs/zh-cn/concept/dependent.md ================================================ ### Dependent - Dependent = connector + subComponent | subAdapter 的组合,它表达了小组件|小适配器是如何连接到 Component 的。 - 示例代码 ```dart /// todo ``` ================================================ FILE: docs/zh-cn/concept/directory.md ================================================ # Directory 推荐的目录结构会是这样 ``` sample_page -- action.dart /// define action types and action creator -- page.dart /// config a page or component -- view.dart /// define a function which expresses the presentation of user interface -- effect.dart /// define a function which handles the side-effect -- reducer.dart /// define a function which handles state-change -- state.dart /// define a state and some connector of substate components sample_component -- action.dart -- component.dart -- view.dart -- effect.dart -- reducer.dart -- state.dart ``` 上层负责组装,下层负责实现。 ================================================ FILE: docs/zh-cn/concept/dynamic-flow-adapter.md ================================================ # DynamicFlowAdapter - 模版是一个 Map,接受一个数组类型的数据驱动 - 示例代码 ```dart class RecommendAdapter extends DynamicFlowAdapter { RecommendAdapter() : super( pool: >{ 'card_0': RecommendTitleComponent(), 'card_1': RecommendRowComponent(), }, connector: RecommendCardListConnector(), ); } ``` ================================================ FILE: docs/zh-cn/concept/effect.md ================================================ # Effect Effect顾名思义,用于处理Action的副作用。 我估摸着有人就要问我了,副作用是啥玩意? 打个比方吧,假如我拥有一个函数 `f()` ```text fn f(x): return x * 1 ``` 此时此刻,另一个函数 `g()` ```text fn g(x): changeSystemEntropy() return ax ^ 2 + bx + c ``` 我们可以发现,`g()`里边有个改变系统熵的行为。这在函数式编程思想中,就叫做副作用,因为它可能影响到除了这个函数内部自身状态以外的其他状态。 在Fish-Redux中同样,我们通过 `dispatch()` 一些action实现状态修改,但是相对于状态来说,对外部的操作,类似于 `SystemChrome.setSystemUIOverlayStyle()`这样的操作,都是副作用。 现在介绍完了副作用,也没啥可介绍的了。 Effect用法跟Reducer差不太多,但是作用完全不同。 除了上面介绍的场景之外,异步请求也是一个经常会有的情况,这时候Effect可以帮你方便的解决这些问题。 你可以通过控制effect的返回值来达到某些目的,默认情况下,effect会在reducer之前被执行。 当前effect返回 `true` 的时候,就会停止后续的effect和reducer的操作 当前effect返回 `false` 的时候,后续effect和reducer继续执行 - Effect 是一个处理所有副作用的函数。它接收下面的参数 - Action action - Context context - BuildContext context - T state - dispatch - isDisposed Effect会接收来自 View 的“意图”,包括对应的生命周期的回调,然后做出具体的执行。 - 它的处理可能是一个异步函数,数据可能在过程中被修改,所以我们应该通过 context.state 获取最新数据。 - 如果它要修改数据,应该发一个 Action 到 Reducer 里去处理。它对数据是只读的,不能直接去修改数据。 - 如果它的返回值是一个非空值,则代表自己优先处理,不再做下一步的动作;否则广播给其他组件的 Effect 部分,同时发送给 Reducer。 > Self-First-Broadcast。 > ![image.png | left | 747x399](https://cdn.nlark.com/lark/0/2018/png/82574/1545365233153-4c8105b4-050c-49e6-be02-dbf28a861caa.png) - 示例代码 ```dart /// one style of writing FutureOr sideEffect(Action action, Context ctx) async { if (action.type == Lifecycle.initState) { //do something on initState return true; } else if (action.type == 'onShare') { //do something on onShare await Future.delayed(Duration(milliseconds: 1000)); ctx.dispatch(const Action('shared')); return true; } return null; } class MessageComponent extends Component { MessageComponent(): super( view: buildMessageView, effect: sideEffect, ); } ``` ```dart /// another style of writing Effect buildEffect() { return combineEffects(>{ Lifecycle.initState: _initState, 'onShare': _onShare, }); } void _initState(Action action, Context ctx) { //do something on initState } void _onShare(Action action, Context ctx) async { //do something on onShare await Future.delayed(Duration(milliseconds: 1000)); ctx.dispatch(const Action('shared')); } class MessageComponent extends Component { MessageComponent(): super( view: buildMessageView, effect: buildEffect(), ); } ``` ================================================ FILE: docs/zh-cn/concept/filter.md ================================================ # Filter - Filter 是用来优化 Reducer 的性能的。因为 Reducer 是层层组装的,所以处理每一个 Action,理论上会遍历一遍所有的小 Reducer,在一些非常复杂的场景下,这样的一次深度遍历的耗时可能会到毫秒级别(一般情况下都应该小于 1 毫秒)。那么我们需要对 Reducer 做性能优化,提前决定要不要遍历这份 Reducer 子树,减少遍历的深度和次数。 - 示例代码 ```dart bool filter(Action action) { return action.type == 'some action'; } ``` ================================================ FILE: docs/zh-cn/concept/higher-effect.md ================================================ # HigherEffect - 由于 Effect 有可能有自己一些临时状态(尽管不建议这么做,但还是提供了支持),为了支持这个特性,我们将 Effect 提升为高阶函数,将它的状态放在闭包里。 - 框架支持 Effect|HigherEffect 的配置,但是不能对一个组件或适配器同时都配置,那样会带来困扰,一般情况下,都配置往往是个显式的疏忽大意。 - HigherEffect = (Context ctx) => (Action action) => FutureOr - 更详细的例子请参考 [OOP](oop-cn.md) - EffectPart ================================================ FILE: docs/zh-cn/concept/lifecycle.md ================================================ # Lifecycle - 默认的所有生命周期,本质上都来自于 flutter State 中的生命周期。 - initState - didChangeDependencies - build - didUpdateWidget - deactivate - dispose - 在组件内,Reducer 的生命周期是和页面一致的,Effect 和 View 的生命周期是和组件的 Widget 一致的。 - 在适配器中,Reducer 的生命周期是和页面一致的,Effect 的生命周期是和 ListView 的生命周期一致,View 的生命周期是短暂的(划入不可见区域即销毁)。同时增加了 appear 和 disappear 的生命周期, 代表这个 adapter 管理的视图数组,刚进入显示区和完全离开显示区的回调。 ================================================ FILE: docs/zh-cn/concept/mechanism.md ================================================ # Communication Mechanism ## 页面内通信 - 组件|适配器内通信 - 组件|适配器间内通信 ![image.png | left | 747x399](https://cdn.nlark.com/lark/0/2018/png/82574/1545365233153-4c8105b4-050c-49e6-be02-dbf28a861caa.png) Self-First-Broadcast。 发出的 Action,自己优先处理,否则广播给其他组件和 Redux 处理。 最终我们通过一个简单而直观的 dispatch 完成了组件内,组件间(父到子,子到父,兄弟间等)的通信。 ## 页面间通信 - 页面间通信 - Context.appBroadcast - 每一个页面的 PageStore 都会收到消息,各自独立负责处理。 ![image.png | left | 691x519](https://cdn.nlark.com/lark/0/2018/png/82574/1545368705599-745c46a3-f5c6-41a7-a757-1bc6f9a389d4.png) # Refresh Mechanism ## 数据刷新 - 局部数据修改,自动层层触发上层数据的浅拷贝,对业务代码是透明的。 - 层层的数据的拷贝 - 一方面是对 Redux 数据修改的严格的 follow。 - 另一方面也是对数据驱动展示的严格的 follow。 - 数据的任何一个局部的变动,必须要让能看到这个局部的所有视图感知到。如果不拷贝,对应的视图通过新旧两份数据的比较(同一个引用),会错以为自己没有发生变化。 ![image.png | left | 747x361](https://cdn.nlark.com/lark/0/2018/png/82574/1545386668521-0081cb5f-8017-47d1-ad7c-8802bb0be8a0.png) ## 视图刷新 - 扁平化通知到所有组件,组件通过 shouldUpdate 确定自己是否需要刷新 ![image.png | left | 747x336](https://cdn.nlark.com/lark/0/2018/png/82574/1545386773247-2eddfa99-e6b9-4be9-ac43-d1944ff44e9b.png) ================================================ FILE: docs/zh-cn/concept/middleware.md ================================================ # Middleware 关于 Middleware 的定义、签名和 ReduxJS 社区是一致的。 示例代码 ```dart Middleware logMiddleware({ String tag = 'redux', String Function(T) monitor, }) { return ({Dispatch dispatch, Get getState}) { return (Dispatch next) { return isDebug() ? (Action action) { print('---------- [$tag] ----------'); print('[$tag] ${action.type} ${action.payload}'); final T prevState = getState(); if (monitor != null) { print('[$tag] prev-state: ${monitor(prevState)}'); } next(action); final T nextState = getState(); if (monitor != null) { print('[$tag] next-state: ${monitor(nextState)}'); } if (prevState == nextState) { print('[$tag] warning: ${action.type} has not been used.'); } print('========== [$tag] ================'); } : next; }; }; } ``` 更多的参考 src/utils/common_middleware ================================================ FILE: docs/zh-cn/concept/on-error.md ================================================ # OnError - 集中处理由 Effect 产生的业务异常,无论是同步函数还是异步函数。有了统一的异常处理机制,我们就能站在一个更高的抽象角度,对业务代码做出合理的简化。 - 示例代码 ```dart bool onMessageError(Exception e, Context ctx) { if(e is BizException) { ///do some toast return true; } return false; } class MessageComponent extends Component { MessageComponent(): super( view: buildMessageView, effect: buildEffect(), reducer: buildMessageReducer(), onError: onMessageError, ); } ``` ================================================ FILE: docs/zh-cn/concept/oop.md ================================================ # OOP - 虽然框架推荐使用的函数式的编程方式,也提供面向对象式的编程方式的支持。 - ViewPart - 需要复写 build 函数。 - 需要的 state,dispatch,viewService 的参数,已经成为了对象的字段可以直接使用。 - 它是@immutable 的,所以不应该也不需要在内部定义可变字段。 - EffectPart - 需要复写 createMap 函数。 - 需要的 Context 已经被打平,作为了对象的字段可以直接使用。 - 可以定义字段,它的可见性也仅限于自身。 - 它必须配合 higherEffect 一起使用。 - 示例代码 ```dart class MessageView extends ViewPart { @override Widget build() { return Column(children: [ viewService.buildComponent('profile'), InkWell( child: Text('$message'), onTap: () => dispatch(const Action('onShare')), ), ]); } } class MessageEffect extends EffectPart { ///we could put some Non-UI fields here. @override Map createMap() { return { Lifecycle.initState: _initState, 'onShare': _onShare, }; } void _initState(Action action) { //do something on initState } void _onShare(Action action) async { //do something on onShare await Future.delayed(Duration(milliseconds: 1000)); dispatch(const Action('shared')); } } class MessageComponent extends Component { MessageComponent(): super( view: MessageView().asView(), higherEffect: higherEffect(() => MessageEffect()), ); } ``` ================================================ FILE: docs/zh-cn/concept/page.md ================================================ # Page - 一个页面内都有且仅有一个 Store - Page 继承于 Component,所以它能配置所有 Component 的要素 - Page 能配置 Middleware,用于对 Redux 做 AOP 管理 - Page 必须配置一个初始化页面数据的初始化函数  initState - 示例代码 ```dart /// Hello World class HelloWordPage extends Page { HelloWordPage(): super( initState: (String msg) => msg, view:(String msg, _, __) => Text('Hello ${msg}'), ); } HelloWordPage().buildPage('world') ``` ================================================ FILE: docs/zh-cn/concept/reducer.md ================================================ # Reducer - Reducer 是一个上下文无关的 pure function。它接收下面的参数 - T state - Action action - 它主要包含三方面的信息 - 接收一个“意图”, 做出数据修改。 - 如果要修改数据,需要创建一份新的拷贝,修改在拷贝上。 - 如果数据修改了,它会自动触发 State 的层层数据的拷贝,再以扁平化方式通知组件刷新。 - 示例代码 ```dart /// one style of writing String messageReducer(String msg, Action action) { if (action.type == 'shared') { return '$msg [shared]'; } return msg; } class MessageComponent extends Component { MessageComponent(): super( view: buildMessageView, effect: buildEffect(), reducer: messageReducer, ); } ``` ```dart /// another style of writing Reducer buildMessageReducer() { return asReducer(>{ 'shared': _shared, }); } String _shared(String msg, Action action) { return '$msg [shared]'; } class MessageComponent extends Component { MessageComponent(): super( view: buildMessageView, effect: buildEffect(), reducer: buildMessageReducer(), ); } ``` > 推荐的是第二种写法 ================================================ FILE: docs/zh-cn/concept/redux.md ================================================ ### Redux - State - [Action](action-cn.md) - [Reducer](reducer-cn.md) - Store - [Middleware](middleware-cn.md) 以上概念和社区的 Redux 是完全一致的。 Redux 是一个用来做[可预测][集中式][易调试][灵活性]的数据管理的框架。 如果想对 Redux 有更近一步的理解,请参考 [https://github.com/reduxjs/redux](https://github.com/reduxjs/redux) ================================================ FILE: docs/zh-cn/concept/should-update.md ================================================ # ShouldUpdate - 当数据发生变更,Store 扁平化地通知所有组件 - 框架默认使用 identical 比较新旧两份数据来决定是否需要刷新。 - 如果我们对组件的刷新会有非常精确化的诉求, 那么我们可以自己定义一个 ShouldUpdate。 - 示例代码 ```dart bool shouldUpdate(DetailState old, DetailState now) { return old.message != now.message; } ``` ================================================ FILE: docs/zh-cn/concept/static-flow-adapter.md ================================================ # StaticFlowAdapter - 模版是一个 Array,接受 Object|Map 的数据驱动。 - 模版接收一个 Dependent 的数组,每一个 Dependent 可以是 Component 或者 Adapter + Connector 的组合。 - 抽象地看,它非常的像是一个 flatMap + compact 的操作。 - 示例代码 ```dart class ItemBodyComponent extends Component { ItemBodyComponent() : super( view: buildItemBody, dependencies: Dependencies( adapter: StaticFlowAdapter( slots: >[ VideoAdapter().asDependent(videoConnector()), UserInfoComponent().asDependent(userInfoConnector()), DescComponent().asDependent(descConnector()), ItemImageComponent().asDependent(itemImageConnector()), OriginDescComponent().asDependent(originDescConnector()), VisitComponent().asDependent(visitConnector()), SameMoreComponent().asDependent(sameMoreConnector()), PondComponent().asDependent(pondConnector()), CommentAdapter().asDependent(commentConnector()), RecommendAdapter().asDependent(recommendConnector()), PaddingComponent().asDependent(paddingConnector()), ]), ), ); } ``` ================================================ FILE: docs/zh-cn/concept/view.md ================================================ # View - View 是一个输出 Widget 的上下文无关的函数。它接收下面的参数 - T state - Dispatch - ViewService - 它主要包含三方面的信息 - 视图完全由数据驱动。 - 视图产生的事件/回调,通过 Dispatch 发出“意图”,但绝不做具体的实现。 - 使用依赖的组件/适配器,通过在组件上显示配置,再通过 ViewService 标准化调用。 - 其中 ViewService 提供了三个能力 - BuildContext context,获取 flutter Build-Context 的能力 - Widget buildView(String name), 直接创建子组件的能力 - 这里传入的 name 即在 Dependencies 上配置的名称。 - 创建子组件不需要传入任何其他的参数,因为子组件需要的参数,已经通过 Dependencies 配置中,将它们的数据关系,通过 connector 确立。 - ListAdapter buildAdapter(), 直接创建适配器的能力 - 示例代码 ```dart Widget buildMessageView(String message, Dispatch dispatch, ViewService viewService) { return Column(children: [ viewService.buildComponent('profile'), InkWell( child: Text('$message'), onTap: () => dispatch(const Action('onShare')), ), ]); } class MessageComponent extends Component { MessageComponent(): super( view: buildMessageView, ); } ``` ================================================ FILE: docs/zh-cn/concept/what's-adapter.md ================================================ # What's adapter 面向 ListView 场景的分治设计 Adapter。 > 在解答什么是 adapter 之前,我们来看下一般框架对 ListView 的分治是怎么做的。传统的手段,我们对 ListView 的分治更多的局限于它展现部分,而它的逻辑部分往往是集中的。而当我们试图将 ListView 下的某一局部的展现和逻辑封装在一起,我们就会遇到"Big-Cell"问题,面临性能的显著降低。 > 这里面存在一个分治和性能上的矛盾。这个矛盾带来了复用难,可维护差,难以协作等中大型场景下的问题。 > > 解决这个问题,有两种思路: > > 1. 下沉到 UI 表达层(Widgets),去实现一个高性能的 ScrollView。 > 2. 向上做模型抽象,得到一个逻辑上的 ScrollView,性能上的 ListView。 > > fish redux 选择了第二条更加通用的路径来解决 LisView 下的分治问题。 > > 一个 ListView 对应了一个 Adapter,这看上去非常的像 Android 里的设计,但事实上 fish-redux 里的 Adapter 概念走的更远。 > > 1. 一个 Adapter 是可以由多个 Component 和 Adapter 组合而成,它有点像 flatmap & compact 的 api 的叠加。 > 2. Adapter 以及它的子 Adapter 的生命周期是和 ListView 是等效的。它像跨斗一般附着于 ListView 的生命周期之上。同时由于 Adapter 生命周期的提升,我们额外收获了两个非常有用的事件消息(appear & disappear)。 > > > 注意 ⚠️ 在 Adapter 里配置的子 Component,它的生命周期和它所对应的 WidgetState 是一致的,所以它的是短暂的。 - Adapter 的容器有两类,用图来说明吧: ================================================ FILE: docs/zh-cn/concept/what's-connector.md ================================================ # What's connector 在解答 connector 是什么之前,我们来先看一个代码片段 ```javascript let hasChanged = false; const nextState = {}; for (let i = 0; i < finalReducerKeys.length; i++) { const key = finalReducerKeys[i]; const reducer = finalReducers[key]; const previousStateForKey = state[key]; const nextStateForKey = reducer(previousStateForKey, action); if (typeof nextStateForKey === 'undefined') { const errorMessage = getUndefinedStateErrorMessage(key, action); throw new Error(errorMessage); } nextState[key] = nextStateForKey; hasChanged = hasChanged || nextStateForKey !== previousStateForKey; } return hasChanged ? nextState : state; ``` 以上来自于 Reduxjs-[combineReducers](https://github.com/reduxjs/redux/blob/master/src/combineReducers.js)的核心实现。 combineReducers 是一个将 Reducer 分治的函数,让一个庞大数据的 Reducer 可以由多层的更小的 Reducer 组合而成。 这是 Redux 框架里的核心 API,但是他有缺点。他有非常明显的语言的局限性,如下 3 点: 1. 浅拷贝一个任意对象 ```javascript const nextState = {}; ``` 2. 读取字段 ```javascript const previousStateForKey = state[key]; ``` 3. 写入字段 ```javascript nextState[key] = nextStateForKey; ``` 将上面的 3 点抽象来看: 1. State 的 clone 的能力(浅拷贝) 2. Get & Set 的能力,即为 Connector 的概念。 有了 以上两点,我们才完全集成了 Redux 的所有精华,同时将它的设计更上一个通用的维度。 ================================================ FILE: docs/zh-cn/concept/what's-the-diiference.md ================================================ # What's different with Redux ? ## 它们是解决不同层面问题的两个框架 > Redux 是一个专注于状态管理的框架;Fish Redux 是基于 Redux 做状态管理的应用框架。 > 应用框架不仅仅要解决状态管理的问题,还要解决分治,通信,数据驱动,解耦等等问题。 ## Fish Redux 解决了集中和分治的矛盾。 > Redux 通过使用者手动组织代码的形式来完成从小的 Reducer 到主 Reducer 的合并过程; > Fish Redux 通过显式的表达组件之间的依赖关系,由框架自动完成从细力度的 Reducer 到主 Reducer 的合并过程; ## Fish Redux 提供了一个简单的组件抽象模型 > 它通过简单的 3 个函数组合而成 ## Fish Redux 提供了一个 Adapter 的抽象组件模型 > 在基础的组件模型以外,Fish Redux 提供了一个 Adapter 抽象模型,用来解决在 ListView 上大 Cell 的性能问题。 > 通过上层抽象,我们得到了逻辑上的 ScrollView,性能上的 ListView。 ================================================ FILE: docs/zh-cn/concept/widget-wrapper.md ================================================ ### WidgetWrapper - 它用来解决 flutter 的 ui 体系下,一些需要实现特色接口的 Widget,比如 KeepAlive,因为通过 Component 产生的 Widget 会被一个框架内部的 Stateful 的 Widget 所包裹。 - 示例代码 ```dart import 'package:flutter/material.dart' hide Action; Widget repaintBoundaryWrapper(Widget widget) { return RepaintBoundary(child: widget); } ``` ================================================ FILE: example/.flutter-plugins-dependencies ================================================ {"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"path_provider","path":"/Users/linyunhe/.pub-cache/hosted/pub.dartlang.org/path_provider-0.4.1/","dependencies":[]}],"android":[{"name":"path_provider","path":"/Users/linyunhe/.pub-cache/hosted/pub.dartlang.org/path_provider-0.4.1/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"path_provider","dependencies":[]}],"date_created":"2021-06-08 17:37:57.339650","version":"1.22.6-xianyu"} ================================================ FILE: example/.gitignore ================================================ # Miscellaneous *.class *.lock *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ # IntelliJ related *.iml *.ipr *.iws .idea/ # Visual Studio Code related .vscode/ # Flutter/Dart/Pub related **/doc/api/ .dart_tool/ .flutter-plugins .packages .pub-cache/ .pub/ build/ # Android related **/android/**/gradle-wrapper.jar **/android/.gradle **/android/captures/ **/android/gradlew **/android/gradlew.bat **/android/local.properties **/android/**/GeneratedPluginRegistrant.java # iOS/XCode related **/ios/**/*.mode1v3 **/ios/**/*.mode2v3 **/ios/**/*.moved-aside **/ios/**/*.pbxuser **/ios/**/*.perspectivev3 **/ios/**/*sync/ **/ios/**/.sconsign.dblite **/ios/**/.tags* **/ios/**/.vagrant/ **/ios/**/DerivedData/ **/ios/**/Icon? **/ios/**/Pods/ **/ios/**/.symlinks/ **/ios/**/profile **/ios/**/xcuserdata **/ios/.generated/ **/ios/Flutter/App.framework **/ios/Flutter/Flutter.framework **/ios/Flutter/Generated.xcconfig **/ios/Flutter/app.flx **/ios/Flutter/app.zip **/ios/Flutter/flutter_assets/ **/ios/ServiceDefinitions.json **/ios/Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !**/ios/**/default.mode1v3 !**/ios/**/default.mode2v3 !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages ================================================ FILE: example/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: 5391447fae6209bb21a89e6a5a6583cac1af9b4b channel: stable project_type: app ================================================ FILE: example/README.md ================================================ # sample A new Flutter project. ## Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this is your first Flutter project: - [Lab: Write your first Flutter app](https://flutter.io/docs/get-started/codelab) - [Cookbook: Useful Flutter samples](https://flutter.io/docs/cookbook) For help getting started with Flutter, view our [online documentation](https://flutter.io/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. ================================================ FILE: example/lib/app.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import 'global_store/state.dart'; import 'global_store/store.dart'; import 'todo_edit_page/page.dart'; import 'todo_list_page/page.dart'; /// 创建应用的根 Widget /// 1. 创建一个简单的路由,并注册页面 /// 2. 对所需的页面进行和 AppStore 的连接 /// 3. 对所需的页面进行 AOP 的增强 Widget createApp() { final AbstractRoutes routes = PageRoutes( pages: >{ /// 注册TodoList主页面 'todo_list': ToDoListPage(), /// 注册Todo编辑页面 'todo_edit': TodoEditPage(), }, visitor: (String path, Page page) { /// 只有特定的范围的 Page 才需要建立和 AppStore 的连接关系 /// 满足 Page ,T 是 GlobalBaseState 的子类 if (page.isTypeof()) { /// 建立 AppStore 驱动 PageStore 的单向数据连接 /// 1. 参数1 AppStore /// 2. 参数2 当 AppStore.state 变化时, PageStore.state 该如何变化 page.connectExtraStore(GlobalStore.store, (Object pagestate, GlobalState appState) { final GlobalBaseState p = pagestate; if (p.themeColor != appState.themeColor) { if (pagestate is Cloneable) { final Object copy = pagestate.clone(); final GlobalBaseState newState = copy; newState.themeColor = appState.themeColor; return newState; } } return pagestate; }); } /// AOP /// 页面可以有一些私有的 AOP 的增强, 但往往会有一些 AOP 是整个应用下,所有页面都会有的。 /// 这些公共的通用 AOP ,通过遍历路由页面的形式统一加入。 page.enhancer.append( /// View AOP viewMiddleware: >[ safetyView(), ], /// Adapter AOP adapterMiddleware: >[ safetyAdapter() ], /// Effect AOP effectMiddleware: >[ _pageAnalyticsMiddleware(), ], /// Store AOP middleware: >[ logMiddleware(tag: page.runtimeType.toString()), ], ); }, ); return MaterialApp( title: 'Fluro', debugShowCheckedModeBanner: false, theme: ThemeData( primarySwatch: Colors.blue, ), home: routes.buildPage('todo_list', null), onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute(builder: (BuildContext context) { return routes.buildPage(settings.name, settings.arguments); }); }, ); } /// 简单的 Effect AOP /// 只针对页面的生命周期进行打印 EffectMiddleware _pageAnalyticsMiddleware({String tag = 'redux'}) { return (AbstractLogic logic, Store store) { return (Effect effect) { return (Action action, Context ctx) { if (logic is Page && action.type is Lifecycle) { print('${logic.runtimeType} ${action.type.toString()} '); } return effect?.call(action, ctx); }; }; }; } ================================================ FILE: example/lib/global_store/action.dart ================================================ import 'package:fish_redux/fish_redux.dart'; enum GlobalAction { changeThemeColor } class GlobalActionCreator { static Action onchangeThemeColor() { return const Action(GlobalAction.changeThemeColor); } } ================================================ FILE: example/lib/global_store/reducer.dart ================================================ import 'dart:ui'; import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import 'action.dart'; import 'state.dart'; Reducer buildReducer() { return asReducer( >{ GlobalAction.changeThemeColor: _onchangeThemeColor, }, ); } List _colors = [ Colors.green, Colors.red, Colors.black, Colors.blue ]; GlobalState _onchangeThemeColor(GlobalState state, Action action) { final Color next = _colors[((_colors.indexOf(state.themeColor) + 1) % _colors.length)]; return state.clone()..themeColor = next; } ================================================ FILE: example/lib/global_store/state.dart ================================================ import 'dart:ui'; import 'package:fish_redux/fish_redux.dart'; abstract class GlobalBaseState { Color get themeColor; set themeColor(Color color); } class GlobalState implements GlobalBaseState, Cloneable { @override Color themeColor; @override GlobalState clone() { return GlobalState(); } } ================================================ FILE: example/lib/global_store/store.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'reducer.dart'; import 'state.dart'; /// 建立一个AppStore /// 目前它的功能只有切换主题 class GlobalStore { static Store _globalStore; static Store get store => _globalStore ??= createStore(GlobalState(), buildReducer()); } ================================================ FILE: example/lib/main.dart ================================================ import 'package:flutter/material.dart' hide Action, Page; import 'app.dart'; void main() => runApp(createApp()); ================================================ FILE: example/lib/todo_edit_page/action.dart ================================================ import 'package:fish_redux/fish_redux.dart'; enum ToDoEditAction { onDone, onChangeTheme } class ToDoEditActionCreator { static Action onDone() { return const Action(ToDoEditAction.onDone); } static Action onChangeTheme() { return const Action(ToDoEditAction.onChangeTheme); } } ================================================ FILE: example/lib/todo_edit_page/effect.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import '../global_store/action.dart'; import '../global_store/store.dart'; import '../todo_list_page/todo_component/component.dart'; import 'action.dart'; import 'state.dart'; Effect buildEffect() { return combineEffects(>{ ToDoEditAction.onDone: _onDone, ToDoEditAction.onChangeTheme: _onChangeTheme, }); } void _onDone(Action action, Context ctx) { Navigator.of(ctx.context).pop( ctx.state.toDo.clone() ..desc = ctx.state.descEditController.text ..title = ctx.state.nameEditController.text, ); } void _onChangeTheme(Action action, Context ctx) { GlobalStore.store.dispatch(GlobalActionCreator.onchangeThemeColor()); } ================================================ FILE: example/lib/todo_edit_page/page.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import '../todo_list_page/todo_component/component.dart'; import 'effect.dart'; import 'state.dart'; import 'view.dart'; class TodoEditPage extends Page { TodoEditPage() : super( initState: initState, effect: buildEffect(), view: buildView, /// 页面私有AOP,如果需要 // middleware: >[ // logMiddleware(tag: 'TodoEditPage'), // ], ); } ================================================ FILE: example/lib/todo_edit_page/state.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import '../global_store/state.dart'; import '../todo_list_page/todo_component/component.dart'; class TodoEditState implements GlobalBaseState, Cloneable { ToDoState toDo; TextEditingController nameEditController; TextEditingController descEditController; FocusNode focusNodeName; FocusNode focusNodeDesc; @override Color themeColor; @override TodoEditState clone() { return TodoEditState() ..nameEditController = nameEditController ..descEditController = descEditController ..focusNodeName = focusNodeName ..focusNodeDesc = focusNodeDesc ..toDo = toDo ..themeColor = themeColor; } } TodoEditState initState(ToDoState arg) { final TodoEditState state = TodoEditState(); state.toDo = arg?.clone() ?? ToDoState(); state.nameEditController = TextEditingController(text: arg?.title); state.descEditController = TextEditingController(text: arg?.desc); state.focusNodeName = FocusNode(); state.focusNodeDesc = FocusNode(); return state; } ================================================ FILE: example/lib/todo_edit_page/view.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import 'action.dart'; import 'state.dart'; Widget buildView( TodoEditState state, Dispatch dispatch, ViewService viewService) { return Scaffold( appBar: AppBar( backgroundColor: state.themeColor, title: const Text('Todo'), ), body: Container( padding: const EdgeInsets.all(16.0), child: Column( children: [ Container( child: Row( children: [ Container( child: const Text('title:', style: TextStyle(color: Colors.black, fontSize: 20.0)), width: 56.0, alignment: AlignmentDirectional.topEnd, ), Expanded( child: Container( color: const Color(0xFFE0E0E0), padding: const EdgeInsets.all(8.0), margin: const EdgeInsets.only(left: 8.0), child: EditableText( controller: state.nameEditController, focusNode: state.focusNodeName, autofocus: true, style: const TextStyle(color: Colors.black, fontSize: 16.0), cursorColor: Colors.yellow, backgroundCursorColor: const Color(0xFFFFF59D), ), )) ], ), ), RaisedButton( padding: const EdgeInsets.only( left: 20.0, top: 10.0, right: 20.0, bottom: 10.0), color: Colors.blue, child: const Text('Change theme', style: TextStyle(fontSize: 18), overflow: TextOverflow.ellipsis), onPressed: () { dispatch(ToDoEditActionCreator.onChangeTheme()); }), Expanded( child: Container( margin: const EdgeInsets.only(top: 32.0), alignment: AlignmentDirectional.topStart, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( child: const Text('desc:', style: TextStyle(color: Colors.black, fontSize: 20.0)), width: 56.0, alignment: AlignmentDirectional.topEnd, ), Expanded( child: Container( color: const Color(0xFFE0E0E0), padding: const EdgeInsets.all(8.0), margin: const EdgeInsets.only(left: 8.0), child: EditableText( controller: state.descEditController, backgroundCursorColor: const Color(0xFFE0E0E0), maxLines: 10, focusNode: state.focusNodeDesc, style: const TextStyle(color: Colors.black, fontSize: 16.0), cursorColor: Colors.yellow), )) ], ), )) ], ), ), floatingActionButton: FloatingActionButton( onPressed: () => dispatch(ToDoEditActionCreator.onDone()), tooltip: 'Done', child: const Icon(Icons.done), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, ); } ================================================ FILE: example/lib/todo_list_page/action.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'todo_component/component.dart'; enum PageAction { initToDos, onAdd } class PageActionCreator { static Action initToDosAction(List toDos) { return Action(PageAction.initToDos, payload: toDos); } static Action onAddAction() { return const Action(PageAction.onAdd); } } ================================================ FILE: example/lib/todo_list_page/effect.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import 'action.dart'; import 'list_adapter/action.dart' as list_action; import 'state.dart'; import 'todo_component/component.dart'; Effect buildEffect() { return combineEffects(>{ Lifecycle.initState: _init, PageAction.onAdd: _onAdd, }); } void _init(Action action, Context ctx) { final List initToDos = [ ToDoState( uniqueId: '0', title: 'Hello world', desc: 'Learn how to program.', isDone: true, ), ToDoState( uniqueId: '1', title: 'Hello Flutter', desc: 'Learn how to build a flutter application.', isDone: true, ), ToDoState( uniqueId: '2', title: 'How Fish Redux', desc: 'Learn how to use Fish Redux in a flutter application.', isDone: false, ) ]; ctx.dispatch(PageActionCreator.initToDosAction(initToDos)); } void _onAdd(Action action, Context ctx) { Navigator.of(ctx.context) .pushNamed('todo_edit', arguments: null) .then((dynamic toDo) { if (toDo != null && (toDo.title?.isNotEmpty == true || toDo.desc?.isNotEmpty == true)) { ctx.dispatch(list_action.ToDoListActionCreator.add(toDo)); } }); } ================================================ FILE: example/lib/todo_list_page/flow_adapter/adapter.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import '../state.dart'; import '../todo_component/component.dart'; import 'reducer.dart'; import 'connector.dart'; FlowAdapter get adapter => FlowAdapter( reducer: buildReducer(), view: (PageState state) => DependentArray( length: state.itemCount, builder: (int index) { return ToDoConnector(index: index) + ToDoComponent(); }, )); ================================================ FILE: example/lib/todo_list_page/flow_adapter/connector.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import '../state.dart'; import '../todo_component/component.dart'; class ToDoConnector extends ConnOp { ToDoConnector({this.index}); final int index; @override ToDoState get(PageState state) { if (index >= state.toDos.length) { return null; } return state.toDos[index]; } @override void set(PageState state, ToDoState subState) { state.toDos[index] = subState; } } ================================================ FILE: example/lib/todo_list_page/flow_adapter/reducer.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import '../list_adapter/action.dart'; import '../state.dart'; import '../todo_component/action.dart' as todo_action; import '../todo_component/component.dart'; Reducer buildReducer() { return asReducer(>{ ToDoListAction.add: _add, todo_action.ToDoAction.remove: _remove }); } PageState _add(PageState state, Action action) { final ToDoState toDo = action.payload; return state.clone()..toDos = (state.toDos.toList()..add(toDo)); } PageState _remove(PageState state, Action action) { final String unique = action.payload; return state.clone() ..toDos = (state.toDos.toList() ..removeWhere((ToDoState state) => state.uniqueId == unique)); } ================================================ FILE: example/lib/todo_list_page/list_adapter/action.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import '../todo_component/component.dart'; enum ToDoListAction { add } class ToDoListActionCreator { static Action add(ToDoState state) { return Action(ToDoListAction.add, payload: state); } } ================================================ FILE: example/lib/todo_list_page/list_adapter/adapter.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import '../state.dart'; import '../todo_component/component.dart'; import 'reducer.dart'; class ToDoListAdapter extends SourceFlowAdapter { ToDoListAdapter() : super( pool: >{ 'toDo': ToDoComponent(), }, reducer: buildReducer(), ); } ================================================ FILE: example/lib/todo_list_page/list_adapter/reducer.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import '../state.dart'; import '../todo_component/action.dart' as todo_action; import '../todo_component/component.dart'; import 'action.dart'; Reducer buildReducer() { return asReducer(>{ ToDoListAction.add: _add, todo_action.ToDoAction.remove: _remove }); } PageState _add(PageState state, Action action) { final ToDoState toDo = action.payload; return state.clone()..toDos = (state.toDos.toList()..add(toDo)); } PageState _remove(PageState state, Action action) { final String unique = action.payload; return state.clone() ..toDos = (state.toDos.toList() ..removeWhere((ToDoState state) => state.uniqueId == unique)); } ================================================ FILE: example/lib/todo_list_page/page.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'effect.dart'; import 'list_adapter/adapter.dart'; import 'flow_adapter/adapter.dart'; import 'reducer.dart'; import 'report_component/component.dart'; import 'state.dart'; import 'view.dart'; class ToDoListPage extends Page> { ToDoListPage() : super( initState: initState, effect: buildEffect(), reducer: buildReducer(), view: buildView, dependencies: Dependencies( adapter: const NoneConn() + adapter,//NoneConn() + ToDoListAdapter(), slots: >{ 'report': ReportConnector() + ReportComponent() }), /// 页面私有AOP, 如果需要 // middleware: >[ // logMiddleware(tag: 'ToDoListPage'), // ], ); } ================================================ FILE: example/lib/todo_list_page/reducer.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'action.dart'; import 'state.dart'; import 'todo_component/component.dart'; Reducer buildReducer() { return asReducer( >{PageAction.initToDos: _initToDosReducer}, ); } PageState _initToDosReducer(PageState state, Action action) { final List toDos = action.payload ?? []; final PageState newState = state.clone(); newState.toDos = toDos; return newState; } ================================================ FILE: example/lib/todo_list_page/report_component/component.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'state.dart'; import 'view.dart'; export 'state.dart'; class ReportComponent extends Component { ReportComponent() : super( view: buildView, ); } ================================================ FILE: example/lib/todo_list_page/report_component/state.dart ================================================ import 'package:fish_redux/fish_redux.dart'; class ReportState implements Cloneable { int total; int done; ReportState({this.total = 0, this.done = 0}); @override ReportState clone() { return ReportState() ..total = total ..done = done; } @override String toString() { return 'ReportState{total: $total, done: $done}'; } } ================================================ FILE: example/lib/todo_list_page/report_component/view.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import 'state.dart'; Widget buildView( ReportState state, Dispatch dispatch, ViewService viewService, ) { return Container( margin: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0), color: Colors.blue, child: Row( children: [ Container( child: const Icon(Icons.report), margin: const EdgeInsets.only(right: 8.0), ), Text( 'Total ${state.total} tasks, ${state.done} done.', style: const TextStyle(fontSize: 18.0, color: Colors.white), ) ], )); } ================================================ FILE: example/lib/todo_list_page/state.dart ================================================ import 'dart:ui'; import 'package:fish_redux/fish_redux.dart'; import '../global_store/state.dart'; import 'report_component/component.dart'; import 'todo_component/component.dart'; class PageState extends ItemListLike implements GlobalBaseState, Cloneable { List toDos; @override Color themeColor; @override PageState clone() { return PageState() ..toDos = toDos ..themeColor = themeColor; } @override Object getItemData(int index) => toDos[index]; @override String getItemType(int index) => 'toDo'; @override int get itemCount => toDos?.length ?? 0; @override ItemListLike updateItemData(int index, Object data, bool isStateCopied) { toDos[index] = data; return this; } } PageState initState(Map args) { //just demo, do nothing here... return PageState(); } class ReportConnector extends ConnOp with ReselectMixin { @override ReportState computed(PageState state) { return ReportState() ..done = state.toDos.where((ToDoState tds) => tds.isDone).length ..total = state.toDos.length; } @override List factors(PageState state) { return [ state.toDos.where((ToDoState tds) => tds.isDone).length, state.toDos.length ]; } @override void set(PageState state, ReportState subState) { throw Exception('Unexcepted to set PageState from ReportState'); } } ================================================ FILE: example/lib/todo_list_page/todo_component/action.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'state.dart'; enum ToDoAction { onEdit, edit, done, onRemove, remove } class ToDoActionCreator { static Action onEditAction(String uniqueId) { return Action(ToDoAction.onEdit, payload: uniqueId); } static Action editAction(ToDoState toDo) { return Action(ToDoAction.edit, payload: toDo); } static Action doneAction(String uniqueId) { return Action(ToDoAction.done, payload: uniqueId); } static Action onRemoveAction(String uniqueId) { return Action(ToDoAction.onRemove, payload: uniqueId); } static Action removeAction(String uniqueId) { return Action(ToDoAction.remove, payload: uniqueId); } } ================================================ FILE: example/lib/todo_list_page/todo_component/component.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'effect.dart'; import 'reducer.dart'; import 'state.dart'; import 'view.dart'; export 'state.dart'; class ToDoComponent extends Component { ToDoComponent() : super( view: buildView, effect: buildEffect(), reducer: buildReducer(), ); } ================================================ FILE: example/lib/todo_list_page/todo_component/effect.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import 'action.dart'; import 'state.dart'; Effect buildEffect() { return combineEffects(>{ ToDoAction.onEdit: _onEdit, ToDoAction.onRemove: _onRemove, }); } void _onEdit(Action action, Context ctx) { if (action.payload == ctx.state.uniqueId) { Navigator.of(ctx.context) // .push(MaterialPageRoute( // builder: (BuildContext buildCtx) => // edit_page.TodoEditPage().buildPage(ctx.state))) .pushNamed('todo_edit', arguments: ctx.state) .then((dynamic toDo) { if (toDo != null) { ctx.dispatch(ToDoActionCreator.editAction(toDo)); } }); } } void _onRemove(Action action, Context ctx) async { final String select = await showDialog( context: ctx.context, builder: (BuildContext buildContext) { return AlertDialog( title: Text('Are you sure to delete "${ctx.state.title}"?'), actions: [ GestureDetector( child: const Text( 'Cancel', style: TextStyle(fontSize: 16.0), ), onTap: () => Navigator.of(buildContext).pop(), ), GestureDetector( child: const Text('Yes', style: TextStyle(fontSize: 16.0)), onTap: () => Navigator.of(buildContext).pop('Yes'), ) ], ); }); if (select == 'Yes') { ctx.dispatch(ToDoActionCreator.removeAction(ctx.state.uniqueId)); } } ================================================ FILE: example/lib/todo_list_page/todo_component/reducer.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'action.dart'; import 'state.dart'; Reducer buildReducer() { return asReducer(>{ ToDoAction.edit: _edit, ToDoAction.done: _markDone }); } ToDoState _edit(ToDoState state, Action action) { final ToDoState toDo = action.payload; if (state.uniqueId == toDo.uniqueId) { return state.clone() ..title = toDo.title ..desc = toDo.desc; } return state; } ToDoState _markDone(ToDoState state, Action action) { final String uniqueId = action.payload; if (state.uniqueId == uniqueId) { return state.clone()..isDone = !state.isDone; } return state; } ================================================ FILE: example/lib/todo_list_page/todo_component/state.dart ================================================ import 'package:fish_redux/fish_redux.dart'; // import 'package:uuid/uuid.dart'; class ToDoState implements Cloneable { String uniqueId; String title; String desc; bool isDone; static int _seed = 202103051044; ToDoState({this.uniqueId, this.title, this.desc, this.isDone = false}) { uniqueId ??= '${_seed++}'; } @override ToDoState clone() { return ToDoState() ..uniqueId = uniqueId ..title = title ..desc = desc ..isDone = isDone; } @override String toString() { return 'ToDoState{uniqueId: $uniqueId, title: $title, desc: $desc, isDone: $isDone}'; } } ================================================ FILE: example/lib/todo_list_page/todo_component/view.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import 'action.dart'; import 'state.dart'; Widget buildView( ToDoState state, Dispatch dispatch, ViewService viewService, ) { return Container( padding: const EdgeInsets.all(8.0), child: GestureDetector( child: Column( children: [ GestureDetector( child: Container( height: 36.0, color: state.isDone ? Colors.green : Colors.red, child: Row( children: [ Container( child: const Icon(Icons.label_outline), margin: const EdgeInsets.all(8.0), ), Expanded( child: Text( state.title ?? '', maxLines: 1, style: const TextStyle(color: Colors.white, fontSize: 18.0), )), GestureDetector( child: Container( margin: const EdgeInsets.only(right: 16.0), child: (() => state.isDone ? const Icon(Icons.check_box) : const Icon(Icons.check_box_outline_blank))(), ), onTap: () { dispatch(ToDoActionCreator.doneAction(state.uniqueId)); }, ) ], ), alignment: AlignmentDirectional.centerStart, ), ), Container( padding: const EdgeInsets.fromLTRB(16.0, 24.0, 16.0, 24.0), color: const Color(0xFFE0E0E0), child: Row( children: [ Expanded( child: Container( child: Text( state.desc ?? '', style: const TextStyle(color: Colors.black, fontSize: 16.0), ), )), GestureDetector( child: Container( child: const Icon(Icons.edit), ), onTap: () { dispatch(ToDoActionCreator.onEditAction(state.uniqueId)); }, ) ], ), ) ], ), onLongPress: () { dispatch(ToDoActionCreator.onRemoveAction(state.uniqueId)); }, ), ); } ================================================ FILE: example/lib/todo_list_page/view.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import 'action.dart'; import 'state.dart'; Widget buildView(PageState state, Dispatch dispatch, ViewService viewService) { final ListAdapter adapter = viewService.buildAdapter(); return Scaffold( appBar: AppBar( backgroundColor: state.themeColor, title: const Text('ToDoList'), ), body: Container( child: Column( children: [ viewService.buildComponent('report'), Expanded( child: ListView.builder( itemBuilder: adapter.itemBuilder, itemCount: adapter.itemCount)) ], ), ), floatingActionButton: FloatingActionButton( onPressed: () => dispatch(PageActionCreator.onAddAction()), tooltip: 'Add', child: const Icon(Icons.add), ), ); } ================================================ FILE: example/pubspec.yaml ================================================ name: sample description: Demonstrates how to use the fish_redux. # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 # followed by an optional build number separated by a +. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. # Read more about versioning at semver.org. version: 1.0.0+1 environment: sdk: '>=2.0.0-dev.68.0 <3.0.0' dependencies: fish_redux: path: ../ flutter: sdk: flutter path_provider: ^0.4.1 dev_dependencies: test: ^1.3.0 mockito: ^3.0.0 flutter_driver: sdk: flutter flutter_test: sdk: flutter # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec # The following section is specific to Flutter. flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.io/assets-and-images/#resolution-aware. # For details regarding adding assets from package dependencies, see # https://flutter.io/assets-and-images/#from-packages # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts from package dependencies, # see https://flutter.io/custom-fonts/#from-packages ================================================ FILE: example/test/widget_test.dart ================================================ // This is a basic Flutter widget test. // // To perform an interaction with a widget in your test, use the WidgetTester // utility that Flutter provides. For example, you can send tap and scroll // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async {}); } ================================================ FILE: example/web/index.html ================================================ example ================================================ FILE: example/web/manifest.json ================================================ { "name": "example", "short_name": "example", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" } ] } ================================================ FILE: fish_redux.iml ================================================ ================================================ FILE: lib/fish_redux.dart ================================================ export 'src/extensions/extendsions.dart'; export 'src/redux/redux.dart'; export 'src/redux_adapter/redux_adapter.dart'; export 'src/redux_aop/redux_aop.dart'; export 'src/redux_component/redux_component.dart'; export 'src/redux_component_mixin/redux_component_mixin.dart'; export 'src/redux_connector/redux_connector.dart'; export 'src/redux_middleware/redux_middleware.dart'; export 'src/redux_routes/redux_routes.dart'; export 'src/utils/utils.dart'; ================================================ FILE: lib/src/extensions/adapter_extensions.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/foundation.dart'; import '../redux/basic.dart'; import '../redux_adapter/redux_adapter.dart'; import '../redux_component/redux_component.dart'; import '../redux_connector/redux_connector.dart'; import 'connector_extensions.dart'; class SimpleFlowAdapter extends FlowAdapter { SimpleFlowAdapter({ @required FlowAdapterView view, ReducerFilter filter, Reducer reducer, Effect effect, @deprecated Object Function(T state) key, }) : super( view: view, filter: filter, reducer: reducer, effect: effect, key: key, ); SimpleFlowAdapter.static({ @required List> children, ReducerFilter filter, Reducer reducer, Effect effect, @deprecated Object Function(T state) key, }) : this( view: _buildByStatic(children), filter: filter, reducer: reducer, effect: effect, key: key, ); SimpleFlowAdapter.dynamic({ @required Map> pool, @required AbstractConnector> connector, ReducerFilter filter, Reducer reducer, Effect effect, @deprecated Object Function(T state) key, }) : this( view: _buildByDynamic(pool: pool, connector: connector), filter: filter, reducer: reducer, effect: effect, key: key, ); SimpleFlowAdapter.listLike({ @required Map> pool, @required AbstractConnector connector, ReducerFilter filter, Reducer reducer, Effect effect, @deprecated Object Function(T state) key, }) : this( view: _buildByListLike(pool: pool, connector: connector), filter: filter, reducer: reducer, effect: effect, key: key, ); } FlowAdapterView _buildByStatic(List> children) { return (T state) { return DependentArray.fromList(children .where((Dependent dep) => dep.subGetter(() => state).call() != null) .toList()); }; } FlowAdapterView _buildByListLike({ @required Map> pool, @required AbstractConnector connector, }) { return (T state) { final MutableItemListLike source = connector.get(state); final DependentArray depList = DependentArray( length: source.itemCount, builder: (int index) { final String type = source.getItemType(index); final Dependent dep = ConnHelper.join( ConnHelper.to( connector, IndexedListLikeConn(index), ), pool[type], ); return dep; }, ); return depList; }; } FlowAdapterView _buildByDynamic({ @required Map> pool, @required AbstractConnector> connector, }) { return (T state) { final List list = connector.get(state); final DependentArray depList = DependentArray( length: list.length, builder: (int index) { assert(index < list.length); if (index < list.length) { final ItemBean ib = list[index]; assert(ib != null); return ConnHelper.join( ConnHelper.to, Object>( connector, IndexedListConn(index), ), pool[ib.type], ); } return null; }, ); return depList; }; } ================================================ FILE: lib/src/extensions/component_extensions.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart' hide Action; abstract class SimpleComponent extends Component { SimpleComponent({ ReducerFilter filter, Dependencies dependencies, ShouldUpdate shouldUpdate, WidgetWrapper wrapper, @deprecated Key Function(T) key, bool clearOnDependenciesChanged = false, }) : super( view: null, reducer: null, effect: null, filter: filter, dependencies: dependencies, shouldUpdate: shouldUpdate, wrapper: wrapper, key: key, clearOnDependenciesChanged: clearOnDependenciesChanged, ); @override ViewBuilder get protectedView => view; @override Reducer get protectedReducer => reducer; @override Effect get protectedEffect => effect; Widget view(T state, Dispatch dispatch, ViewService viewService); T reducer(T state, Action action) { return state; } /// interrupted if not [false, null] dynamic effect(Action action, Context ctx) { return true; } } ================================================ FILE: lib/src/extensions/connector_extensions.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import '../redux/redux.dart'; import '../redux_component/basic.dart'; import '../redux_connector/redux_connector.dart'; mixin IndexedConnMixin on AbstractConnector { P _cached; int get index; P getByIndex(T state, int index); @override P get(T state) { final P newState = getByIndex(state, index); return checkNextState(newState); } /// fix get 存在状态不同步 P checkNextState(Object newState) { final Object lastState = _cached; final Object nextState = ((newState is! P || newState.runtimeType != lastState.runtimeType) ? false : (newState is StateKey ? newState.key() : null) == (lastState is StateKey ? lastState.key() : null)) ? newState : lastState; return _cached = nextState; } } abstract class ImmutableIndexedConn extends ImmutableConn with ConnOpMixin, IndexedConnMixin { @override final int index; ImmutableIndexedConn(this.index); T setByIndex(T state, P subState, int index); @override T set(T state, P subState) => setByIndex(state, subState, index); } abstract class MutableIndexedConn extends MutableConn with ConnOpMixin, IndexedConnMixin { @override final int index; MutableIndexedConn(this.index); void setByIndex(T state, P subState, int index); @override void set(T state, P subState) => setByIndex(state, subState, index); } /////////////////////////////////////////////////////////////////////////////// class IndexedListConn

extends MutableIndexedConn, P> with ConnOpMixin, P>, IndexedConnMixin, P> { IndexedListConn(int index) : super(index); @override P getByIndex(List

state, int index) { final P newState = state[index]; return checkNextState(newState); } @override void setByIndex(List

state, Object subState, int index) { state[index] = subState; } } class IndexedListLikeConn extends MutableIndexedConn with ConnOpMixin { IndexedListLikeConn(int index) : super(index); @override Object getByIndex(T state, int index) { final Object newState = state.getItemData(index); return checkNextState(newState); } @override void setByIndex(T state, Object subState, int index) { state.setItemData(index, subState); } } ================================================ FILE: lib/src/extensions/extendsions.dart ================================================ export 'adapter_extensions.dart'; export 'component_extensions.dart'; export 'connector_extensions.dart'; ================================================ FILE: lib/src/redux/apply_middleware.dart ================================================ import 'basic.dart'; /// Accumulate a list of Middleware that enhances Dispatch to the Store. /// The wrapped direction of the Store.dispatch is from inside to outside. StoreEnhancer applyMiddleware(List> middleware) { return middleware == null || middleware.isEmpty ? null : (StoreCreator creator) => (T initState, Reducer reducer) { assert(middleware != null && middleware.isNotEmpty); final Store store = creator(initState, reducer); final Dispatch initialValue = store.dispatch; store.dispatch = (Action action) { throw Exception( 'Dispatching while constructing your middleware is not allowed. ' 'Other middleware would not be applied to this dispatch.'); }; store.dispatch = middleware .map((Middleware middleware) => middleware( dispatch: (Action action) => store.dispatch(action), getState: store.getState, )) .fold( initialValue, (Dispatch previousValue, Dispatch Function(Dispatch) element) => element(previousValue), ); return store; }; } ================================================ FILE: lib/src/redux/basic.dart ================================================ import 'dart:async'; /// This document describes the core concepts under the Redux system and their standard definitions. /// Mainly includes: /// 1. The concepts of ReduxJs community /// Action ---- Definition of intention by plain object /// Reducer ---- How to modify the data by a pure function /// Dispatch ---- Expression of intention /// Middleware ---- AOP /// Store ---- State management center /// 2. Additional abstractions beyond the basic concepts of the ReduxJs community. /// Connector ---- The connection between big object and small object

/// SubReducer ---- A function that modifies data of partial /// The role of this layer of abstraction /// a. It is obvious that the implementation of combineReducers are decoupled with the grammatical features of JS /// b. The deeper is the contradiction between the centralization of Redux and the division of components can be solved. /// Action is a way of defining "intention". /// 1. It emphasizes the clarity of an intention, not the implementation of the intent. /// 2. Usually the implementation of the intent is done by Effect or Reducer. /// 3. type: indicates the type of intent; payload: the original information loaded with the intent. /// 4. Action definitions and standards, strictly follow the definition and standards of Action in the Redux community. class Action { const Action(this.type, {this.payload}); final Object type; final dynamic payload; } /// Definition of the standard Reducer. /// If the Reducer needs to respond to the Action, it returns a new state, otherwise it returns the old state. typedef Reducer = T Function(T state, Action action); /// Definition of the standard Dispatch. /// Send an "intention". typedef Dispatch = dynamic Function(Action action); /// Definition of a standard subscription function. /// input a subscriber and output an anti-subscription function. typedef Subscribe = void Function() Function(void Function() callback); /// ReplaceReducer 的定义 typedef ReplaceReducer = void Function(Reducer reducer); /// Definition of the standard observable flow. typedef Observable = Stream Function(); /// Definition of synthesizable functions. typedef Composable = T Function(T next); /// Definition of the function type that returns type R. typedef Get = R Function(); /// Definition of the standard Middleware. typedef Middleware = Composable Function({ Dispatch dispatch, Get getState, }); /// Definition of the standard Store. class Store { Get getState; Dispatch dispatch; Subscribe subscribe; Observable observable; ReplaceReducer replaceReducer; Future Function() teardown; } /// Create a store definition typedef StoreCreator = Store Function( T preloadedState, Reducer reducer, ); /// Definition of Enhanced creating a store typedef StoreEnhancer = StoreCreator Function(StoreCreator creator); /// Definition of SubReducer /// [isStateCopied] is Used to optimize execution performance. /// Ensure that a T will be cloned at most once during the entire process. typedef SubReducer = T Function(T state, Action action, bool isStateCopied); /// Definition of Connector which connects Reducer with Reducer

. /// 1. How to get an instance of type P from an instance of type S. /// 2. How to synchronize changes of an instance of type P to an instance of type S. /// 3. How to clone a new S. abstract class AbstractConnector { P get(S state); /// For mutable state, there are three abilities needed to be met. /// 1. get: (S) => P /// 2. set: (S, P) => void /// 3. shallow copy: s.clone() /// /// For immutable state, there are two abilities needed to be met. /// 1. get: (S) => P /// 2. set: (S, P) => S /// /// See in [connector]. SubReducer subReducer(Reducer

reducer); } ================================================ FILE: lib/src/redux/combine_reducers.dart ================================================ import 'basic.dart'; /// Combine an iterable of SubReducer into one Reducer Reducer combineSubReducers(Iterable> subReducers) { final List> notNullReducers = subReducers ?.where((SubReducer e) => e != null) ?.toList(growable: false); if (notNullReducers == null || notNullReducers.isEmpty) { return null; } if (notNullReducers.length == 1) { final SubReducer single = notNullReducers.single; return (T state, Action action) => single(state, action, false); } return (T state, Action action) { T copy = state; bool hasChanged = false; for (SubReducer subReducer in notNullReducers) { copy = subReducer(copy, action, hasChanged); hasChanged = hasChanged || copy != state; } assert(copy != null); return copy; }; } /// Combine an iterable of Reducer into one Reducer Reducer combineReducers(Iterable> reducers) { final List> notNullReducers = reducers?.where((Reducer r) => r != null)?.toList(growable: false); if (notNullReducers == null || notNullReducers.isEmpty) { return null; } if (notNullReducers.length == 1) { return notNullReducers.single; } return (T state, Action action) { T nextState = state; for (Reducer reducer in notNullReducers) { nextState = reducer(nextState, action); } assert(nextState != null); return nextState; }; } /// Convert a super Reducer to a sub Reducer Reducer castReducer(Reducer sup) { return sup == null ? null : (Sub state, Action action) { final Sub result = sup(state, action); return result; }; } ================================================ FILE: lib/src/redux/connector.dart ================================================ import 'basic.dart'; /// Define a basic connector for immutable state. /// /// Example: /// class State { /// final SubState sub; /// final String name; /// const State({this.sub, this.name}); /// } /// /// class SubState {} /// /// class Conn extends ImmutableConn { /// SubState get(State state) => state.sub; /// State set(State state, SubState sub) => State(sub: sub, name: state.name); /// } abstract class ImmutableConn implements AbstractConnector { const ImmutableConn(); T set(T state, P subState); @override SubReducer subReducer(Reducer

reducer) { return reducer == null ? null : (T state, Action action, bool isStateCopied) { final P props = get(state); if (props == null) { return state; } final P newProps = reducer(props, action); final bool hasChanged = !identical(newProps, props); if (hasChanged) { final T result = set(state, newProps); assert(result != null, 'Expected to return a non-null value.'); return result; } return state; }; } } /// Definition of Cloneable abstract class Cloneable> { T clone(); } /// how to clone an object dynamic _clone(T state) { if (state is Cloneable) { return state.clone(); } else if (state is List) { return state.toList(); } else if (state is Map) { return {}..addAll(state); } else if (state == null) { return null; } else { throw ArgumentError( 'Could not clone this state of type ${state.runtimeType}.'); } } /// Define a basic connector for mutable state. /// /// Example: /// class State implments Cloneable{ /// SubState sub; /// String name; /// State({this.sub, this.name}); /// /// State clone() => State(sub: sub, name: name); /// } /// /// class SubState {} /// /// class Conn extends MutableConn { /// SubState get(State state) => state.sub; /// void set(State state, SubState sub) => state.sub = sub; /// } abstract class MutableConn implements AbstractConnector { const MutableConn(); void set(T state, P subState); @override SubReducer subReducer(Reducer

reducer) { return reducer == null ? null : (T state, Action action, bool isStateCopied) { final P props = get(state); if (props == null) { return state; } final P newProps = reducer(props, action); final bool hasChanged = newProps != props; final T copy = (hasChanged && !isStateCopied) ? _clone(state) : state; if (hasChanged) { set(copy, newProps); } return copy; }; } } ================================================ FILE: lib/src/redux/create_store.dart ================================================ import 'dart:async'; import 'basic.dart'; Reducer _noop() => (T state, Action action) => state; typedef _VoidCallback = void Function(); void _throwIfNot(bool condition, [String message]) { if (!condition) { throw ArgumentError(message); } } Store _createStore(final T preloadedState, final Reducer reducer) { _throwIfNot( preloadedState != null, 'Expected the preloadedState to be non-null value.', ); final List<_VoidCallback> _listeners = <_VoidCallback>[]; final StreamController _notifyController = StreamController.broadcast(sync: true); T _state = preloadedState; Reducer _reducer = reducer ?? _noop(); bool _isDispatching = false; bool _isDisposed = false; return Store() ..getState = (() => _state) ..dispatch = (Action action) { _throwIfNot(action != null, 'Expected the action to be non-null value.'); _throwIfNot(action.type != null, 'Expected the action.type to be non-null value.'); _throwIfNot(!_isDispatching, 'Reducers may not dispatch actions.'); if (_isDisposed) { return; } try { _isDispatching = true; _state = _reducer(_state, action); } finally { _isDispatching = false; } final List<_VoidCallback> _notifyListeners = _listeners.toList( growable: false, ); for (_VoidCallback listener in _notifyListeners) { listener(); } _notifyController.add(_state); } ..replaceReducer = (Reducer replaceReducer) { _reducer = replaceReducer ?? _noop; } ..subscribe = (_VoidCallback listener) { _throwIfNot( listener != null, 'Expected the listener to be non-null value.', ); _throwIfNot( !_isDispatching, 'You may not call store.subscribe() while the reducer is executing.', ); _listeners.add(listener); return () { _throwIfNot( !_isDispatching, 'You may not unsubscribe from a store listener while the reducer is executing.', ); _listeners.remove(listener); }; } ..observable = (() => _notifyController.stream) ..teardown = () { _isDisposed = true; _listeners.clear(); return _notifyController.close(); }; } /// create a store with enhancer Store createStore(T preloadedState, Reducer reducer, [StoreEnhancer enhancer]) => enhancer != null ? enhancer(_createStore)(preloadedState, reducer) : _createStore(preloadedState, reducer); StoreEnhancer composeStoreEnhancer(List> enhancers) => enhancers == null || enhancers.isEmpty ? null : enhancers.reduce((StoreEnhancer previous, StoreEnhancer next) => (StoreCreator creator) => next(previous(creator))); ================================================ FILE: lib/src/redux/redux.dart ================================================ export 'apply_middleware.dart'; export 'basic.dart'; export 'combine_reducers.dart'; export 'connector.dart'; export 'create_store.dart'; ================================================ FILE: lib/src/redux_adapter/adapter.dart ================================================ import 'package:flutter/widgets.dart' hide Action, Page; import '../redux/redux.dart'; import '../redux_component/redux_component.dart'; /// abstract for custom extends abstract class Adapter extends Logic implements AbstractAdapter { final AdapterBuilder _adapter; AdapterBuilder get protectedAdapter => _adapter; Adapter({ @required AdapterBuilder adapter, Reducer reducer, ReducerFilter filter, Effect effect, Dependencies dependencies, @deprecated Object Function(T) key, }) : assert(adapter != null), assert(dependencies?.adapter == null, 'Unexpected dependencies.list for Adapter.'), _adapter = adapter, super( reducer: reducer, filter: filter, effect: effect, dependencies: dependencies, // ignore:deprecated_member_use_from_same_package key: key, ); @override ListAdapter buildAdapter(ContextSys ctx) => ctx.enhancer ?.adapterEnhance(protectedAdapter, this, ctx.store) ?.call(ctx.state, ctx.dispatch, ctx) ?? protectedAdapter?.call(ctx.state, ctx.dispatch, ctx); @override ContextSys createContext( Store store, BuildContext buildContext, Get getState, { @required Enhancer enhancer, @required DispatchBus bus, }) { assert(bus != null && enhancer != null); return AdapterContext( logic: this, store: store, buildContext: buildContext, getState: getState, bus: bus, enhancer: enhancer, ); } } class AdapterContext extends LogicContext { AdapterContext({ @required AbstractAdapter logic, @required Store store, @required BuildContext buildContext, @required Get getState, @required DispatchBus bus, @required Enhancer enhancer, }) : assert(bus != null && enhancer != null), super( logic: logic, store: store, buildContext: buildContext, getState: getState, bus: bus, enhancer: enhancer, ); @override ListAdapter buildAdapter() { final AbstractAdapter curLogic = logic; return curLogic.buildAdapter(this); } } ================================================ FILE: lib/src/redux_adapter/dynamic_flow_adapter.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/widgets.dart' hide Action, Page; import '../redux/redux.dart'; import '../redux_component/redux_component.dart'; import '../utils/utils.dart'; import 'recycle_context.dart'; /// template is a map, driven by array /// Use [SimpleFlowAdapter.dynamic] instead of [DynamicFlowAdapter] /// see in example @deprecated class DynamicFlowAdapter extends Logic with RecycleContextMixin { final Map> pool; final AbstractConnector> connector; DynamicFlowAdapter({ @required this.pool, @required this.connector, ReducerFilter filter, Reducer reducer, Effect effect, /// implement [StateKey] in T instead of using key in Logic. /// class T implements StateKey { /// Object _key = UniqueKey(); /// Object key() => _key; /// } @deprecated Object Function(T) key, }) : super( reducer: _dynamicReducer(reducer, pool, connector), effect: effect, filter: filter, dependencies: null, // ignore:deprecated_member_use_from_same_package key: key, ); @override ListAdapter buildAdapter(ContextSys ctx) { final List list = connector.get(ctx.state); assert(list != null); final RecycleContext recycleCtx = ctx; final List adapters = []; recycleCtx.markAllUnused(); for (int index = 0; index < list.length; index++) { final ItemBean itemBean = list[index]; final String type = itemBean.type; final AbstractLogic result = pool[type]; assert( result != null, 'Type of $type has not benn registered in the pool.'); if (result != null) { if (result is AbstractAdapter) { final ContextSys subCtx = recycleCtx.reuseOrCreate( Tuple2( result.runtimeType, result.key(itemBean.data), ), () => result.createContext( recycleCtx.store, recycleCtx.context, _subGetter(() => connector.get(recycleCtx.state), index), bus: recycleCtx.bus, enhancer: recycleCtx.enhancer, ), ); /// hack to reduce adapter's rebuilding adapters.add(memoizeListAdapter(result, subCtx)); } else if (result is AbstractComponent) { adapters.add(ListAdapter((BuildContext buildContext, int _) { return result.buildComponent( recycleCtx.store, _subGetter(() => connector.get(recycleCtx.state), index), bus: recycleCtx.bus, enhancer: recycleCtx.enhancer, ); }, 1)); } } } recycleCtx.cleanUnused(); return combineListAdapters(adapters); } } /// Generate reducer for List and combine them into one Reducer _dynamicReducer( Reducer reducer, Map> pool, AbstractConnector> connector, ) { final Reducer> dyReducer = (List state, Action action) { List copy; for (int i = 0; i < state.length; i++) { final ItemBean itemBean = state[i]; final AbstractLogic result = pool[itemBean.type]; if (result != null) { final Object newData = result.onReducer(itemBean.data, action); if (newData != itemBean.data) { copy ??= state.toList(); copy[i] = itemBean.clone(data: newData); } } } return copy ?? state; }; return combineReducers(>[ reducer, combineSubReducers(>[connector.subReducer(dyReducer)]), ]); } /// Define itemBean how to get state with connector /// /// [_isSimilar] return true just use newState after reducer safely /// [_isSimilar] return false we should use cache state before reducer invoke. /// for reducer change state immediately but sub component will refresh on next /// frame. in this time the sub component will use cache state. Get _subGetter(Get> getter, int index) { final List curState = getter(); ItemBean cacheItem = curState[index]; return () { final List newState = getter(); /// Either all sub-components use cache or not. if (newState != null && newState.length > index) { final ItemBean newItem = newState[index]; if (_couldReuse(cacheItem, newItem)) { cacheItem = newItem; } } return cacheItem.data; }; } bool _couldReuse(ItemBean beanA, ItemBean beanB) { if (beanA.type != beanB.type) { return false; } final Object dataA = beanA.data; final Object dataB = beanB.data; if (dataA.runtimeType != dataB.runtimeType) { return false; } final Object keyA = dataA is StateKey ? dataA.key() : null; final Object keyB = dataB is StateKey ? dataB.key() : null; return keyA == keyB; } ================================================ FILE: lib/src/redux_adapter/flow_adapter.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/widgets.dart' hide Action, Page; import '../redux/redux.dart'; import '../redux_component/redux_component.dart'; import 'recycle_context.dart'; class FlowAdapter extends Logic with RecycleContextMixin implements AbstractAdapter { final FlowDependencies _flowDependencies; FlowAdapter({ @required FlowAdapterView view, ReducerFilter filter, Reducer reducer, Effect effect, @deprecated Object Function(T state) key, }) : assert(view != null), _flowDependencies = FlowDependencies(_memoize>(view)), super( reducer: reducer, effect: effect, filter: filter, dependencies: null, // ignore: deprecated_member_use_from_same_package key: key, ); @override Reducer get protectedDependenciesReducer =>_flowDependencies.createReducer(); @override ListAdapter buildAdapter(ContextSys ctx) { final T state = ctx.state; final DependentArray depArray = _flowDependencies.build(state); final RecycleContext recycleCtx = ctx; final List adapters = []; recycleCtx.markAllUnused(); final int count = depArray.length; for (int index = 0; index < count; index++) { final Dependent dependent = depArray[index]; if (dependent == null) { continue; } if (dependent.isAdapter()) { /// use dependent's key final ContextSys subCtx = recycleCtx.reuseOrCreate( dependent.key(state), () { return dependent.createContext( recycleCtx.store, recycleCtx.context, recycleCtx.getState, bus: recycleCtx.bus, enhancer: recycleCtx.enhancer, ); }, ); adapters.add(dependent.buildAdapter(subCtx)); } else if (dependent.isComponent()) { adapters.add(ListAdapter((BuildContext buildContext, int index) { return dependent.buildComponent( recycleCtx.store, recycleCtx.getState, bus: recycleCtx.bus, enhancer: recycleCtx.enhancer, ); }, 1)); } } recycleCtx.cleanUnused(); return combineListAdapters(adapters); } } ////////////////////////////////////////// typedef IndexedDependentBuilder = Dependent Function(int); class DependentArray { final IndexedDependentBuilder builder; final int length; DependentArray({@required this.builder, @required this.length}) : assert(builder != null && length >= 0); DependentArray.fromList(List> list) : this(builder: (int index) => list[index], length: list.length); Dependent operator [](int index) => builder(index); } typedef FlowAdapterView = DependentArray Function(T); class FlowDependencies { final FlowAdapterView build; const FlowDependencies(this.build); Reducer createReducer() => (T state, Action action) { T copy = state; bool hasChanged = false; final DependentArray list = build(state); if (list != null) { for (int i = 0; i < list.length; i++) { final Dependent dep = list[i]; final SubReducer subReducer = dep?.createSubReducer(); if (subReducer != null) { copy = subReducer(copy, action, hasChanged); hasChanged = hasChanged || copy != state; } } } return copy; }; } ////////////////////////////////////////// /// Use [ItemListLike] instead of [List] abstract class ItemListLike { int get itemCount; String getItemType(int index); Object getItemData(int index); ItemListLike updateItemData(int index, Object data, bool isStateCopied); } abstract class MutableItemListLike extends ItemListLike { @mustCallSuper @override MutableItemListLike updateItemData( int index, Object data, bool isStateCopied) { final MutableItemListLike result = isStateCopied ? this : clone(); return result..setItemData(index, data); } void setItemData(int index, Object data); MutableItemListLike clone(); } abstract class ImmutableItemListLike extends ItemListLike { @mustCallSuper @override ImmutableItemListLike updateItemData( int index, Object data, bool isStateCopied) => setItemData(index, data); ImmutableItemListLike setItemData(int index, Object data); ImmutableItemListLike clone(); } ////////////////////////////////////////// class ItemBean { final String type; final Object data; const ItemBean(this.type, this.data); ItemBean clone({String type, Object data}) => ItemBean(type ?? this.type, data ?? this.data); } /// Optimize flow-adapter-view performance R Function(P) _memoize(R Function(P) functor) { bool hasInvoked = false; P cahcedKey; R cachedValue; return (P param) { if (!hasInvoked) { hasInvoked = true; cahcedKey = param; cachedValue = functor(param); } else if (param != cahcedKey) { cahcedKey = param; cachedValue = functor(param); } return cachedValue; }; } ================================================ FILE: lib/src/redux_adapter/recycle_context.dart ================================================ import 'package:flutter/widgets.dart' hide Action, Page; import '../redux/redux.dart'; import '../redux_component/redux_component.dart'; import 'adapter.dart'; class RecycleContext extends AdapterContext { final Map>> _cachedMap = >>{}; final Map _usedIndexMap = {}; RecycleContext({ @required AbstractAdapter logic, @required @required Store store, @required BuildContext buildContext, @required Get getState, @required DispatchBus bus, @required Enhancer enhancer, }) : super( logic: logic, store: store, buildContext: buildContext, getState: getState, bus: bus, enhancer: enhancer, ); @override void onLifecycle(Action action) { _cachedMap.forEach((Object key, List> list) { for (ContextSys sub in list) { sub.onLifecycle(action); } }); super.onLifecycle(action); } void markAllUnused() { _usedIndexMap.clear(); } ContextSys reuseOrCreate(Object key, Get> create) { final int length = _usedIndexMap[key] = (_usedIndexMap[key] ?? 0) + 1; final List> list = _cachedMap[key] ??= >[]; if (length > list.length) { _cachedMap[key].add( create() ..setParent(this) ..onLifecycle(LifecycleCreator.initState()), ); } return list[length - 1]; } void cleanUnused() { _cachedMap.removeWhere((Object key, List> value) { final int usedCount = _usedIndexMap[key] ?? 0; for (int i = usedCount; i < value.length; i++) { value[i].onLifecycle(LifecycleCreator.dispose()); value[i].dispose(); } value.removeRange(usedCount, value.length); return usedCount == 0; }); } } mixin RecycleContextMixin implements AbstractAdapter { @override RecycleContext createContext( Store store, BuildContext buildContext, Get getState, { @required DispatchBus bus, @required Enhancer enhancer, }) { assert(bus != null && enhancer != null); return RecycleContext( logic: this, store: store, buildContext: buildContext, getState: asGetter(getState), bus: bus, enhancer: enhancer, ); } } ListAdapter combineListAdapters(Iterable adapters) { final List list = adapters .where((ListAdapter e) => e != null && e.itemCount > 0) .toList(growable: false); if (list.every((ListAdapter e) => e.itemCount == 1)) { /// The result is AbstractComponent return ListAdapter( (BuildContext buildContext, final int index) => list[index].itemBuilder(buildContext, 0), list.length, ); } else if (list.length == 1) { return list.single; } final int maxItemCount = list.fold(0, (int count, ListAdapter adapter) { return count + adapter.itemCount; }); /// The result is AbstractAdapter return ListAdapter( (BuildContext buildContext, final int index) { assert(index >= 0 && index < maxItemCount); int yIndex = index; int xIndex = 0; while (xIndex < list.length && list[xIndex].itemCount <= yIndex) { yIndex -= list[xIndex].itemCount; xIndex++; } assert(xIndex < list.length); return list[xIndex].itemBuilder(buildContext, yIndex); }, maxItemCount, ); } ListAdapter memoizeListAdapter( AbstractAdapterBuilder result, ContextSys subCtx, ) { final Object newState = subCtx.state; if (subCtx.extra['@last-state'] != newState) { subCtx.extra['@last-state'] = newState; subCtx.extra['@last-adapter'] = _memoizeListAdapter(result.buildAdapter(subCtx)); } return subCtx.extra['@last-adapter']; } ListAdapter _memoizeListAdapter(ListAdapter adapter) { if (adapter.itemCount > 0) { final List memoized = List.filled(adapter.itemCount, null, growable: false); return ListAdapter((BuildContext context, int index) { return (memoized[index] ??= adapter.itemBuilder(context, index)); }, adapter.itemCount); } else { return adapter; } } ================================================ FILE: lib/src/redux_adapter/redux_adapter.dart ================================================ export 'adapter.dart'; export 'dynamic_flow_adapter.dart'; export 'flow_adapter.dart'; export 'recycle_context.dart'; export 'source_flow_adapter.dart'; export 'static_flow_adapter.dart'; ================================================ FILE: lib/src/redux_adapter/source_flow_adapter.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/widgets.dart' hide Action, Page; import '../redux/redux.dart'; import '../redux_component/redux_component.dart'; import '../utils/utils.dart'; import 'recycle_context.dart'; /// template is a map, driven by array /// Use [FlowAdapter.source] instead of [SourceFlowAdapter] /// see in example /// template is a map, driven by source @deprecated class SourceFlowAdapter extends Logic with RecycleContextMixin { final Map> pool; SourceFlowAdapter({ @required this.pool, ReducerFilter filter, Reducer reducer, Effect effect, /// implement [StateKey] in T instead of using key in Logic. /// class T implements StateKey { /// Object _key = UniqueKey(); /// Object key() => _key; /// } @deprecated Object Function(T) key, }) : super( reducer: _dynamicReducer(reducer, pool), effect: effect, filter: filter, dependencies: null, // ignore:deprecated_member_use_from_same_package key: key, ); @override ListAdapter buildAdapter(ContextSys ctx) { final ItemListLike adapterSource = ctx.state; assert(adapterSource != null); final RecycleContext recycleCtx = ctx; final List adapters = []; recycleCtx.markAllUnused(); for (int index = 0; index < adapterSource.itemCount; index++) { final String type = adapterSource.getItemType(index); final AbstractLogic result = pool[type]; assert( result != null, 'Type of $type has not benn registered in the pool.'); if (result != null) { if (result is AbstractAdapter) { final ContextSys subCtx = recycleCtx.reuseOrCreate( Tuple2( result.runtimeType, result.key(adapterSource.getItemData(index)), ), () => result.createContext( recycleCtx.store, recycleCtx.context, _subGetter(() => recycleCtx.state, index), bus: recycleCtx.bus, enhancer: recycleCtx.enhancer, ), ); /// hack to reduce adapter's rebuilding adapters.add(memoizeListAdapter(result, subCtx)); } else if (result is AbstractComponent) { adapters.add(ListAdapter((BuildContext buildContext, int _) { return result.buildComponent( recycleCtx.store, _subGetter(() => recycleCtx.state, index), bus: recycleCtx.bus, enhancer: recycleCtx.enhancer, ); }, 1)); } } } recycleCtx.cleanUnused(); return combineListAdapters(adapters); } } /// Generate reducer for List and combine them into one Reducer _dynamicReducer( Reducer reducer, Map> pool, ) { final Reducer dyReducer = (ItemListLike state, Action action) { ItemListLike copy; for (int i = 0; i < state.itemCount; i++) { final AbstractLogic result = pool[state.getItemType(i)]; if (result != null) { final Object oldData = state.getItemData(i); final Object newData = result.onReducer(oldData, action); if (newData != oldData) { copy = state.updateItemData(i, newData, copy != null); } } } return copy ?? state; }; return combineReducers(>[reducer, dyReducer]); } /// Define itemBean how to get state with connector /// /// [_isSimilar] return true just use newState after reducer safely /// [_isSimilar] return false we should use cache state before reducer invoke. /// for reducer change state immediately but sub component will refresh on next /// frame. in this time the sub component will use cache state. Get _subGetter(Get getter, int index) { final ItemListLike curState = getter(); String type = curState.getItemType(index); Object data = curState.getItemData(index); return () { final ItemListLike newState = getter(); /// Either all sub-components use cache or not. if (newState != null && newState.itemCount > index) { final String newType = newState.getItemType(index); final Object newData = newState.getItemData(index); if (_couldReuse( typeA: type, typeB: newType, dataA: data, dataB: newData, )) { type = newType; data = newData; } } return data; }; } bool _couldReuse({String typeA, String typeB, Object dataA, Object dataB}) { return typeA != typeB ? false : dataA.runtimeType != dataB.runtimeType ? false : (dataA is StateKey ? dataA.key() : null) == (dataB is StateKey ? dataB.key() : null); } ================================================ FILE: lib/src/redux_adapter/static_flow_adapter.dart ================================================ import 'package:flutter/widgets.dart' hide Action, Page; import '../redux/redux.dart'; import '../redux_component/redux_component.dart'; import '../utils/utils.dart'; import 'recycle_context.dart'; /// template is a map, driven by array /// Use [FlowAdapter.static] instead of [StaticFlowAdapter] /// see in example /// template is an array, driven by map like @deprecated class StaticFlowAdapter extends Logic with RecycleContextMixin implements AbstractAdapter { final List> _slots; StaticFlowAdapter({ @required List> slots, Reducer reducer, Effect effect, ReducerFilter filter, /// implement [StateKey] in T instead of using key in Logic. /// class T implements StateKey { /// Object _key = UniqueKey(); /// Object key() => _key; /// } @deprecated Object Function(T) key, }) : assert(slots != null), _slots = Collections.compact(slots), super( reducer: combineReducers(>[ reducer, combineSubReducers( slots.map( (Dependent dependent) => dependent?.createSubReducer(), ), ) ]), effect: effect, filter: filter, dependencies: null, // ignore:deprecated_member_use_from_same_package key: key, ); @override ListAdapter buildAdapter(ContextSys ctx) { final RecycleContext recycleCtx = ctx; final List adapters = []; recycleCtx.markAllUnused(); for (int i = 0; i < _slots.length; i++) { final Dependent dependent = _slots[i]; final Object subObject = dependent.subGetter(recycleCtx.getState)(); if (!dependent.isComponent()) { /// precondition is subObject != null if (subObject != null) { /// use index of key final ContextSys subCtx = recycleCtx.reuseOrCreate(i, () { return dependent.createContext( recycleCtx.store, recycleCtx.context, recycleCtx.getState, bus: recycleCtx.bus, enhancer: recycleCtx.enhancer, ); }); /// hack to reduce adapter's rebuilding adapters.add(memoizeListAdapter(dependent, subCtx)); } } else if (subObject != null) { adapters.add(ListAdapter((BuildContext buildContext, int index) { return dependent.buildComponent( recycleCtx.store, recycleCtx.getState, bus: recycleCtx.bus, enhancer: recycleCtx.enhancer, ); }, 1)); } } recycleCtx.cleanUnused(); return combineListAdapters(adapters); } } ================================================ FILE: lib/src/redux_aop/aop.dart ================================================ typedef TypedApplyLike = R Function(List, [Map]); /// Unified abstraction of functions which used in [Function.apply] typedef ApplyLike = dynamic Function(List, [Map]); /// Unified abstraction of function AOP, input one function output another with some enhancement inside. typedef ApplyLikeEnhancer = ApplyLike Function(ApplyLike functor); ApplyLike _identity(ApplyLike f) => f; ApplyLikeEnhancer _combine(ApplyLikeEnhancer e0, ApplyLikeEnhancer e1) => (ApplyLike f) => (e1 ?? _identity)((e0 ?? _identity)(f)); const ApplyLikeEnhancer ApplyLikeEnhancerIdentity = _identity; /// Implement AOP with Currying tec. /// [AOP]: https://en.wikipedia.org/wiki/Aspect-oriented_programming /// [Currying]: https://en.wikipedia.org/wiki/Currying /// Process /// 1. Input user [Function] /// 2. Cast to [ApplyLike] /// 3. Add some enhancement (by [ApplyLikeEnhancer]) /// 4. Get new [ApplyLike] /// 5. Cast to [TypedApplyLike] /// 6. Cast to user [Function] class AOP { final ApplyLikeEnhancer _enhancer; AOP(List enhances) : _enhancer = enhances?.isNotEmpty == true ? enhances.reduce(_combine) : ApplyLikeEnhancerIdentity; TypedApplyLike enhance(Function functor) { /// cast functor to ApplyLike final ApplyLike init = (List positionalArguments, [Map namedArguments]) => Function.apply(functor, positionalArguments, namedArguments); /// enhance ApplyLike final ApplyLike enhanced = _enhancer(init); /// if not enhanced if (init == enhanced) { return null; } /// cast ApplyLike to TypedApplyLike return (List positionalArguments, [Map namedArguments]) { final R result = enhanced(positionalArguments); return result; }; } R Function() withZero(R Function() f) { final TypedApplyLike enhanced = enhance(f); return enhanced != null ? () => enhanced([]) : f; } R Function(P) withOne(R Function(P) f) { final TypedApplyLike enhanced = enhance(f); return enhanced != null ? (P p) => enhanced([p]) : f; } R Function(P0, P1) withTwo(R Function(P0, P1) f) { final R Function(List) enhanced = enhance(f); return enhanced != null ? (P0 p0, P1 p1) => enhanced([p0, p1]) : f; } R Function(P0, P1, P2) withThree(R Function(P0, P1, P2) f) { final TypedApplyLike enhanced = enhance(f); return enhanced != null ? (P0 p0, P1 p1, P2 p2) => enhanced([p0, p1, p2]) : f; } R Function(P0, P1, P2, P3) withFour( R Function(P0, P1, P2, P3) f) { final TypedApplyLike enhanced = enhance(f); return enhanced != null ? (P0 p0, P1 p1, P2 p2, P3 p3) => enhanced([p0, p1, p2, p3]) : f; } R Function(P0, P1, P2, P3, P4) withFive( R Function(P0, P1, P2, P3, P4) f) { final TypedApplyLike enhanced = enhance(f); return enhanced != null ? (P0 p0, P1 p1, P2 p2, P3 p3, P4 p4) => enhanced([p0, p1, p2, p3, p4]) : f; } R Function(P0, P1, P2, P3, P4, P5) withSix( R Function(P0, P1, P2, P3, P4, P5) f) { final TypedApplyLike enhanced = enhance(f); return enhanced != null ? (P0 p0, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5) => enhanced([p0, p1, p2, p3, p4, p5]) : f; } } ================================================ FILE: lib/src/redux_aop/common_aop/common_aop.dart ================================================ export 'debounce.dart'; export 'delay.dart'; export 'log.dart'; export 'memoize.dart'; export 'performance.dart'; export 'throttle.dart'; export 'wait_until.dart'; ================================================ FILE: lib/src/redux_aop/common_aop/debounce.dart ================================================ import 'dart:async'; import '../aop.dart'; /// debounce the stream, means the [millis] span functor call once and drop other event. /// it difference with [throttle]. ApplyLikeEnhancer debounce(int millis) { return (dynamic Function(List) functor) { int idGenerator = 0; return (List positionalArguments, [Map namedArguments]) async { final int newId = ++idGenerator; await Future.delayed(Duration(milliseconds: millis)); if (newId == idGenerator) { return functor(positionalArguments); } }; }; } ================================================ FILE: lib/src/redux_aop/common_aop/debug.dart ================================================ bool _debugFlag = false; /// Is app run a debug mode. bool isDebug() { /// Assert statements have no effect in production code; /// they’re for development only. Flutter enables asserts in debug mode. assert(() { _debugFlag = true; return _debugFlag; }()); return _debugFlag; } ================================================ FILE: lib/src/redux_aop/common_aop/delay.dart ================================================ import 'dart:async'; import '../aop.dart'; /// functor will be call after [millis]. ApplyLikeEnhancer delay(int millis) { return (dynamic Function(List) functor) { return (List positionalArguments, [Map namedArguments]) async { await Future.delayed(Duration(milliseconds: millis)); return functor(positionalArguments); }; }; } ================================================ FILE: lib/src/redux_aop/common_aop/log.dart ================================================ import 'dart:async'; import '../aop.dart'; import 'debug.dart'; /// AOP for functor log. ApplyLikeEnhancer logAOP(String tag) { return isDebug() ? (dynamic Function(List) functor) { return (List positionalArguments, [Map namedArguments]) { print('$tag input: $positionalArguments'); final Object result = functor(positionalArguments); if (result is Future) { result.then((Object r) { print('$tag output : $r'); return r; }); } else { print('$tag output: $result'); } return result; }; } : ApplyLikeEnhancerIdentity; } ================================================ FILE: lib/src/redux_aop/common_aop/memoize.dart ================================================ import '../aop.dart'; bool _listEquals(List list1, List list2) { if (identical(list1, list2)) { return true; } if (list1 == null || list2 == null) { return false; } final int length = list1.length; if (length != list2.length) { return false; } for (int i = 0; i < length; i++) { if (list1[i] != list2[i]) { return false; } } return true; } /// memoize returns cached result of function call when inputs were not changed from previous invocation. ApplyLikeEnhancer memoize() { return (dynamic Function(List) functor) { List memoizeArguments; dynamic memoizeResult; bool hasBeenCalled = false; return (List positionalArguments, [Map namedArguments]) { if (!hasBeenCalled || !_listEquals(positionalArguments, memoizeArguments)) { memoizeResult = functor(positionalArguments); memoizeArguments = positionalArguments; hasBeenCalled = true; } return memoizeResult; }; }; } ================================================ FILE: lib/src/redux_aop/common_aop/performance.dart ================================================ import 'dart:async'; import '../aop.dart'; import 'debug.dart'; int _microSecsSinceEpoch() => DateTime.now().microsecondsSinceEpoch; /// functor performance by time consuming. ApplyLikeEnhancer performanceAOP(String tag) { return isDebug() ? (dynamic Function(List) functor) { return (List positionalArguments, [Map namedArguments]) { final int marked = DateTime.now().microsecondsSinceEpoch; final Object result = functor(positionalArguments); if (result is Future) { result.then((Object r) { print( '$tag performance : ${_microSecsSinceEpoch() - marked}'); return r; }); } else { print('$tag performance: ${_microSecsSinceEpoch() - marked}'); } return result; }; } : ApplyLikeEnhancerIdentity; } ================================================ FILE: lib/src/redux_aop/common_aop/throttle.dart ================================================ import '../aop.dart'; int _microSecsSinceEpoch() => DateTime.now().microsecondsSinceEpoch; /// throttle the stream, means every [millis] span functor call once. /// it difference with [debounce]. ApplyLikeEnhancer throttle(int millis) { return (dynamic Function(List) functor) { int last = 0; return (List positionalArguments, [Map namedArguments]) { final int now = _microSecsSinceEpoch(); final int elapsed = now - last; if (elapsed >= millis) { last = now; return functor(positionalArguments); } }; }; } ================================================ FILE: lib/src/redux_aop/common_aop/wait_until.dart ================================================ import 'dart:async'; import '../aop.dart'; /// Wait the future return. ApplyLikeEnhancer waitUntil() { return (dynamic Function(List) functor) { bool isLocked = false; return (List positionalArguments, [Map namedArguments]) { if (isLocked) { return null; } else { final Object result = functor(positionalArguments); if (result is Future) { isLocked = true; return result.whenComplete(() { isLocked = false; }); } return result; } }; }; } ================================================ FILE: lib/src/redux_aop/redux_aop.dart ================================================ export 'aop.dart'; export 'common_aop/common_aop.dart'; ================================================ FILE: lib/src/redux_component/auto_dispose.dart ================================================ class _Fields { bool isDisposed = false; Set children; AutoDispose parent; void Function() onDisposed; } /// Ultra-lightweight lifecycle management system /// When an object's dispose is called /// 1. Dispose all children /// 2. Cut off the connection with parent /// 3. The hook function of onDisposed is triggered /// 4. Status marked as isDisposed = true class AutoDispose { final _Fields _fields = _Fields(); void visit(void Function(AutoDispose) visitor) => _fields.children?.forEach(visitor); bool get isDisposed => _fields.isDisposed; void dispose() { /// dispose all children if (_fields.children != null) { final List copy = _fields.children.toList(growable: false); for (AutoDispose child in copy) { child.dispose(); } _fields.children = null; } /// Cut off the connection with parent. _fields.parent?._fields?.children?.remove(this); _fields.parent = null; /// The hook function of onDisposed is triggered. _fields.onDisposed?.call(); _fields.onDisposed = null; /// Status marked as isDisposed = true. _fields.isDisposed = true; } void onDisposed(void Function() onDisposed) { assert(_fields.onDisposed == null); if (_fields.isDisposed) { onDisposed?.call(); } else { _fields.onDisposed = onDisposed; } } void setParent(AutoDispose newParent) { assert(newParent != this); final AutoDispose oldParent = _fields.parent; if (oldParent == newParent || isDisposed) { return; } if (newParent != null && newParent.isDisposed) { dispose(); return; } if (newParent != null) { newParent._fields.children ??= {}; newParent._fields.children.add(this); } if (oldParent != null) { oldParent._fields.children.remove(this); } _fields.parent = newParent; } AutoDispose registerOnDisposed(void Function() onDisposed) => AutoDispose() ..setParent(this) ..onDisposed(onDisposed); } ================================================ FILE: lib/src/redux_component/basic.dart ================================================ import 'package:flutter/widgets.dart' hide Action, Page; import '../redux/redux.dart'; import 'auto_dispose.dart'; /// Component's view part /// 1.State is used to decide how to render /// 2.Dispatch is used to send actions /// 3.ViewService is used to build sub-components or adapter. typedef ViewBuilder = Widget Function( T state, Dispatch dispatch, ViewService viewService, ); /// Define a base ListAdapter which is used for ListView.builder. /// Many small listAdapters could be merged to a bigger one. class ListAdapter { final int itemCount; final IndexedWidgetBuilder itemBuilder; const ListAdapter(this.itemBuilder, this.itemCount); } /// Adapter's view part /// 1.State is used to decide how to render /// 2.Dispatch is used to send actions /// 3.ViewService is used to build sub-components or adapter. typedef AdapterBuilder = ListAdapter Function( T state, Dispatch dispatch, ViewService viewService, ); /// Data driven ui /// 1. How to render /// 2. When to update abstract class ViewUpdater { Widget buildWidget(); void didUpdateWidget(); void onNotify(); void forceUpdate(); void clearCache(); } /// A little different with Dispatch (with if it is interrupted). /// bool for sync-functions, interrupted if true /// Future for async-functions, should always be interrupted. // typedef OnAction = Dispatch; /// Predicate if a component should be updated when the store is changed. typedef ShouldUpdate = bool Function(T old, T now); /// Interrupt if not null not false /// bool for sync-functions, interrupted if true /// Future for async-functions, should always be interrupted. typedef Effect = dynamic Function(Action action, Context ctx); /// AOP on view /// usage /// ViewMiddleware safetyView( /// {Widget Function(dynamic, StackTrace, /// {AbstractComponent component, Store store}) /// onError}) { /// return (AbstractComponent component, Store store) { /// return (ViewBuilder next) { /// return isDebug() /// ? next /// : (dynamic state, Dispatch dispatch, ViewService viewService) { /// try { /// return next(state, dispatch, viewService); /// } catch (e, stackTrace) { /// return onError?.call( /// e, /// stackTrace, /// component: component, /// store: store, /// ) ?? /// Container(width: 0, height: 0); /// } /// }; /// }; /// }; /// } typedef ViewMiddleware = Composable> Function( AbstractComponent, Store, ); /// AOP on adapter typedef AdapterMiddleware = Composable> Function( AbstractAdapter, Store, ); /// AOP on effect /// usage /// EffectMiddleware pageAnalyticsMiddleware() { /// return (AbstractLogic logic, Store store) { /// return (Effect effect) { /// return effect == null ? null : (Action action, Context ctx) { /// if (logic is Page) { /// print('${logic.runtimeType} ${action.type.toString()} ${ctx.hashCode}'); /// } /// return effect(action, ctx); /// }; /// }; /// }; /// } typedef EffectMiddleware = Composable> Function( AbstractLogic, Store, ); /// AOP in page on store, view, adapter, effect... abstract class Enhancer { ViewBuilder viewEnhance( ViewBuilder view, AbstractComponent component, Store store, ); AdapterBuilder adapterEnhance( AdapterBuilder adapterBuilder, AbstractAdapter logic, Store store, ); Effect effectEnhance( Effect effect, AbstractLogic logic, Store store, ); StoreCreator storeEnhance(StoreCreator creator); void unshift({ List> middleware, List> viewMiddleware, List> effectMiddleware, List> adapterMiddleware, }); void append({ List> middleware, List> viewMiddleware, List> effectMiddleware, List> adapterMiddleware, }); } /// AOP End abstract class ExtraData { /// Get|Set extra data in context if needed. Map get extra; } /// Seen in view-part or adapter-part abstract class ViewService implements ExtraData { /// The way to build adapter which is configured in Dependencies.list ListAdapter buildAdapter(); /// The way to build slot component which is configured in Dependencies.slots Widget buildComponent(String name, {Widget defaultWidget}); /// Get BuildContext from the host-widget BuildContext get context; /// Broadcast action(the intent) in app (inter-pages) void broadcast(Action action); /// Broadcast in all component receivers; /// Dispatch is enough. Use [Dispatch] instead of [broadcastEffect] /// [Dispatch] = [SelfEffect] | ([broadcastEffect] & [store.dispatch]) @deprecated void broadcastEffect(Action action, {bool excluded}); } /// Seen in effect-part abstract class Context extends AutoDispose implements ExtraData { /// Get the latest state T get state; /// The way to send action, which will be consumed by self, or by broadcast-module and store. dynamic dispatch(Action action); /// Get BuildContext from the host-widget BuildContext get context; /// In general, we should not need this field. /// When we have to use this field, it means that we have encountered difficulties. /// This is a contradiction between presentation & logical separation, and Flutter's Widgets system. /// /// How to use ? /// For example, we want to use SingleTickerProviderStateMixin /// We should /// 1. Define a new Component mixin SingleTickerProviderMixin /// class MyComponent extends Component with SingleTickerProviderMixin {} /// 2. Get the CustomStfState via context.stfState in Effect. /// /// Through BuildContext -> StatefulElement -> State /// final TickerProvider tickerProvider = context.stfState; /// AnimationController controller = AnimationController(vsync: tickerProvider); /// context.dispatch(ActionCreator.createController(controller)); State get stfState; /// The way to build slot component which is configured in Dependencies.slots /// such as custom mask or dialog Widget buildComponent(String name); /// Broadcast action in app (inter-stores) void broadcast(Action action); /// Broadcast in all component receivers; void broadcastEffect(Action action, {bool excluded}); /// add observable void Function() addObservable(Subscribe observable); void forceUpdate(); /// listen on the changes of some parts of . void Function() listen({ bool Function(T, T) isChanged, void Function() onChange, }); } /// Seen in framework-component abstract class ContextSys extends Context implements ViewService { /// Response to lifecycle calls void onLifecycle(Action action); void bindForceUpdate(void Function() forceUpdate); Store get store; Enhancer get enhancer; DispatchBus get bus; } abstract class AbstractAdapterBuilder { ListAdapter buildAdapter(ContextSys ctx); } /// Representation of each dependency abstract class Dependent implements AbstractAdapterBuilder { Get subGetter(Get getter); SubReducer createSubReducer(); Widget buildComponent( Store store, Get getter, { @required DispatchBus bus, @required Enhancer enhancer, }); ContextSys createContext( Store store, BuildContext buildContext, Get getState, { @required DispatchBus bus, @required Enhancer enhancer, }); bool isComponent(); bool isAdapter(); Object key(T state); } /// Encapsulation of the logic part of the component /// The logic is divided into two parts, Reducer & SideEffect. abstract class AbstractLogic { /// To create a reducer Reducer createReducer(); /// To solve Reducer is neither a subtype nor a supertype of Reducer issue. Object onReducer(Object state, Action action); /// To create each instance's side-effect-action-handler Dispatch createEffectDispatch(ContextSys ctx, Enhancer enhancer); /// To create each instance's side-effect-action-handler Dispatch createNextDispatch(ContextSys ctx, Enhancer enhancer); /// To create each instance's dispatch /// Dispatch is the most important api for users which is provided by framework Dispatch createDispatch( Dispatch effectDispatch, Dispatch nextDispatch, ContextSys ctx, ); /// To create each instance's context ContextSys createContext( Store store, BuildContext buildContext, Get getState, { @required DispatchBus bus, @required Enhancer enhancer, }); /// To create each instance's key (for recycle) if needed Object key(T state); /// Find a dependent by name Dependent slot(String name); /// Get a adapter-dependent Dependent adapterDep(); Type get propertyType; } abstract class AbstractComponent implements AbstractLogic { /// How to build component instance Widget buildComponent( Store store, Get getter, { @required DispatchBus bus, @required Enhancer enhancer, }); } abstract class AbstractAdapter implements AbstractLogic, AbstractAdapterBuilder {} /// Because a main reducer will be very complicated with multiple level's state. /// When a reducer is slow to handle an action, maybe we should use ReducerFilter to improve the performance. typedef ReducerFilter = bool Function(T state, Action action); /// implement [StateKey] in T . /// class T implements StateKey { /// Object _key = UniqueKey(); /// Object key() => _key; /// } /// see [https://github.com/alibaba/fish-redux/issues/461] abstract class StateKey { Object key(); } /// Define a DispatchBus abstract class DispatchBus { void attach(DispatchBus parent); void detach(); void dispatch(Action action, {Dispatch excluded}); void broadcast(Action action, {DispatchBus excluded}); void Function() registerReceiver(Dispatch dispatch); } ================================================ FILE: lib/src/redux_component/batch_store.dart ================================================ import 'dart:async'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart' hide Action, Page; import '../redux/redux.dart'; /// batch notify to subscribers. mixin _BatchNotify on Store { final List _listeners = []; bool _isBatching = false; bool _isSetupBatch = false; T _prevState; void setupBatch() { if (!_isSetupBatch) { _isSetupBatch = true; super.subscribe(_batch); subscribe = (void Function() callback) { assert(callback != null); _listeners.add(callback); return () { _listeners.remove(callback); }; }; } } bool isInSuitablePhase() { return SchedulerBinding.instance != null && SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks && !(SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle && WidgetsBinding.instance.renderViewElement == null); } void _batch() { if (!isInSuitablePhase()) { if (!_isBatching) { _isBatching = true; SchedulerBinding.instance.addPostFrameCallback((Duration duration) { if (_isBatching) { _batch(); } }); } } else { final T curState = getState(); if (!identical(_prevState, curState)) { _prevState = curState; final List notifyListeners = _listeners.toList( growable: false, ); for (void Function() listener in notifyListeners) { listener(); } _isBatching = false; } } } } class _BatchStore extends Store with _BatchNotify { _BatchStore(Store store) : assert(store != null) { getState = store.getState; subscribe = store.subscribe; replaceReducer = store.replaceReducer; dispatch = store.dispatch; observable = store.observable; teardown = store.teardown; setupBatch(); } } Store createBatchStore( T preloadedState, Reducer reducer, { StoreEnhancer storeEnhancer, }) => _BatchStore( createStore( preloadedState, _appendUpdateStateReducer(reducer), storeEnhancer, ), ); /// connect with app-store enum _UpdateState { Assign } // replace current state Reducer _appendUpdateStateReducer(Reducer reducer) => (T state, Action action) => action.type == _UpdateState.Assign ? action.payload : reducer == null ? state : reducer(state, action); Store connectStores( Store mainStore, Store extraStore, T Function(T, K) update, ) { final void Function() subscriber = () { final T prevT = mainStore.getState(); final T nextT = update(prevT, extraStore.getState()); if (nextT != null && !identical(prevT, nextT)) { mainStore.dispatch(Action(_UpdateState.Assign, payload: nextT)); } }; final void Function() unsubscribe = extraStore.subscribe(subscriber); /// should triggle once subscriber(); final Future Function() superMainTD = mainStore.teardown; mainStore.teardown = () { unsubscribe?.call(); return superMainTD(); }; return mainStore; } ================================================ FILE: lib/src/redux_component/component.dart ================================================ import 'package:flutter/widgets.dart' hide Action, Page; import '../redux/basic.dart'; import 'basic.dart'; import 'context.dart'; import 'dependencies.dart'; import 'helper.dart'; import 'lifecycle.dart'; import 'logic.dart'; /// Wrapper ComponentWidget if needed like KeepAlive, RepaintBoundary etc. typedef WidgetWrapper = Widget Function(Widget child); @immutable abstract class Component extends Logic implements AbstractComponent { final ViewBuilder _view; final ShouldUpdate _shouldUpdate; final WidgetWrapper _wrapper; final bool _clearOnDependenciesChanged; ViewBuilder get protectedView => _view; ShouldUpdate get protectedShouldUpdate => _shouldUpdate; WidgetWrapper get protectedWrapper => _wrapper; bool get protectedClearOnDependenciesChanged => _clearOnDependenciesChanged; Component({ @required ViewBuilder view, Reducer reducer, ReducerFilter filter, Effect effect, Dependencies dependencies, ShouldUpdate shouldUpdate, WidgetWrapper wrapper, /// implement [StateKey] in T instead of using key in Logic. /// class T implements StateKey { /// Object _key = UniqueKey(); /// Object key() => _key; /// } @deprecated Key Function(T) key, bool clearOnDependenciesChanged = false, }) : _view = view, _wrapper = wrapper ?? _wrapperByDefault, _shouldUpdate = shouldUpdate ?? updateByDefault(), _clearOnDependenciesChanged = clearOnDependenciesChanged, super( reducer: reducer, filter: filter, effect: effect, dependencies: dependencies, // ignore:deprecated_member_use_from_same_package key: key, ); @override Widget buildComponent( Store store, Get getter, { @required DispatchBus bus, @required Enhancer enhancer, }) { /// Check bus: DispatchBusDefault(); enhancer: EnhancerDefault(); assert(bus != null && enhancer != null); return protectedWrapper( ComponentWidget( component: this, getter: asGetter(getter), store: store, key: key(getter()), bus: bus, enhancer: enhancer, ), ); } @override ComponentContext createContext( Store store, BuildContext buildContext, Get getState, { @required void Function() markNeedsBuild, @required DispatchBus bus, @required Enhancer enhancer, }) { assert(bus != null && enhancer != null); return ComponentContext( logic: this, store: store, buildContext: buildContext, getState: getState, view: enhancer.viewEnhance(protectedView, this, store), shouldUpdate: protectedShouldUpdate, name: name, markNeedsBuild: markNeedsBuild, sidecarCtx: adapterDep()?.createContext( store, buildContext, getState, bus: bus, enhancer: enhancer, ), enhancer: enhancer, bus: bus, ); } ComponentState createState() => ComponentState(); String get name => cache('name', () => runtimeType.toString()); static ShouldUpdate neverUpdate() => (K _, K __) => false; static ShouldUpdate alwaysUpdate() => (K _, K __) => true; static ShouldUpdate updateByDefault() => (K _, K __) => !identical(_, __); static Widget _wrapperByDefault(Widget child) => child; } class ComponentWidget extends StatefulWidget { final Component component; final Store store; final Get getter; final DispatchBus bus; final Enhancer enhancer; const ComponentWidget({ @required this.component, @required this.store, @required this.getter, this.bus, this.enhancer, Key key, }) : assert(component != null), assert(store != null), assert(getter != null), super(key: key); @override ComponentState createState() => component.createState(); } class ComponentState extends State> { ComponentContext _ctx; ComponentContext get ctx => _ctx; @mustCallSuper @override Widget build(BuildContext context) => _ctx.buildWidget(); @override @protected @mustCallSuper void reassemble() { super.reassemble(); _ctx.clearCache(); _ctx.onLifecycle(LifecycleCreator.reassemble()); } @mustCallSuper @override void initState() { super.initState(); /// init context _ctx = widget.component.createContext( widget.store, context, () => widget.getter(), markNeedsBuild: () { if (mounted) { setState(() {}); } }, bus: widget.bus, enhancer: widget.enhancer, ); /// register store.subscribe _ctx.registerOnDisposed(widget.store.subscribe(() => _ctx.onNotify())); _ctx.onLifecycle(LifecycleCreator.initState()); } @mustCallSuper @override void didChangeDependencies() { super.didChangeDependencies(); if (widget.component.protectedClearOnDependenciesChanged != false) { _ctx.clearCache(); } _ctx.onLifecycle(LifecycleCreator.didChangeDependencies()); } @mustCallSuper @override void deactivate() { super.deactivate(); _ctx.onLifecycle(LifecycleCreator.deactivate()); } @mustCallSuper @override void didUpdateWidget(ComponentWidget oldWidget) { super.didUpdateWidget(oldWidget); _ctx.didUpdateWidget(); _ctx.onLifecycle(LifecycleCreator.didUpdateWidget()); } @mustCallSuper void disposeCtx() { if (!_ctx.isDisposed) { _ctx ..onLifecycle(LifecycleCreator.dispose()) ..dispose(); } } @mustCallSuper @override void dispose() { disposeCtx(); super.dispose(); } } ================================================ FILE: lib/src/redux_component/context.dart ================================================ import 'package:flutter/widgets.dart' hide Action, Page; import '../redux/redux.dart'; import 'auto_dispose.dart'; import 'basic.dart'; import 'lifecycle.dart'; mixin _ExtraMixin { Map _extra; Map get extra => _extra ??= {}; } /// Default Context abstract class LogicContext extends ContextSys with _ExtraMixin { final AbstractLogic logic; @override final Store store; @override final DispatchBus bus; @override final Enhancer enhancer; final Get getState; void Function() _forceUpdate; BuildContext _buildContext; Dispatch _dispatch; Dispatch _effectDispatch; LogicContext({ @required this.logic, @required this.store, @required BuildContext buildContext, @required this.getState, /// pageBus @required this.bus, @required this.enhancer, }) : assert(logic != null), assert(store != null), assert(buildContext != null), assert(getState != null), assert(bus != null && enhancer != null), _buildContext = buildContext { /// _effectDispatch = logic.createEffectDispatch(this, enhancer); /// create Dispatch _dispatch = logic.createDispatch( _effectDispatch, logic.createNextDispatch( this, enhancer, ), this, ); /// Register inter-component broadcast registerOnDisposed(bus.registerReceiver(_effectDispatch)); } @override void bindForceUpdate(void Function() forceUpdate) { assert(_forceUpdate == null); _forceUpdate = forceUpdate; } @override BuildContext get context => _buildContext; @override T get state => getState(); @override dynamic dispatch(Action action) => _dispatch(action); @override Widget buildComponent(String name, {Widget defaultWidget}) { assert(name != null, 'The name must be NotNull for buildComponent.'); final Dependent dependent = logic.slot(name); final Widget result = dependent?.buildComponent(store, getState, bus: bus, enhancer: enhancer); assert(result != null || defaultWidget != null, 'Could not found component by name "$name." You can set a default widget for buildComponent'); return result ?? (defaultWidget ?? Container()); } @override void onLifecycle(Action action) { assert(_throwIfDisposed()); _dispatch(action); } @override void dispose() { super.dispose(); _buildContext = null; _forceUpdate = null; } bool _throwIfDisposed() { if (isDisposed) { throw Exception( 'Ctx has been disposed which could not been used any more.'); } return true; } @override State get stfState { assert(_buildContext is StatefulElement); if (_buildContext is StatefulElement) { final StatefulElement stfElement = _buildContext; return stfElement.state; } return null; } @override void broadcastEffect(Action action, {bool excluded}) => bus.dispatch(action, excluded: excluded == true ? _effectDispatch : null); @override void broadcast(Action action) => bus.broadcast(action); @override void Function() addObservable(Subscribe observable) { final void Function() unsubscribe = observable(() { _forceUpdate?.call(); }); registerOnDisposed(unsubscribe); return unsubscribe; } @override void forceUpdate() => _forceUpdate?.call(); @override void Function() listen({ bool Function(T, T) isChanged, @required void Function() onChange, }) { assert(onChange != null); T oldState; final AutoDispose disposable = registerOnDisposed( store.subscribe( () => () { final T newState = state; final bool flag = isChanged == null ? !identical(oldState, newState) : isChanged(oldState, newState); oldState = newState; if (flag) { onChange(); } }, ), ); return () => disposable?.dispose(); } } class ComponentContext extends LogicContext implements ViewUpdater { final ViewBuilder view; final ShouldUpdate shouldUpdate; final String name; final Function() markNeedsBuild; final ContextSys sidecarCtx; Widget _widgetCache; T _latestState; ComponentContext({ @required AbstractComponent logic, @required Store store, @required BuildContext buildContext, @required Get getState, @required this.view, @required this.shouldUpdate, @required this.name, @required this.markNeedsBuild, @required this.sidecarCtx, @required DispatchBus bus, @required Enhancer enhancer, }) : assert(bus != null && enhancer != null), super( logic: logic, store: store, buildContext: buildContext, getState: getState, bus: bus, enhancer: enhancer, ) { _latestState = state; sidecarCtx?.setParent(this); } @override void onLifecycle(Action action) { super.onLifecycle(action); sidecarCtx?.onLifecycle(action); } @override ListAdapter buildAdapter() { assert(sidecarCtx != null); return logic.adapterDep()?.buildAdapter(sidecarCtx) ?? const ListAdapter(null, 0); } @override Widget buildWidget() { Widget result = _widgetCache; if (result == null) { result = _widgetCache = view(state, dispatch, this); dispatch(LifecycleCreator.build(name)); } return result; } @override void didUpdateWidget() { final T now = state; if (shouldUpdate(_latestState, now)) { _widgetCache = null; _latestState = now; } } @override void onNotify() { final T now = state; if (shouldUpdate(_latestState, now)) { _widgetCache = null; markNeedsBuild(); _latestState = now; } } @override void clearCache() { _widgetCache = null; } @override void forceUpdate() { _widgetCache = null; try { markNeedsBuild(); } catch (e) { /// TODO /// should try-catch in force mode which is called from outside } } } class PureViewViewService implements ViewService { final DispatchBus bus; @override final BuildContext context; PureViewViewService(this.bus, this.context); @override void broadcast(Action action) => bus.broadcast(action); @override void broadcastEffect(Action action, {bool excluded}) => bus.dispatch(action); @override ListAdapter buildAdapter() => throw Exception( 'Unexpected call of "buildAdapter" in a PureViewComponent'); @override Widget buildComponent(String name, {Widget defaultWidget}) => throw Exception( 'Unexpected call of "buildComponent" in a PureViewComponent'); @override Map get extra => throw Exception('Unexpected call of "extra" in a PureViewComponent'); } ================================================ FILE: lib/src/redux_component/dependencies.dart ================================================ import '../redux/redux.dart'; import 'basic.dart'; class Dependencies { final Map> slots; final Dependent adapter; /// Use [adapter: NoneConn() + Adapter()] instead of [adapter: Adapter()], /// Which is better reusability and consistency. Dependencies({ this.slots, this.adapter, }) : assert(adapter == null || adapter.isAdapter(), 'The dependent must contains adapter.'); Reducer createReducer() { final List> subs = >[]; if (slots != null && slots.isNotEmpty) { subs.addAll(slots.entries.map>( (MapEntry> entry) => entry.value.createSubReducer(), )); } if (adapter != null) { subs.add(adapter.createSubReducer()); } return combineReducers(>[combineSubReducers(subs)]); } Dependent slot(String type) => slots[type]; Dependencies trim() => adapter != null || slots?.isNotEmpty == true ? this : null; } ================================================ FILE: lib/src/redux_component/dependent.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/widgets.dart' hide Action, Page; import '../redux/redux.dart'; import 'basic.dart'; class _Dependent implements Dependent { final AbstractConnector connector; final AbstractLogic

logic; final SubReducer subReducer; _Dependent({ @required this.logic, @required this.connector, }) : assert(logic != null), assert(connector != null), subReducer = logic.createReducer != null ? connector.subReducer(logic.createReducer()) : null; @override SubReducer createSubReducer() => subReducer; @override Widget buildComponent( Store store, Get getter, { @required DispatchBus bus, @required Enhancer enhancer, }) { assert(bus != null && enhancer != null); assert(isComponent(), 'Unexpected type of ${logic.runtimeType}.'); final AbstractComponent

component = logic; return component.buildComponent( store, () => connector.get(getter()), bus: bus, enhancer: enhancer, ); } @override ListAdapter buildAdapter(covariant ContextSys

ctx) { assert(isAdapter(), 'Unexpected type of ${logic.runtimeType}.'); final AbstractAdapter

adapter = logic; return adapter.buildAdapter(ctx); } @override Get

subGetter(Get getter) => () => connector.get(getter()); @override ContextSys

createContext( Store store, BuildContext buildContext, Get getState, { @required DispatchBus bus, @required Enhancer enhancer, }) { assert(bus != null && enhancer != null); return logic.createContext( store, buildContext, subGetter(getState), bus: bus, enhancer: enhancer, ); } @override bool isComponent() => logic is AbstractComponent; @override bool isAdapter() => logic is AbstractAdapter; @override Object key(T state) { return Tuple3( logic.runtimeType, connector.runtimeType, logic.key(connector.get(state)), ); } } Dependent createDependent( AbstractConnector connector, AbstractLogic logic) => logic != null ? _Dependent(connector: connector, logic: logic) : null; ================================================ FILE: lib/src/redux_component/dispatch_bus.dart ================================================ import '../redux/redux.dart'; import 'basic.dart'; class DispatchBusDefault implements DispatchBus { final List _dispatchList = []; DispatchBus parent; void Function() unregister; DispatchBusDefault(); @override void attach(DispatchBus parent) { this.parent = parent; unregister?.call(); unregister = parent?.registerReceiver(dispatch); } @override void detach() { unregister?.call(); } @override void dispatch(Action action, {Dispatch excluded}) { final List list = _dispatchList .where((Dispatch dispatch) => dispatch != excluded) .toList(growable: false); for (Dispatch dispatch in list) { dispatch(action); } } @override void broadcast(Action action, {DispatchBus excluded}) { parent?.dispatch(action, excluded: excluded?.dispatch); } @override void Function() registerReceiver(Dispatch dispatch) { assert(!_dispatchList.contains(dispatch), 'Do not register a dispatch which is already existed'); if (dispatch != null) { _dispatchList.add(dispatch); return () { _dispatchList.remove(dispatch); }; } else { return null; } } } ================================================ FILE: lib/src/redux_component/enhancer.dart ================================================ import 'package:fish_redux/src/redux_component/helper.dart'; import '../redux/redux.dart'; import 'basic.dart'; class EnhancerDefault implements Enhancer { StoreEnhancer _storeEnhancer; ViewMiddleware _viewEnhancer; EffectMiddleware _effectEnhancer; AdapterMiddleware _adapterEnhancer; final List> _middleware = >[]; final List> _viewMiddleware = >[]; final List> _effectMiddleware = >[]; final List> _adapterMiddleware = >[]; EnhancerDefault({ List> middleware, List> viewMiddleware, List> effectMiddleware, List> adapterMiddleware, }) { append( middleware: middleware, viewMiddleware: viewMiddleware, effectMiddleware: effectMiddleware, adapterMiddleware: adapterMiddleware, ); } @override void unshift({ List> middleware, List> viewMiddleware, List> effectMiddleware, List> adapterMiddleware, }) { if (middleware != null) { _middleware.insertAll(0, middleware); _storeEnhancer = applyMiddleware(_middleware); } if (viewMiddleware != null) { _viewMiddleware.insertAll(0, viewMiddleware); _viewEnhancer = mergeViewMiddleware(_viewMiddleware); } if (effectMiddleware != null) { _effectMiddleware.insertAll(0, effectMiddleware); _effectEnhancer = mergeEffectMiddleware(_effectMiddleware); } if (adapterMiddleware != null) { _adapterMiddleware.insertAll(0, adapterMiddleware); _adapterEnhancer = mergeAdapterMiddleware(_adapterMiddleware); } } @override void append({ List> middleware, List> viewMiddleware, List> effectMiddleware, List> adapterMiddleware, }) { if (middleware != null) { _middleware.addAll(middleware); _storeEnhancer = applyMiddleware(_middleware); } if (viewMiddleware != null) { _viewMiddleware.addAll(viewMiddleware); _viewEnhancer = mergeViewMiddleware(_viewMiddleware); } if (effectMiddleware != null) { _effectMiddleware.addAll(effectMiddleware); _effectEnhancer = mergeEffectMiddleware(_effectMiddleware); } if (adapterMiddleware != null) { _adapterMiddleware.addAll(adapterMiddleware); _adapterEnhancer = mergeAdapterMiddleware(_adapterMiddleware); } } @override ViewBuilder viewEnhance( ViewBuilder view, AbstractComponent component, Store store, ) => _viewEnhancer?.call(component, store)?.call(_inverterView(view)) ?? view; @override AdapterBuilder adapterEnhance( AdapterBuilder adapterBuilder, AbstractAdapter logic, Store store, ) => _adapterEnhancer ?.call(logic, store) ?.call(_inverterAdapter(adapterBuilder)) ?? adapterBuilder; @override Effect effectEnhance( Effect effect, AbstractLogic logic, Store store, ) => _effectEnhancer?.call(logic, store)?.call(_inverterEffect(effect)) ?? effect; @override StoreCreator storeEnhance(StoreCreator creator) => _storeEnhancer?.call(creator) ?? creator; Effect _inverterEffect(Effect effect) => effect == null ? null : (Action action, Context ctx) => effect(action, ctx); ViewBuilder _inverterView(ViewBuilder view) => view == null ? null : (dynamic state, Dispatch dispatch, ViewService viewService) => view(state, dispatch, viewService); AdapterBuilder _inverterAdapter(AdapterBuilder adapter) => adapter == null ? null : (dynamic state, Dispatch dispatch, ViewService viewService) => adapter(state, dispatch, viewService); } ================================================ FILE: lib/src/redux_component/helper.dart ================================================ import 'dart:async'; import 'package:flutter/widgets.dart' hide Action, Page; import '../redux/basic.dart'; import '../utils/utils.dart'; import 'basic.dart'; import 'lifecycle.dart'; Get asGetter(Get getter) { Get runtimeGetter; if (getter is Get) { runtimeGetter = getter; } else { runtimeGetter = () { final T result = getter(); return result; }; } return runtimeGetter; } AdapterBuilder asAdapter(ViewBuilder view) { return (T unstableState, Dispatch dispatch, ViewService service) { final ContextSys ctx = service; return ListAdapter( (BuildContext buildContext, int index) => view(ctx.state, dispatch, service), 1, ); }; } Reducer mergeReducers(Reducer sup, [Reducer sub]) { return (T state, Action action) { return sub?.call(sup(state, action), action) ?? sup(state, action); }; } Effect mergeEffects(Effect sup, [Effect sub]) { return (Action action, Context ctx) { return sub?.call(action, ctx) ?? sup.call(action, ctx); }; } /// combine & as /// for action.type which override it's == operator Reducer asReducer(Map> map) => (map == null || map.isEmpty) ? null : (T state, Action action) => map.entries .firstWhere( (MapEntry> entry) => action.type == entry.key, orElse: () => null) ?.value(state, action) ?? state; Reducer filterReducer(Reducer reducer, ReducerFilter filter) { return (reducer == null || filter == null) ? reducer : (T state, Action action) { return filter(state, action) ? reducer(state, action) : state; }; } const Object _SUB_EFFECT_RETURN_NULL = Object(); typedef SubEffect = FutureOr Function(Action action, Context ctx); /// for action.type which override it's == operator /// return [UserEffecr] Effect combineEffects(Map> map) => (map == null || map.isEmpty) ? null : (Action action, Context ctx) { final SubEffect subEffect = map.entries .firstWhere( (MapEntry> entry) => action.type == entry.key, orElse: () => null, ) ?.value; if (subEffect != null) { return subEffect.call(action, ctx) ?? _SUB_EFFECT_RETURN_NULL; } //skip-lifecycle-actions if (action.type is Lifecycle) { return _SUB_EFFECT_RETURN_NULL; } /// no subEffect return null; }; /// return [EffectDispatch] Dispatch createEffectDispatch(Effect userEffect, Context ctx) { return (Action action) { final Object result = userEffect?.call(action, ctx); //skip-lifecycle-actions if (action.type is Lifecycle && (result == null || result == false)) { return _SUB_EFFECT_RETURN_NULL; } return result; }; } /// return [NextDispatch] Dispatch createNextDispatch(ContextSys ctx) => (Action action) { ctx.broadcastEffect(action, excluded: true); ctx.store.dispatch(action); }; /// return [Dispatch] Dispatch createDispatch(Dispatch onEffect, Dispatch next, Context ctx) => (Action action) { final Object result = onEffect?.call(action); if (result == null || result == false) { next(action); } return result == _SUB_EFFECT_RETURN_NULL ? null : result; }; ViewMiddleware mergeViewMiddleware(List> middleware) { return Collections.reduce>(middleware, (ViewMiddleware first, ViewMiddleware second) { return (AbstractComponent component, Store store) { final Composable> inner = first(component, store); final Composable> outer = second(component, store); return (ViewBuilder view) { return outer(inner(view)); }; }; }); } AdapterMiddleware mergeAdapterMiddleware( List> middleware) { return Collections.reduce>(middleware, (AdapterMiddleware first, AdapterMiddleware second) { return (AbstractAdapter component, Store store) { final Composable> inner = first(component, store); final Composable> outer = second(component, store); return (AdapterBuilder view) { return outer(inner(view)); }; }; }); } EffectMiddleware mergeEffectMiddleware( List> middleware) { return Collections.reduce>(middleware, (EffectMiddleware first, EffectMiddleware second) { return (AbstractLogic logic, Store store) { final Composable> inner = first(logic, store); final Composable> outer = second(logic, store); return (Effect effect) { return outer(inner(effect)); }; }; }); } ================================================ FILE: lib/src/redux_component/lifecycle.dart ================================================ import 'package:flutter/scheduler.dart'; import '../redux/redux.dart'; enum Lifecycle { /// componenmt(page) or adapter receives the following events initState, didChangeDependencies, build, reassemble, didUpdateWidget, deactivate, dispose, // didDisposed, /// Only a adapter mixin VisibleChangeMixin will receive appear & disappear events. /// class MyAdapter extends Adapter with VisibleChangeMixin { /// MyAdapter():super( /// /// /// ); /// } appear, disappear, /// Only a componenmt(page) or adapter mixin WidgetsBindingObserverMixin will receive didChangeAppLifecycleState event. /// class MyComponent extends Component with WidgetsBindingObserverMixin { /// MyComponent():super( /// /// /// ); /// } didChangeAppLifecycleState, } class LifecycleCreator { static Action initState() => const Action(Lifecycle.initState); static Action build(String name) => Action(Lifecycle.build, payload: name); static Action reassemble() => const Action(Lifecycle.reassemble); static Action dispose() => const Action(Lifecycle.dispose); // static Action didDisposed() => const Action(Lifecycle.didDisposed); static Action didUpdateWidget() => const Action(Lifecycle.didUpdateWidget); static Action didChangeDependencies() => const Action(Lifecycle.didChangeDependencies); static Action deactivate() => const Action(Lifecycle.deactivate); static Action appear(int index) => Action(Lifecycle.appear, payload: index); static Action disappear(int index) => Action(Lifecycle.disappear, payload: index); static Action didChangeAppLifecycleState(AppLifecycleState state) => Action(Lifecycle.didChangeAppLifecycleState, payload: state); } ================================================ FILE: lib/src/redux_component/local.dart ================================================ import 'package:flutter/foundation.dart'; import 'basic.dart'; /// /// Description: /// /// LocalProps的状态变化不会触发View的刷新 /// /// /// Define: /// /// ```dart /// class ComponentLocalProps extends LocalProps { /// final TextEditingController controller = TextEditingController(); /// /// ComponentLocalProps(Context ctx) : super(ctx); /// /// factory ComponentLocalProps.of(ExtraData ctx) { /// return LocalProps.provide((_) => ComponentLocalProps(_)).of(ctx); /// } /// /// @override /// void destructor(Context ctx) { /// controller.dispose(); /// } /// } /// /// ``` /// /// Usage: /// /// in View /// ```dart /// ComponentLocalProps.of(viewService).controller /// ``` /// in effect /// ```dart /// ComponentLocalProps.of(ctx).controller /// ``` /// abstract class LocalProps> { LocalProps(Context ctx) : assert(ctx != null); void destructor(Context ctx); static _LocalPropsProvider provide>( T Function(Context) construct) => _LocalPropsProvider( construct: construct, destruct: (T local, Context ctx) => local.destructor(ctx), ); } @immutable class _LocalPropsProvider { final T Function(Context) construct; final void Function(T, Context) destruct; const _LocalPropsProvider({@required this.construct, this.destruct}) : assert(construct != null, 'Please provide a constructor to create instance.'); T of(ExtraData context) { assert(context is Context); final Context ctx = context; if (ctx.extra[_key] == null) { final T result = construct(ctx); ctx.extra[_key] = result; if (destruct != null) { ctx.registerOnDisposed(() => destruct(result, ctx)); } } return ctx.extra[_key]; } String get _key => '\$ ${T.toString()}'; } // class ComponentLocalProps extends LocalProps { // ComponentLocalProps(Context ctx) : super(ctx); // factory ComponentLocalProps.of(ExtraData ctx) { // return LocalProps.provide((_) => ComponentLocalProps(_)).of(ctx); // } // @override // void destructor(Context ctx) {} // } ================================================ FILE: lib/src/redux_component/logic.dart ================================================ import 'package:flutter/widgets.dart' hide Action, Page; import '../redux/redux.dart'; import '../utils/utils.dart'; import 'basic.dart'; import 'dependencies.dart'; import 'helper.dart' as helper; /// Four parts /// 1. Reducer & ReducerFilter /// 2. Effect /// 3. Dependencies /// 4. Key abstract class Logic implements AbstractLogic { final Reducer _reducer; final ReducerFilter _filter; final Effect _effect; final Dependencies _dependencies; final Object Function(T state) _key; /// for extends Reducer get protectedReducer => _reducer; ReducerFilter get protectedFilter => _filter; Effect get protectedEffect => _effect; Dependencies get protectedDependencies => _dependencies; Reducer get protectedDependenciesReducer => protectedDependencies?.createReducer(); Object Function(T state) get protectedKey => _key; /// Used as function cache to improve operational efficiency final Map _resultCache = {}; Logic({ Reducer reducer, Dependencies dependencies, ReducerFilter filter, Effect effect, /// implement [StateKey] in T instead of using key in Logic. /// class T implements StateKey { /// Object _key = UniqueKey(); /// Object key() => _key; /// } @deprecated Object Function(T state) key, }) : _reducer = reducer, _filter = filter, _effect = effect, _dependencies = dependencies?.trim(), // ignore:deprecated_member_use_from_same_package assert(isAssignFrom() == false || key == null, 'Implements [StateKey] in T instead of using key in Logic.'), _key = isAssignFrom() // ignore:avoid_as ? ((T state) => (state as StateKey).key()) // ignore:deprecated_member_use_from_same_package : key; @override Type get propertyType => T; bool isSuperTypeof() => Tuple0() is Tuple0; bool isTypeof() => Tuple0() is Tuple0; static bool isAssignFrom() => Tuple0

() is Tuple0; /// if /// _resultCache['key'] = null; /// then /// _resultCache.containsKey('key') will be true; R cache(String key, Get getter) => _resultCache.containsKey(key) ? _resultCache[key] : (_resultCache[key] = getter()); @override Reducer createReducer() => helper.filterReducer( combineReducers( >[protectedReducer, protectedDependenciesReducer]), protectedFilter); @override Object onReducer(Object state, Action action) => cache>('onReducer', createReducer)?.call(state, action) ?? state; @override Dispatch createEffectDispatch(ContextSys ctx, Enhancer enhancer) { return helper.createEffectDispatch( /// enhance userEffect enhancer.effectEnhance( protectedEffect, this, ctx.store, ), ctx); } @override Dispatch createNextDispatch(ContextSys ctx, Enhancer enhancer) => helper.createNextDispatch(ctx); @override Dispatch createDispatch( Dispatch effectDispatch, Dispatch nextDispatch, Context ctx, ) => helper.createDispatch(effectDispatch, nextDispatch, ctx); @override Object key(T state) => _key?.call(state) ?? ValueKey(runtimeType); @override Dependent slot(String type) => protectedDependencies?.slot(type); @override Dependent adapterDep() => protectedDependencies?.adapter; } ================================================ FILE: lib/src/redux_component/page.dart ================================================ import 'package:flutter/widgets.dart' hide Action, Page; import '../redux/redux.dart'; import 'basic.dart'; import 'batch_store.dart'; import 'component.dart'; import 'dependencies.dart'; import 'dispatch_bus.dart'; import 'enhancer.dart'; /// init store's state by route-params typedef InitState = T Function(P params); typedef StoreUpdater = Store Function(Store store); final DispatchBus sharedBus = DispatchBusDefault(); @immutable abstract class Page extends Component { /// AppBus is a event-bus used to communicate between pages. final DispatchBus appBus = sharedBus; final InitState _initState; final Enhancer enhancer; /// connect with other stores final List> _storeUpdaters = >[]; Page({ @required InitState initState, @required ViewBuilder view, Reducer reducer, ReducerFilter filter, Effect effect, Dependencies dependencies, ShouldUpdate shouldUpdate, WidgetWrapper wrapper, /// implement [StateKey] in T instead of using key in Logic. /// class T implements StateKey { /// Object _key = UniqueKey(); /// Object key() => _key; /// } @deprecated Key Function(T) key, List> middleware, List> viewMiddleware, List> effectMiddleware, List> adapterMiddleware, }) : assert(initState != null), _initState = initState, enhancer = EnhancerDefault( middleware: middleware, viewMiddleware: viewMiddleware, effectMiddleware: effectMiddleware, adapterMiddleware: adapterMiddleware, ), super( view: view, dependencies: dependencies, reducer: reducer, filter: filter, effect: effect, shouldUpdate: shouldUpdate, wrapper: wrapper, // ignore:deprecated_member_use_from_same_package key: key, ); Widget buildPage(P param) => protectedWrapper(_PageWidget( page: this, param: param, )); Store createStore(P param) => updateStore(createBatchStore( _initState(param), createReducer(), storeEnhancer: enhancer.storeEnhance, )); Store updateStore(Store store) => _storeUpdaters.fold( store, (Store previousValue, StoreUpdater element) => element(previousValue), ); /// page-store connect with app-store void connectExtraStore( Store extraStore, /// To solve Reducer is neither a subtype nor a supertype of Reducer issue. Object Function(Object, K) update, ) => _storeUpdaters.add((Store store) => connectStores( store, extraStore, update, )); DispatchBus createPageBus() => DispatchBusDefault(); void unshift({ List> middleware, List> viewMiddleware, List> effectMiddleware, List> adapterMiddleware, }) { enhancer.unshift( middleware: middleware, viewMiddleware: viewMiddleware, effectMiddleware: effectMiddleware, adapterMiddleware: adapterMiddleware, ); } void append({ List> middleware, List> viewMiddleware, List> effectMiddleware, List> adapterMiddleware, }) { enhancer.append( middleware: middleware, viewMiddleware: viewMiddleware, effectMiddleware: effectMiddleware, adapterMiddleware: adapterMiddleware, ); } } class _PageWidget extends StatefulWidget { final Page page; final P param; const _PageWidget({ Key key, @required this.page, @required this.param, }) : super(key: key); @override State createState() => _PageState(); } class _PageState extends State<_PageWidget> { Store _store; DispatchBus _pageBus; final Map extra = {}; @override void initState() { super.initState(); _store = widget.page.createStore(widget.param); _pageBus = widget.page.createPageBus(); } @override void didChangeDependencies() { super.didChangeDependencies(); /// Register inter-page broadcast _pageBus.attach(widget.page.appBus); } @override Widget build(BuildContext context) { // return PageProvider( // store: _store, // extra: extra, // child: // ); return widget.page.buildComponent( _store, _store.getState, bus: _pageBus, enhancer: widget.page.enhancer, ); } @override void dispose() { _pageBus.detach(); _store.teardown(); super.dispose(); } } @deprecated class PageProvider extends InheritedWidget { final Store store; /// Used to store page data if needed final Map extra; const PageProvider({ @required this.store, @required this.extra, @required Widget child, Key key, }) : assert(store != null), assert(child != null), super(child: child, key: key); static PageProvider tryOf(BuildContext context) { final PageProvider provider = context.dependOnInheritedWidgetOfExactType(); return provider; } @override bool updateShouldNotify(PageProvider oldWidget) => store != oldWidget.store && extra != oldWidget.extra; } ================================================ FILE: lib/src/redux_component/redux_component.dart ================================================ export 'auto_dispose.dart'; export 'basic.dart'; export 'batch_store.dart'; export 'component.dart'; export 'context.dart'; export 'dependencies.dart'; export 'dependent.dart'; export 'dispatch_bus.dart'; export 'enhancer.dart'; export 'helper.dart'; export 'lifecycle.dart'; export 'local.dart'; export 'logic.dart'; export 'page.dart'; ================================================ FILE: lib/src/redux_component_mixin/keep_alive_mixin.dart ================================================ import 'package:flutter/widgets.dart' hide Action, Page; import '../redux_component/redux_component.dart'; /// usage /// class MyComponent extends Component with KeepAliveMixin { /// MyComponent():super( /// /// /// ); /// } /// Only For [Component] mixin KeepAliveMixin on Component { @override ComponentState createState() => _KeepAliveStfState(); } class _KeepAliveStfState extends ComponentState with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; @override Widget build(BuildContext context) { super.build(context); return ctx.buildWidget(); } } /// usage /// class MyComponent extends Component { /// MyComponent():super( /// wrapper: keepAliveClientWrapper, /// ); /// } /// For Both [Component] & [Page] Widget keepAliveClientWrapper(Widget child) => _KeepAliveWidget(child); class _KeepAliveWidget extends StatefulWidget { final Widget child; const _KeepAliveWidget(this.child); @override State createState() => _KeepAliveState(); } class _KeepAliveState extends State<_KeepAliveWidget> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; @override Widget build(BuildContext context) { super.build(context); return widget.child; } } ================================================ FILE: lib/src/redux_component_mixin/private_reducer_mixin.dart ================================================ import '../redux/redux.dart'; import '../redux_component/redux_component.dart'; /// usage /// class MyComponent extends Component with PrivateReducerMixin { /// MyComponent():super( /// /// /// ); /// } mixin PrivateReducerMixin on Logic { @override Reducer get protectedReducer { final Reducer superReducer = super.protectedReducer; return superReducer != null ? (T state, Action action) { if (action is PrivateAction && action.target == state) { return superReducer(state, action.asAction()); } return state; } : null; } @override Dispatch createDispatch(Dispatch effect, Dispatch next, Context ctx) { final Dispatch superDispatch = super.createDispatch(effect, next, ctx); return (Action action) { if (action.type is! Lifecycle && action is! PrivateAction) { action = PrivateAction( action.type, payload: action.payload, target: ctx.state, ); } return superDispatch(action); }; } } class PrivateAction extends Action { final Object target; PrivateAction(Object type, {dynamic payload, this.target}) : super(type, payload: payload); Action asAction() => Action(type, payload: payload); } ================================================ FILE: lib/src/redux_component_mixin/redux_component_mixin.dart ================================================ export 'keep_alive_mixin.dart'; export 'private_reducer_mixin.dart'; export 'single_ticker_provider_mixin.dart'; export 'ticker_provider_mixin.dart'; export 'visible_change_mixin.dart'; export 'widgets_binding_observer_mixin.dart'; ================================================ FILE: lib/src/redux_component_mixin/single_ticker_provider_mixin.dart ================================================ import 'package:flutter/widgets.dart' hide Action, Page; import '../redux_component/redux_component.dart'; /// usage /// class MyComponent extends Component with SingleTickerProviderMixin { /// MyComponent():super( /// /// /// ); /// } /// For Both [Component] & [Page] mixin SingleTickerProviderMixin on Component { @override _SingleTickerProviderStfState createState() => _SingleTickerProviderStfState(); } class _SingleTickerProviderStfState extends ComponentState with SingleTickerProviderStateMixin { /// fix SingleTickerProviderStateMixin dispose bug @override void dispose() { disposeCtx(); super.dispose(); } } ================================================ FILE: lib/src/redux_component_mixin/ticker_provider_mixin.dart ================================================ import 'package:flutter/widgets.dart' hide Action, Page; import '../redux_component/redux_component.dart'; /// usage /// class MyComponent extends Component with TickerProviderMixin { /// MyComponent():super( /// /// /// ); /// } /// For Both [Component] & [Page] mixin TickerProviderMixin on Component { @override _TickerProviderStfState createState() => _TickerProviderStfState(); } class _TickerProviderStfState extends ComponentState with TickerProviderStateMixin { /// fix TickerProviderStateMixin dispose bug @override void dispose() { disposeCtx(); super.dispose(); } } ================================================ FILE: lib/src/redux_component_mixin/visible_change_mixin.dart ================================================ import 'package:flutter/widgets.dart' hide Action, Page; import '../redux/redux.dart'; import '../redux_component/redux_component.dart'; import '../utils/utils.dart'; /// usage /// class MyAdapter extends Adapter with VisibleChangeMixin { /// MyAdapter():super( /// /// /// ); /// } mixin VisibleChangeMixin on AbstractAdapter { @override ListAdapter buildAdapter(ContextSys ctx) { return _wrapVisibleChange(super.buildAdapter(ctx), ctx); } } class _VisibleChangeState extends State<_VisibleChangeWidget> { @override Widget build(BuildContext context) => widget.itemBuilder(context, widget.index); @override void initState() { super.initState(); widget.dispatch(LifecycleCreator.appear(widget.index)); } @override void dispose() { widget.dispatch(LifecycleCreator.disappear(widget.index)); super.dispose(); } } class _VisibleChangeWidget extends StatefulWidget { final IndexedWidgetBuilder itemBuilder; final int index; final Dispatch dispatch; const _VisibleChangeWidget({ Key key, this.itemBuilder, this.index, this.dispatch, }) : super(key: key); @override State createState() => _VisibleChangeState(); } ListAdapter _wrapVisibleChange( ListAdapter listAdapter, LogicContext ctx, ) { final _VisibleChangeDispatch onChange = (ctx.extra['\$visible'] ??= _VisibleChangeDispatch(ctx.dispatch)); return listAdapter == null ? null : ListAdapter( (BuildContext buildContext, int index) => _VisibleChangeWidget( itemBuilder: listAdapter.itemBuilder, index: index, dispatch: onChange.onAction, key: ValueKey>(Tuple2(ctx, index)), ), listAdapter.itemCount, ); } class _VisibleChangeDispatch extends AutoDispose { int _appearsCount = 0; final Dispatch dispatch; _VisibleChangeDispatch(this.dispatch); void onAction(Action action) { if (action.type == Lifecycle.appear) { assert(_appearsCount >= 0); if (_appearsCount == 0) { if (!isDisposed) { dispatch(action); } } _appearsCount++; } else if (action.type == Lifecycle.disappear) { _appearsCount--; assert(_appearsCount >= 0); if (_appearsCount == 0) { if (!isDisposed) { dispatch(action); } } } } } ================================================ FILE: lib/src/redux_component_mixin/widgets_binding_observer_mixin.dart ================================================ import 'package:flutter/widgets.dart' hide Action, Page; import '../redux_component/redux_component.dart'; /// usage /// class MyComponent extends Component with WidgetsBindingObserverMixin { /// MyComponent():super( /// /// /// ); /// } /// For Both [Component] & [Page] mixin WidgetsBindingObserverMixin on Component { @override _WidgetsBindingObserverStfState createState() => _WidgetsBindingObserverStfState(); } class _WidgetsBindingObserverStfState extends ComponentState with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); ctx.onLifecycle(LifecycleCreator.didChangeAppLifecycleState(state)); } } ================================================ FILE: lib/src/redux_connector/connector.dart ================================================ import '../redux/redux.dart'; import 'op_mixin.dart'; class ConnOp extends MutableConn with ConnOpMixin { final P Function(T) _getter; final void Function(T, P) _setter; const ConnOp({ P Function(T) get, void Function(T, P) set, }) : _getter = get, _setter = set; @override P get(T state) => _getter(state); @override void set(T state, P subState) => _setter(state, subState); } ================================================ FILE: lib/src/redux_connector/generator.dart ================================================ String Function() generator() { int nextId = 0; String prefix = ''; return () { /// fix '0x3FFFFFFFFFFFFFFF' can't be represented exactly in JavaScript. if (++nextId >= 0x3FFFFFFF) { nextId = 0; prefix = '\$' + prefix; } return prefix + nextId.toString(); }; } ================================================ FILE: lib/src/redux_connector/helper.dart ================================================ import '../redux/redux.dart'; import '../redux_component/redux_component.dart'; class ConnHelper { static AbstractConnector to( AbstractConnector one, AbstractConnector two) { return _AbstractConnector(one, two); } static Dependent join( AbstractConnector conn, AbstractLogic

logic) => createDependent(conn, logic); } class _AbstractConnector extends AbstractConnector { final AbstractConnector one; final AbstractConnector two; _AbstractConnector(this.one, this.two); @override K get(T state) { return two.get(one.get(state)); } @override SubReducer subReducer(Reducer reducer) { return one.subReducer((P state, Action action) { return two.subReducer(reducer)(state, action, false); }); } } ================================================ FILE: lib/src/redux_connector/map_like.dart ================================================ import 'connector.dart'; import 'generator.dart'; abstract class MapLike { Map _fieldsMap = {}; void clear() => _fieldsMap.clear(); Object operator [](String key) => _fieldsMap[key]; void operator []=(String key, Object value) => _fieldsMap[key] = value; bool containsKey(String key) => _fieldsMap.containsKey(key); void copyFrom(MapLike from) => _fieldsMap = {}..addAll(from._fieldsMap); } ConnOp withMapLike(String key) => ConnOp( get: (T state) => state[key], set: (T state, P sub) => state[key] = sub, ); class AutoInitConnector extends ConnOp { static final String Function() _gen = generator(); final String _key; final void Function(T state, P sub) _setHook; final P Function(T state) init; AutoInitConnector(this.init, {String key, void set(T state, P sub)}) : assert(init != null), _setHook = set, _key = key ?? _gen(); @override P get(T state) => state.containsKey(_key) ? state[_key] : (state[_key] = init(state)); @override void set(T state, P subState) { state[_key] = subState; _setHook?.call(state, subState); } } ================================================ FILE: lib/src/redux_connector/none.dart ================================================ import '../redux/redux.dart'; import 'op_mixin.dart'; class NoneConn extends ImmutableConn with ConnOpMixin { const NoneConn(); @override T get(T state) => state; @override T set(T state, T subState) => subState; } ================================================ FILE: lib/src/redux_connector/op_mixin.dart ================================================ import '../redux/redux.dart'; import '../redux_component/redux_component.dart'; mixin ConnOpMixin on AbstractConnector { Dependent operator +(AbstractLogic

logic) => createDependent(this, logic); } ================================================ FILE: lib/src/redux_connector/redux_connector.dart ================================================ export 'connector.dart'; export 'helper.dart'; export 'map_like.dart'; export 'none.dart'; export 'op_mixin.dart'; export 'reselect.dart'; ================================================ FILE: lib/src/redux_connector/reselect.dart ================================================ import 'package:flutter/foundation.dart'; import '../redux/redux.dart'; import 'op_mixin.dart'; bool _listEquals(List list1, List list2) { if (identical(list1, list2)) { return true; } if (list1 == null || list2 == null) { return false; } final int length = list1.length; if (length != list2.length) { return false; } for (int i = 0; i < length; i++) { final dynamic e1 = list1[i], e2 = list2[i]; if (e1 != e2) { if (e1 is List && e1.runtimeType == e2?.runtimeType) { if (!_listEquals(e1, e2)) { return false; } } return false; } } return true; } abstract class _BasicReselect extends MutableConn with ConnOpMixin { List _subsCache; P _pCache; bool _hasBeenCalled = false; List getSubs(T state); P reduceSubs(List list); @override P get(T state) { final List subs = getSubs(state); if (!_hasBeenCalled || !_listEquals(subs, _subsCache)) { _subsCache = subs; _pCache = reduceSubs(_subsCache); _hasBeenCalled = true; } return _pCache; } } abstract class Reselect1 extends _BasicReselect { K0 getSub0(T state); P computed(K0 state); @override List getSubs(T state) => [getSub0(state)]; @override P reduceSubs(List list) => Function.apply(computed, list); } abstract class Reselect2 extends _BasicReselect { K0 getSub0(T state); K1 getSub1(T state); P computed(K0 sub0, K1 sub1); @override List getSubs(T state) => [getSub0(state), getSub1(state)]; @override P reduceSubs(List list) => Function.apply(computed, list); } abstract class Reselect3 extends _BasicReselect { K0 getSub0(T state); K1 getSub1(T state); K2 getSub2(T state); P computed(K0 sub0, K1 sub1, K2 sub2); @override List getSubs(T state) => [ getSub0(state), getSub1(state), getSub2(state), ]; @override P reduceSubs(List list) => Function.apply(computed, list); } abstract class Reselect4 extends _BasicReselect { K0 getSub0(T state); K1 getSub1(T state); K2 getSub2(T state); K3 getSub3(T state); P computed(K0 sub0, K1 sub1, K2 sub2, K3 sub3); @override List getSubs(T state) => [ getSub0(state), getSub1(state), getSub2(state), getSub3(state), ]; @override P reduceSubs(List list) => Function.apply(computed, list); } abstract class Reselect5 extends _BasicReselect { K0 getSub0(T state); K1 getSub1(T state); K2 getSub2(T state); K3 getSub3(T state); K4 getSub4(T state); P computed(K0 sub0, K1 sub1, K2 sub2, K3 sub3, K4 sub4); @override List getSubs(T state) => [ getSub0(state), getSub1(state), getSub2(state), getSub3(state), getSub4(state), ]; @override P reduceSubs(List list) => Function.apply(computed, list); } abstract class Reselect6 extends _BasicReselect { K0 getSub0(T state); K1 getSub1(T state); K2 getSub2(T state); K3 getSub3(T state); K4 getSub4(T state); K5 getSub5(T state); P computed(K0 sub0, K1 sub1, K2 sub2, K3 sub3, K4 sub4, K5 sub5); @override List getSubs(T state) => [ getSub0(state), getSub1(state), getSub2(state), getSub3(state), getSub4(state), getSub5(state), ]; @override P reduceSubs(List list) => Function.apply(computed, list); } abstract class Reselect extends _BasicReselect { P computed(List list); @override P reduceSubs(List list) => Function.apply(computed, list); } /// issue [https://github.com/alibaba/fish-redux/issues/482] mixin ReselectMixin on MutableConn { List _cachedFactors; P _cachedResult; bool _hasBeenCalled = false; P computed(T state); List factors(T state) => [state]; @mustCallSuper @override P get(T state) { final List newFactors = factors(state); if (!_hasBeenCalled || !_listEquals(newFactors, _cachedFactors)) { _cachedFactors = newFactors.toList(growable: false); _cachedResult = computed(state); _hasBeenCalled = true; } return _cachedResult; } } ================================================ FILE: lib/src/redux_middleware/adapter_middleware/adapter_middleware.dart ================================================ export 'safety_adapter.dart'; ================================================ FILE: lib/src/redux_middleware/adapter_middleware/safety_adapter.dart ================================================ import 'package:flutter/widgets.dart' hide Action, Page; import '../../redux/redux.dart'; import '../../redux_component/redux_component.dart'; import '../../utils/utils.dart'; /// type = {0, 1} AdapterMiddleware safetyAdapter({ Widget Function(dynamic, StackTrace, {AbstractAdapter adapter, Store store, int type}) onError, }) { return (AbstractAdapter adapter, Store store) { return (AdapterBuilder next) { return isDebug() ? next : (dynamic state, Dispatch dispatch, ViewService viewService) { try { final ListAdapter result = next(state, dispatch, viewService); return ListAdapter((BuildContext buildContext, int index) { try { return result.itemBuilder(buildContext, index); } catch (e, stackTrace) { return onError?.call( e, stackTrace, adapter: adapter, store: store, type: 1, ) ?? Container(width: 0, height: 0); } }, result.itemCount); } catch (e, stackTrace) { final Widget errorWidget = onError?.call( e, stackTrace, adapter: adapter, store: store, type: 0, ); return errorWidget == null ? const ListAdapter(null, 0) : ListAdapter( (BuildContext buildContext, int index) => errorWidget, 1); } }; }; }; } ================================================ FILE: lib/src/redux_middleware/middleware/log.dart ================================================ import '../../redux/redux.dart'; import '../../utils/utils.dart'; /// Middleware for print action dispatch. /// It works on debug mode. Middleware logMiddleware({ String tag = 'redux', String Function(T) monitor, }) { return ({Dispatch dispatch, Get getState}) { return (Dispatch next) { return isDebug() ? (Action action) { print('---------- [$tag] ----------'); print('[$tag] ${action.type} ${action.payload}'); final T prevState = getState(); if (monitor != null) { print('[$tag] prev-state: ${monitor(prevState)}'); } next(action); final T nextState = getState(); if (monitor != null) { print('[$tag] next-state: ${monitor(nextState)}'); } // if (prevState == nextState) { // print('[$tag] warning: ${action.type} has not been used.'); // } print('========== [$tag] ================'); } : next; }; }; } ================================================ FILE: lib/src/redux_middleware/middleware/middleware.dart ================================================ export 'log.dart'; export 'performance.dart'; ================================================ FILE: lib/src/redux_middleware/middleware/performance.dart ================================================ import '../../redux/redux.dart'; import '../../utils/utils.dart'; /// Middleware for print action dispatch performance by time consuming. /// It works on debug mode. Middleware performanceMiddleware({String tag = 'redux'}) { return ({Dispatch dispatch, Get getState}) { return (Dispatch next) { return isDebug() ? (Action action) { final int markPrev = DateTime.now().microsecondsSinceEpoch; next(action); final int markNext = DateTime.now().microsecondsSinceEpoch; print('$tag performance: ${action.type} ${markNext - markPrev}'); } : next; }; }; } ================================================ FILE: lib/src/redux_middleware/redux_middleware.dart ================================================ export 'adapter_middleware/adapter_middleware.dart'; export 'middleware/middleware.dart'; export 'view_middleware/view_middleware.dart'; ================================================ FILE: lib/src/redux_middleware/view_middleware/safety_view.dart ================================================ import 'package:flutter/widgets.dart' hide Action, Page; import '../../redux/redux.dart'; import '../../redux_component/redux_component.dart'; import '../../utils/utils.dart'; ViewMiddleware safetyView( {Widget Function(dynamic, StackTrace, {AbstractComponent component, Store store}) onError}) { return (AbstractComponent component, Store store) { return (ViewBuilder next) { return isDebug() ? next : (dynamic state, Dispatch dispatch, ViewService viewService) { try { return next(state, dispatch, viewService); } catch (e, stackTrace) { return onError?.call( e, stackTrace, component: component, store: store, ) ?? Container(width: 0, height: 0); } }; }; }; } ================================================ FILE: lib/src/redux_middleware/view_middleware/view_middleware.dart ================================================ export 'safety_view.dart'; ================================================ FILE: lib/src/redux_routes/page_routes.dart ================================================ import 'package:flutter/widgets.dart' hide Action, Page; import '../redux_component/redux_component.dart'; /// Define a basic behavior of routes. abstract class AbstractRoutes { Widget buildPage(String path, dynamic arguments); } /// Each page has a unique store. @immutable class PageRoutes implements AbstractRoutes { final Map> pages; PageRoutes({ @required this.pages, /// For common enhance void Function(String, Page) visitor, }) : assert(pages != null, 'Expected the pages to be non-null value.') { if (visitor != null) { pages.forEach(visitor); } } @override Widget buildPage(String path, dynamic arguments) => pages[path]?.buildPage(arguments); } ================================================ FILE: lib/src/redux_routes/redux_routes.dart ================================================ export 'page_routes.dart'; ================================================ FILE: lib/src/utils/collections.dart ================================================ import 'dart:core'; /// Util for collections. class Collections { /// Wrap List.reduce with a check list is null or empty. static E reduce(List list, E combine(E e0, E e1)) => (list == null || list.isEmpty) ? null : list.reduce(combine); /// Wrap List.fold with a check list is null or empty. static T fold(T init, List list, T combine(T e0, E e1)) => (list == null || list.isEmpty) ? init : list.fold(init, combine); /// Flatten list /// For example: /// List a = ['a', 'b', 'c']; /// List b = ['1', '2', '3']; /// List> list = [a, b] // [[a, b, c], [1, 2, 3]] /// List listFlatten = Collections.flatten(list) // [a, b, c, 1, 2, 3] static List flatten(List> lists) => reduce(lists, merge); /// Merge two Iterable /// List a = ['a', 'b', 'c']; /// List b = ['1', '2', '3']; /// List listMerge = Collections.merge(a, b) // [a, b, c, 1, 2, 3] static List merge(Iterable a, Iterable b) => []..addAll(a ?? [])..addAll(b ?? []); static List clone(Iterable a) => (a == null || a.isEmpty) ? [] : ([]..addAll(a)); /// Cast map to list /// Map map = {'key0': 'a', 'key1': 'b', 'key2': 'c'}; /// Function mapFunction = (String value, String key) => value; /// List list = Collections.castMapToList( /// map, mapFunction); // [a, b, c] static List castMapToList(Map map0, T map(V v, K k)) => map0.entries .map((MapEntry entry) => map(entry.value, entry.key)) .toList(); /// Cast map with a map function static Map castMap(Map map0, V1 map(V0 v0, K k)) => {}..addEntries(castMapToList, K, V0>( map0, (V0 v, K k) => MapEntry(k, map(v, k)))); /// Emit item null and return new list. /// List list = ['1', '2', null, '3', null]; /// print(list) // [1, 2, null, 3, null] /// print(Collections.compact(list)); // [1, 2, 3] static List compact(Iterable list, {bool growable = true}) => list?.where((T e) => e != null)?.toList(growable: growable); /// Check if an Object is Empty. static bool isEmpty(Object value) { if (value == null) { return true; } else { if (value is String) { return value.isEmpty; } else if (value is List) { return value.isEmpty; } else if (value is Map) { return value.isEmpty; } else if (value is Set) { return value.isEmpty; } else { return false; } } } static bool isNotEmpty(Object value) => !isEmpty(value); } ================================================ FILE: lib/src/utils/debug.dart ================================================ bool _debugFlag = false; /// Is app run a debug mode. bool isDebug() { /// Assert statements have no effect in production code; /// they’re for development only. Flutter enables asserts in debug mode. assert(() { _debugFlag = true; return _debugFlag; }()); return _debugFlag; } /// wrap println with bool return. bool println(Object object) { print(object); return true; } ================================================ FILE: lib/src/utils/hash.dart ================================================ /// Jenkins hash function, optimized for small integers. /// /// Borrowed from the dart sdk: sdk/lib/math/jenkins_smi_hash.dart. int hash(Iterable values) { int hash = 0; /// combine for (int value in values) { hash = 0x1fffffff & (hash + value); hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); hash = hash ^ (hash >> 6); } /// finish hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); hash = hash ^ (hash >> 11); return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); } ================================================ FILE: lib/src/utils/tuple.dart ================================================ import 'hash.dart'; class Tuple0 { const Tuple0(); @override bool operator ==(Object other) => other is Tuple0; @override int get hashCode => hash([0]); } /// Represents a 1-tuple /// Mutable data types are easier to use. class Tuple1 { T0 i0; Tuple1([this.i0]); @override String toString() => '[$i0]'; @override bool operator ==(Object other) => other is Tuple1 && other.i0 == i0; @override int get hashCode => hash([i0.hashCode]); } /// Represents a 2-tuple or pair. class Tuple2 { /// First item of the tuple T0 i0; /// Second item of the tuple T1 i1; /// Create a new tuple value with the specified items. Tuple2([this.i0, this.i1]); @override String toString() => '[$i0, $i1]'; @override bool operator ==(Object other) => other is Tuple2 && other.i0 == i0 && other.i1 == i1; @override int get hashCode => hash([i0.hashCode, i1.hashCode]); } /// Represents a 3-tuple or pair. class Tuple3 { T0 i0; T1 i1; T2 i2; Tuple3([this.i0, this.i1, this.i2]); @override String toString() => '[$i0, $i1, $i2]'; @override bool operator ==(Object other) => other is Tuple3 && other.i0 == i0 && other.i1 == i1 && other.i2 == i2; @override int get hashCode => hash([i0.hashCode, i1.hashCode, i2.hashCode]); } /// Represents a 4-tuple or pair. class Tuple4 { T0 i0; T1 i1; T2 i2; T3 i3; Tuple4([this.i0, this.i1, this.i2, this.i3]); @override String toString() => '[$i0, $i1, $i2, $i3]'; @override bool operator ==(Object other) => other is Tuple4 && other.i0 == i0 && other.i1 == i1 && other.i2 == i2 && other.i3 == i3; @override int get hashCode => hash([i0.hashCode, i1.hashCode, i2.hashCode, i3.hashCode]); } /// Represents a 5-tuple or pair. class Tuple5 { T0 i0; T1 i1; T2 i2; T3 i3; T4 i4; Tuple5([this.i0, this.i1, this.i2, this.i3, this.i4]); @override String toString() => '[$i0, $i1, $i2, $i3, $i4]'; @override bool operator ==(Object other) => other is Tuple5 && other.i0 == i0 && other.i1 == i1 && other.i2 == i2 && other.i3 == i3 && other.i4 == i4; @override int get hashCode => hash( [i0.hashCode, i1.hashCode, i2.hashCode, i3.hashCode, i4.hashCode]); } /// Represents a 6-tuple or pair. class Tuple6 { T0 i0; T1 i1; T2 i2; T3 i3; T4 i4; T5 i5; Tuple6([this.i0, this.i1, this.i2, this.i3, this.i4, this.i5]); @override String toString() => '[$i0, $i1, $i2, $i3, $i4, $i5]'; @override bool operator ==(Object other) => other is Tuple6 && other.i0 == i0 && other.i1 == i1 && other.i2 == i2 && other.i3 == i3 && other.i4 == i4 && other.i5 == i5; @override int get hashCode => hash([ i0.hashCode, i1.hashCode, i2.hashCode, i3.hashCode, i4.hashCode, i5.hashCode ]); } ================================================ FILE: lib/src/utils/utils.dart ================================================ export 'collections.dart'; export 'debug.dart'; export 'tuple.dart'; ================================================ FILE: pubspec.yaml ================================================ name: fish_redux description: Fish Redux is an assembled flutter application framework based on Redux state management. version: 0.3.7 author: Alibaba Xianyu Team homepage: https://github.com/alibaba/fish-redux environment: sdk: ">=2.2.0 <3.0.0" dependencies: flutter: sdk: flutter dev_dependencies: test: ^1.5.1 flutter_test: sdk: flutter test_widgets: path: test/test_widgets # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec # The following section is specific to Flutter. flutter: # To add assets to your package, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # # For details regarding assets in packages, see # https://flutter.io/assets-and-images/#from-packages # # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.io/assets-and-images/#resolution-aware. # To add custom fonts to your package, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts in packages, see # https://flutter.io/custom-fonts/#from-packages ================================================ FILE: test/lib/all_test.dart ================================================ import 'package:test/test.dart'; import 'redux/redux_test.dart' as redux; import 'redux_adapter/redux_adapter_test.dart' as redux_adapter; import 'redux_aop/redux_aop_test.dart' as redux_aop; import 'redux_component/redux_component_test.dart' as redux_component; import 'redux_connector/redux_connector_test.dart' as redux_connector; import 'redux_middleware/redux_middleware_test.dart' as redux_middleware; import 'redux_routes/redux_routes_test.dart' as redux_routes; import 'utils/utils_test.dart' as utils; void main() { group('all_test', () { redux.main(); redux_adapter.main(); redux_aop.main(); redux_component.main(); redux_connector.main(); redux_middleware.main(); redux_routes.main(); utils.main(); }); } ================================================ FILE: test/lib/instrument.dart ================================================ import 'package:fish_redux/fish_redux.dart'; typedef ViewInstrument = void Function( T state, Dispatch dispatch, ViewService viewService); ViewBuilder instrumentView( ViewBuilder builder, ViewInstrument pre) => ( T state, Dispatch dispatch, ViewService viewService, ) { if (pre != null) { pre(state, dispatch, viewService); } return builder(state, dispatch, viewService); }; typedef initStateInstrumentPre

= void Function(P); typedef initStateInstrumentSuf> = void Function(T); InitState instrumentInitState, P>( InitState initState, {initStateInstrumentPre

pre, initStateInstrumentSuf suf}) => (P params) { if (pre != null) { pre(params); } final T state = initState(params); if (suf != null) { suf(state); } return state; }; typedef ReducerInstrument = void Function(T state, Action action); Reducer instrumentReducer(Reducer reducer, {ReducerInstrument pre, ReducerInstrument suf, ReducerInstrument change}) => (T state, Action action) { T newState = state; if (pre != null) { pre(state, action); } newState = reducer(state, action); if (suf != null) { suf(newState, action); } if (change != null && newState != state) { change(newState, action); } return newState; }; typedef EffectInstrument = void Function(Action action, Get getState); Effect instrumentEffect(Effect effect, EffectInstrument pre) => (Action action, Context ctx) { if (pre != null) { pre(action, () => ctx.state); } return effect(action, ctx); }; typedef MiddlewareInstrument = void Function(Action action, Get getState); Middleware instrumentMiddleware(Middleware middleware, {EffectInstrument pre, EffectInstrument suf}) => ({ Dispatch dispatch, Get getState, }) { return (Dispatch next) { return (Action action) { if (pre != null) { pre(action, getState); } middleware(dispatch: dispatch, getState: getState)(next)(action); if (suf != null) { suf(action, getState); } }; }; }; typedef ErrorInstrument = void Function(Exception exception, Context ctx); ================================================ FILE: test/lib/redux/redux_test.dart ================================================ import 'package:test/test.dart'; import 'store_test.dart' as store; void main() { group('redux_test', () { store.main(); }); } ================================================ FILE: test/lib/redux/store_test.dart ================================================ import 'dart:async'; import 'dart:ui'; import 'package:fish_redux/fish_redux.dart'; import 'package:test/test.dart'; import '../instrument.dart'; import '../track.dart'; enum ToDoAction { add, remove, done } class Todo { String id; String title; String desc; bool isDone = false; Todo(); factory Todo.copy(Todo toDo) { return Todo() ..id = toDo.id ..title = toDo.title ..desc = toDo.desc ..isDone = toDo.isDone; } } class ToDoList { List list = []; ToDoList(); factory ToDoList.copy(ToDoList toDoState) { return ToDoList()..list.addAll(toDoState.list); } } ToDoList toDoReducer(ToDoList state, Action action) { final ToDoList newState = ToDoList.copy(state); if (action.type == ToDoAction.add) { newState.list.add(action.payload); } else if (action.type == ToDoAction.remove) { newState.list.removeWhere((Todo toDo) => toDo.id == action.payload); } else if (action.type == ToDoAction.done) { newState.list .firstWhere((Todo toDo) => toDo.id == action.payload, orElse: () => null) ?.isDone = true; } return newState; } ToDoList defaultReducer(ToDoList state, Action action) => ToDoList.copy(state); void main() { group('store', () { test('create without preloadedState', () { expect(() => createStore(null, null), throwsArgumentError); }); test('create without reducer', () { final Store store = createStore(ToDoList(), null); expect(store, isNotNull); expect(store, const TypeMatcher>()); expect(store.getState(), isNotNull); expect(store.getState(), const TypeMatcher()); }); test('create', () { final Store store = createStore(ToDoList(), toDoReducer); expect(store, isNotNull); expect(store, const TypeMatcher>()); expect(store.getState(), isNotNull); expect(store.getState(), const TypeMatcher()); }); test('dispatch & state', () { final Track track = Track(); final Store store = createStore( ToDoList(), instrumentReducer(toDoReducer, pre: (ToDoList state, Action action) { if (action.type == ToDoAction.add) { track.append('onReduce_Add'); } else if (action.type == ToDoAction.done) { track.append('onReduce_Done'); } else if (action.type == ToDoAction.remove) { track.append('onReduce_Remove'); } })); expect(store.getState(), isNotNull); expect(store.getState().list, isNotNull); expect(store.getState().list, isEmpty); track.append('dispatch_Add'); store.dispatch(Action(ToDoAction.add, payload: Todo() ..id = 'unique' ..title = 'test' ..desc = 'just test')); expect(store.getState(), isNotNull); expect(store.getState().list, isNotNull); expect(store.getState().list, isNotEmpty); expect(store.getState().list.first, isNotNull); expect(store.getState().list.first.id, 'unique'); expect(store.getState().list.first.title, 'test'); expect(store.getState().list.first.desc, 'just test'); expect(store.getState().list.first.isDone, isFalse); track.append('dispatch_Done'); store.dispatch(const Action(ToDoAction.done, payload: 'unique')); expect(store.getState(), isNotNull); expect(store.getState().list, isNotNull); expect(store.getState().list, isNotEmpty); expect(store.getState().list.first, isNotNull); expect(store.getState().list.first.id, 'unique'); expect(store.getState().list.first.title, 'test'); expect(store.getState().list.first.desc, 'just test'); expect(store.getState().list.first.isDone, isTrue); track.append('dispatch_Remove'); store.dispatch(const Action(ToDoAction.remove, payload: 'unique')); expect(store.getState(), isNotNull); expect(store.getState().list, isNotNull); expect(store.getState().list, isEmpty); expect( track, Track.tags([ 'dispatch_Add', 'onReduce_Add', 'dispatch_Done', 'onReduce_Done', 'dispatch_Remove', 'onReduce_Remove' ])); }); test('subscribe', () { final Track track = Track(); final Store store = createStore( ToDoList(), instrumentReducer(toDoReducer, pre: (ToDoList state, Action action) { if (action.type == ToDoAction.add) { track.append('onReduce_Add'); } else if (action.type == ToDoAction.done) { track.append('onReduce_Done'); } else if (action.type == ToDoAction.remove) { track.append('onReduce_Remove'); } else { track.append('onReduce_Unkonw'); } })); Todo firstToDo; store.subscribe(() { track.append('onSubscribe'); firstToDo = store.getState().list.isEmpty ? null : Todo.copy(store.getState().list.first); }); expect(firstToDo, isNull); track.append('dispatch_Add'); store.dispatch(Action(ToDoAction.add, payload: Todo() ..id = 'unique' ..title = 'test' ..desc = 'just test')); expect(firstToDo, isNotNull); expect(firstToDo.id, 'unique'); expect(firstToDo.title, 'test'); expect(firstToDo.desc, 'just test'); expect(firstToDo.isDone, isFalse); track.append('dispatch_Done'); store.dispatch(const Action(ToDoAction.done, payload: 'unique')); expect(firstToDo, isNotNull); expect(firstToDo.id, 'unique'); expect(firstToDo.title, 'test'); expect(firstToDo.desc, 'just test'); expect(firstToDo.isDone, isTrue); track.append('dispatch_Remove'); store.dispatch(const Action(ToDoAction.remove, payload: 'unique')); expect(firstToDo, isNull); expect( track, Track.tags([ 'dispatch_Add', 'onReduce_Add', 'onSubscribe', 'dispatch_Done', 'onReduce_Done', 'onSubscribe', 'dispatch_Remove', 'onReduce_Remove', 'onSubscribe' ])); }); test('unsubscribe', () { final Store store = createStore(ToDoList(), toDoReducer); Todo firstToDo; final VoidCallback unsubscribe = store.subscribe(() { firstToDo = store.getState().list.isEmpty ? null : Todo.copy(store.getState().list.first); }); expect(firstToDo, isNull); store.dispatch(Action(ToDoAction.add, payload: Todo() ..id = 'unique' ..title = 'test' ..desc = 'just test')); expect(firstToDo, isNotNull); expect(firstToDo.id, 'unique'); expect(firstToDo.title, 'test'); expect(firstToDo.desc, 'just test'); expect(firstToDo.isDone, isFalse); unsubscribe(); store.dispatch(const Action(ToDoAction.remove, payload: 'unique')); expect(firstToDo, isNotNull); expect(firstToDo.id, 'unique'); expect(firstToDo.title, 'test'); expect(firstToDo.desc, 'just test'); expect(firstToDo.isDone, isFalse); }); test('observable', () { final Track track = Track(); final Store store = createStore( ToDoList(), instrumentReducer(toDoReducer, pre: (ToDoList state, Action action) { if (action.type == ToDoAction.add) { track.append('onReduce_Add'); } else if (action.type == ToDoAction.done) { track.append('onReduce_Done'); } else if (action.type == ToDoAction.remove) { track.append('onReduce_Remove'); } else { track.append('onReduce_Unkonw'); } })); Todo firstToDo; store.observable().listen((ToDoList list) { track.append('observed'); firstToDo = list.list.isEmpty ? null : Todo.copy(list.list.first); }); expect(firstToDo, isNull); track.append('dispatch_Add'); store.dispatch(Action(ToDoAction.add, payload: Todo() ..id = 'unique' ..title = 'test' ..desc = 'just test')); expect(firstToDo, isNotNull); expect(firstToDo.id, 'unique'); expect(firstToDo.title, 'test'); expect(firstToDo.desc, 'just test'); expect(firstToDo.isDone, isFalse); track.append('dispatch_Done'); store.dispatch(const Action(ToDoAction.done, payload: 'unique')); expect(firstToDo, isNotNull); expect(firstToDo.id, 'unique'); expect(firstToDo.title, 'test'); expect(firstToDo.desc, 'just test'); expect(firstToDo.isDone, isTrue); track.append('dispatch_Remove'); store.dispatch(const Action(ToDoAction.remove, payload: 'unique')); expect(firstToDo, isNull); expect( track, Track.tags([ 'dispatch_Add', 'onReduce_Add', 'observed', 'dispatch_Done', 'onReduce_Done', 'observed', 'dispatch_Remove', 'onReduce_Remove', 'observed' ])); }); test('cancel observable', () { final Store store = createStore(ToDoList(), toDoReducer); Todo firstToDo; final StreamSubscription subscription = store.observable().listen((ToDoList list) { firstToDo = list.list.isEmpty ? null : Todo.copy(list.list.first); }); expect(firstToDo, isNull); store.dispatch(Action(ToDoAction.add, payload: Todo() ..id = 'unique' ..title = 'test' ..desc = 'just test')); expect(firstToDo, isNotNull); expect(firstToDo.id, 'unique'); expect(firstToDo.title, 'test'); expect(firstToDo.desc, 'just test'); expect(firstToDo.isDone, isFalse); subscription.cancel(); store.dispatch(const Action(ToDoAction.remove, payload: 'unique')); expect(firstToDo, isNotNull); expect(firstToDo.id, 'unique'); expect(firstToDo.title, 'test'); expect(firstToDo.desc, 'just test'); expect(firstToDo.isDone, isFalse); }); test('replaceReducer', () { final Store store = createStore(ToDoList(), toDoReducer); expect(store.getState(), isNotNull); expect(store.getState().list, isNotNull); expect(store.getState().list, isEmpty); store.dispatch(Action(ToDoAction.add, payload: Todo() ..id = 'unique' ..title = 'test' ..desc = 'just test')); expect(store.getState(), isNotNull); expect(store.getState().list, isNotNull); expect(store.getState().list, isNotEmpty); expect(store.getState().list.first, isNotNull); expect(store.getState().list.first.id, 'unique'); expect(store.getState().list.first.title, 'test'); expect(store.getState().list.first.desc, 'just test'); expect(store.getState().list.first.isDone, isFalse); store.replaceReducer(defaultReducer); store.dispatch(const Action(ToDoAction.remove, payload: 'unique')); expect(store.getState(), isNotNull); expect(store.getState().list, isNotNull); expect(store.getState().list, isNotEmpty); expect(store.getState().list.first, isNotNull); expect(store.getState().list.first.id, 'unique'); expect(store.getState().list.first.title, 'test'); expect(store.getState().list.first.desc, 'just test'); expect(store.getState().list.first.isDone, isFalse); store.replaceReducer(toDoReducer); store.dispatch(const Action(ToDoAction.remove, payload: 'unique')); expect(store.getState(), isNotNull); expect(store.getState().list, isNotNull); expect(store.getState().list, isEmpty); }); test('applyMiddleware', () { Object lastAction; ToDoList lastState; final Track track = Track(); final Middleware toDoMiddleware = ( {Dispatch dispatch, Get getState}) => (Dispatch next) => (Action action) { lastAction = action.type; lastState = ToDoList.copy(getState()); next(action); }; final Store store = createStore( ToDoList(), instrumentReducer(toDoReducer, pre: (ToDoList state, Action action) { if (action.type == ToDoAction.add) { track.append('onReduce_Add'); } else if (action.type == ToDoAction.done) { track.append('onReduce_Done'); } else if (action.type == ToDoAction.remove) { track.append('onReduce_Remove'); } else { track.append('onReduce_Unkonw'); } }), applyMiddleware(>[ instrumentMiddleware(toDoMiddleware, pre: (Action action, Get getReducer) { if (action.type == ToDoAction.add) { track.append('onMiddleware_Add'); } else if (action.type == ToDoAction.done) { track.append('onMiddleware_Done'); } else if (action.type == ToDoAction.remove) { track.append('onMiddleware_Remove'); } else { track.append('onMiddleware_Unkonw'); } }) ])); store.subscribe(() { track.append('onSubscribe'); }); expect(store, isNotNull); expect(store, const TypeMatcher>()); expect(store.getState(), isNotNull); expect(store.getState(), const TypeMatcher()); expect(store.getState(), isNotNull); expect(store.getState().list, isNotNull); expect(store.getState().list, isEmpty); expect(lastAction, isNull); expect(lastState, isNull); track.append('dispatch_Add'); store.dispatch(Action(ToDoAction.add, payload: Todo() ..id = 'unique' ..title = 'test' ..desc = 'just test')); expect(lastAction, ToDoAction.add); expect(lastState, isNotNull); expect(lastState.list, isNotNull); expect(lastState.list, isEmpty); track.append('dispatch_Done'); store.dispatch(const Action(ToDoAction.done, payload: 'unique')); expect(lastAction, ToDoAction.done); expect(lastState, isNotNull); expect(lastState.list, isNotNull); expect(lastState.list, isNotEmpty); expect(lastState.list.first, isNotNull); expect(lastState.list.first.id, 'unique'); expect(lastState.list.first.title, 'test'); expect(lastState.list.first.desc, 'just test'); expect(lastState.list.first.isDone, isTrue); track.append('dispatch_Remove'); store.dispatch(const Action(ToDoAction.remove, payload: 'unique')); expect(lastAction, ToDoAction.remove); expect(lastState, isNotNull); expect(lastState.list, isNotNull); expect(lastState.list, isNotEmpty); expect(lastState.list.first, isNotNull); expect(lastState.list.first.id, 'unique'); expect(lastState.list.first.title, 'test'); expect(lastState.list.first.desc, 'just test'); expect(lastState.list.first.isDone, isTrue); expect( track, Track.tags([ 'dispatch_Add', 'onMiddleware_Add', 'onReduce_Add', 'onSubscribe', 'dispatch_Done', 'onMiddleware_Done', 'onReduce_Done', 'onSubscribe', 'dispatch_Remove', 'onMiddleware_Remove', 'onReduce_Remove', 'onSubscribe' ])); }); }); } ================================================ FILE: test/lib/redux_adapter/adapter_test.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import 'package:flutter_test/flutter_test.dart'; import 'package:test_widgets/adapter/action.dart'; import 'package:test_widgets/adapter/adapter.dart'; import 'package:test_widgets/adapter/page.dart'; import 'package:test_widgets/adapter/state.dart'; import 'package:test_widgets/test_base.dart'; import '../instrument.dart'; import '../track.dart'; void main() { group('adapter', () { test('create', () { TestPage page = TestPage( initState: initState, view: pageView, dependencies: Dependencies( adapter: NoneConn() + TestAdapter( adapter: toDoListAdapter, reducer: toDoListReducer, effect: toDoListEffect))); expect(page, isNotNull); final Widget pageWidget = page.buildPage(pageInitParams); expect(pageWidget, isNotNull); }); testWidgets('build', (WidgetTester tester) async { await tester.pumpWidget(TestStub(TestPage( initState: initState, view: pageView, dependencies: Dependencies( adapter: NoneConn() + TestAdapter( adapter: toDoListAdapter, reducer: toDoListReducer, effect: toDoListEffect))) .buildPage(pageInitParams))); expect(find.byKey(const ValueKey('mark-0')), findsOneWidget); expect(find.byKey(const ValueKey('edit-0')), findsOneWidget); expect(find.text('desc-0'), findsOneWidget); expect(find.byKey(const ValueKey('remove-0')), findsOneWidget); expect(find.text('title-0'), findsOneWidget); expect(find.byKey(const ValueKey('mark-1')), findsOneWidget); expect(find.byKey(const ValueKey('edit-1')), findsOneWidget); expect(find.text('desc-1'), findsOneWidget); expect(find.byKey(const ValueKey('remove-1')), findsOneWidget); expect(find.text('title-1'), findsOneWidget); expect(find.byKey(const ValueKey('mark-2')), findsOneWidget); expect(find.byKey(const ValueKey('edit-2')), findsOneWidget); expect(find.text('desc-2'), findsOneWidget); expect(find.byKey(const ValueKey('remove-2')), findsOneWidget); expect(find.text('title-2'), findsOneWidget); expect(find.byKey(const ValueKey('mark-3')), findsOneWidget); expect(find.byKey(const ValueKey('edit-3')), findsOneWidget); expect(find.text('desc-3'), findsOneWidget); expect(find.byKey(const ValueKey('remove-3')), findsOneWidget); expect(find.text('title-3'), findsOneWidget); expect(find.text('mark\ndone'), findsNWidgets(3)); expect(find.text('done'), findsOneWidget); }); testWidgets('reducer', (WidgetTester tester) async { Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: initState, view: instrumentView(pageView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('build', state.clone()); }), dependencies: Dependencies( adapter: NoneConn() + TestAdapter( adapter: toDoListAdapter, reducer: instrumentReducer(toDoListReducer, suf: (ToDoList state, Action action) { track.append('onReduce', state.clone()); }), effect: toDoListEffect))) .buildPage(pageInitParams))); await tester.tap(find.byKey(const ValueKey('mark-0'))); await tester.pump(); expect(find.text('mark\ndone'), findsNWidgets(2)); expect(find.text('done'), findsNWidgets(2)); await tester.tap(find.byKey(const ValueKey('mark-1'))); await tester.pump(); expect(find.text('mark\ndone'), findsNWidgets(1)); expect(find.text('done'), findsNWidgets(3)); await tester.tap(find.byKey(const ValueKey('remove-2'))); await tester.pump(); expect(find.byKey(const ValueKey('remove-2')), findsNothing); expect(find.text('desc-2'), findsNothing); expect(find.text('title-2'), findsNothing); await tester.tap(find.byKey(const ValueKey('remove-3'))); await tester.pump(); expect(find.byKey(const ValueKey('remove-3')), findsNothing); expect(find.text('desc-3'), findsNothing); expect(find.text('title-3'), findsNothing); ToDoList mockState = ToDoList.fromMap(pageInitParams); expect( track, Track.pins([ Pin('build', mockState.clone()), Pin('onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.markDone, payload: mockState.list.firstWhere((i) => i.id == '0'))); return mockState.clone(); }), Pin('build', mockState.clone()), Pin('onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.markDone, payload: mockState.list.firstWhere((i) => i.id == '1'))); return mockState.clone(); }), Pin('build', mockState.clone()), Pin('onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.remove, payload: mockState.list.firstWhere((i) => i.id == '2'))); return mockState.clone(); }), Pin('build', mockState.clone()), Pin('onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.remove, payload: mockState.list.firstWhere((i) => i.id == '3'))); return mockState.clone(); }), Pin('build', mockState.clone()), ])); }); testWidgets('effect', (WidgetTester tester) async { Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: initState, view: instrumentView(pageView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('build', state.clone()); }), dependencies: Dependencies( adapter: NoneConn() + TestAdapter( adapter: toDoListAdapter, reducer: instrumentReducer(toDoListReducer, change: (ToDoList state, Action action) { track.append('onReduce', state.clone()); }), effect: instrumentEffect(toDoListEffect, (Action action, Get getState) { if (action.type == ToDoListAction.onAdd) { track.append('onAdd', getState().clone()); } else if (action.type == ToDoListAction.onEdit) { track.append('onEdit', getState().clone()); } })))) .buildPage(pageInitParams))); await tester.longPress(find.byKey(const ValueKey('mark-0'))); await tester.pump(); expect(find.text('title-mock', skipOffstage: false), findsNWidgets(1)); expect(find.text('desc-mock', skipOffstage: false), findsNWidgets(1)); await tester.longPress(find.byKey(const ValueKey('mark-0'))); await tester.pump(); expect(find.text('title-mock', skipOffstage: false), findsNWidgets(2)); expect(find.text('desc-mock', skipOffstage: false), findsNWidgets(2)); await tester.tap(find.byKey(const ValueKey('edit-0'))); await tester.pump(); expect(find.text('title-0', skipOffstage: false), findsOneWidget); expect(find.text('desc-0-effect', skipOffstage: false), findsOneWidget); ToDoList mockState = ToDoList.fromMap(pageInitParams); expect( track, Track.pins([ Pin('build', mockState.clone()), Pin('onAdd', mockState.clone()), Pin('onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.add, payload: Todo.mock())); return mockState.clone(); }), Pin('build', mockState.clone()), Pin('onAdd', mockState.clone()), Pin('onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.add, payload: Todo.mock())); return mockState.clone(); }), Pin('build', mockState.clone()), Pin('onEdit', mockState.clone()), Pin('onReduce', () { Todo toDo = mockState.list[0].clone(); toDo.desc = '${toDo.desc}-effect'; mockState = toDoListReducer( mockState, Action(ToDoListAction.edit, payload: toDo)); return mockState.clone(); }), Pin('build', mockState.clone()), ])); }); testWidgets('effectAsync', (WidgetTester tester) async { Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: initState, view: instrumentView(pageView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('build', state.clone()); }), dependencies: Dependencies( adapter: NoneConn() + TestAdapter( adapter: toDoListAdapter, reducer: instrumentReducer(toDoListReducer, change: (ToDoList state, Action action) { track.append('onReduce', state.clone()); }), effect: instrumentEffect(toDoListEffectAsync, (Action action, Get getState) { if (action.type == ToDoListAction.onAdd) { track.append('onAdd', getState().clone()); } else if (action.type == ToDoListAction.onEdit) { track.append('onEdit', getState().clone()); } })))) .buildPage(pageInitParams))); await tester.longPress(find.byKey(const ValueKey('mark-0'))); await tester.pump(Duration(seconds: 3)); expect(find.text('title-mock', skipOffstage: false), findsNWidgets(1)); expect(find.text('desc-mock', skipOffstage: false), findsNWidgets(1)); await tester.longPress(find.byKey(const ValueKey('mark-0'))); await tester.pump(Duration(seconds: 3)); expect(find.text('title-mock', skipOffstage: false), findsNWidgets(2)); expect(find.text('desc-mock', skipOffstage: false), findsNWidgets(2)); await tester.tap(find.byKey(const ValueKey('edit-0'))); await tester.pump(Duration(seconds: 3)); expect(find.text('title-0', skipOffstage: false), findsOneWidget); expect(find.text('desc-0-effect', skipOffstage: false), findsOneWidget); ToDoList mockState = ToDoList.fromMap(pageInitParams); expect( track, Track.pins([ Pin('build', mockState.clone()), Pin('onAdd', mockState.clone()), Pin('onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.add, payload: Todo.mock())); return mockState.clone(); }), Pin('build', mockState.clone()), Pin('onAdd', mockState.clone()), Pin('onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.add, payload: Todo.mock())); return mockState.clone(); }), Pin('build', mockState.clone()), Pin('onEdit', mockState.clone()), Pin('onReduce', () { Todo toDo = mockState.list[0].clone(); toDo.desc = '${toDo.desc}-effect'; mockState = toDoListReducer( mockState, Action(ToDoListAction.edit, payload: toDo)); print(mockState); return mockState.clone(); }), Pin('build', mockState.clone()), ])); }); testWidgets('effect', (WidgetTester tester) async { Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: initState, view: instrumentView(pageView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('build', state.clone()); }), dependencies: Dependencies( adapter: NoneConn() + TestAdapter( adapter: toDoListAdapter, reducer: instrumentReducer(toDoListReducer, change: (ToDoList state, Action action) { track.append('onReduce', state.clone()); }), effect: (Action action, Context ctx) => instrumentEffect(toDoListEffectAsync, (Action action, Get getState) { if (action.type == ToDoListAction.onAdd) { track.append('onAdd', getState().clone()); } else if (action.type == ToDoListAction.onEdit) { track.append('onEdit', getState().clone()); } })(action, ctx)))) .buildPage(pageInitParams))); await tester.longPress(find.byKey(const ValueKey('mark-0'))); await tester.pump(Duration(seconds: 3)); expect(find.text('title-mock', skipOffstage: false), findsNWidgets(1)); expect(find.text('desc-mock', skipOffstage: false), findsNWidgets(1)); await tester.longPress(find.byKey(const ValueKey('mark-0'))); await tester.pump(Duration(seconds: 3)); expect(find.text('title-mock', skipOffstage: false), findsNWidgets(2)); expect(find.text('desc-mock', skipOffstage: false), findsNWidgets(2)); await tester.tap(find.byKey(const ValueKey('edit-0'))); await tester.pump(Duration(seconds: 3)); expect(find.text('title-0', skipOffstage: false), findsOneWidget); expect(find.text('desc-0-effect', skipOffstage: false), findsOneWidget); ToDoList mockState = ToDoList.fromMap(pageInitParams); expect( track, Track.pins([ Pin('build', mockState.clone()), Pin('onAdd', mockState.clone()), Pin('onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.add, payload: Todo.mock())); return mockState.clone(); }), Pin('build', mockState.clone()), Pin('onAdd', mockState.clone()), Pin('onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.add, payload: Todo.mock())); return mockState.clone(); }), Pin('build', mockState.clone()), Pin('onEdit', mockState.clone()), Pin('onReduce', () { Todo toDo = mockState.list[0].clone(); toDo.desc = '${toDo.desc}-effect'; mockState = toDoListReducer( mockState, Action(ToDoListAction.edit, payload: toDo)); print(mockState); return mockState.clone(); }), Pin('build', mockState.clone()), ])); }); }); } ================================================ FILE: test/lib/redux_adapter/dynamic_adapter_test.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import 'package:flutter_test/flutter_test.dart'; import 'package:test_widgets/dynamic_flow_adapter/action.dart'; import 'package:test_widgets/dynamic_flow_adapter/component.dart'; import 'package:test_widgets/dynamic_flow_adapter/dynamic_flow_adapter.dart'; import 'package:test_widgets/dynamic_flow_adapter/page.dart'; import 'package:test_widgets/dynamic_flow_adapter/state.dart'; import 'package:test_widgets/test_base.dart'; import '../instrument.dart'; import '../track.dart'; class ToDoComponentInstrument extends TestComponent { ToDoComponentInstrument(final Track track) : super( view: instrumentView(toDoView, (Todo state, Dispatch dispatch, ViewService viewService) { track.append('toDo-build', state.clone()); }), reducer: instrumentReducer(toDoReducer, change: (Todo state, Action action) { track.append('toDo-onReduce', state.clone()); }), effect: instrumentEffect(toDoEffect, (Action action, Get getState) { if (action.type == ToDoAction.onEdit) { track.append('toDo-onEdit', getState().clone()); } else if (action.type == ToDoAction.broadcast) { track.append('toDo-onToDoBroadcast', getState().clone()); } else if (action.type == ToDoListAction.broadcast) { track.append('toDo-onPageBroadcast', getState().clone()); } }), shouldUpdate: shouldUpdate, key: (Todo toDo) => GlobalObjectKey(toDo.id), ); } class ToDoComponentNoReducer extends TestComponent { ToDoComponentNoReducer(final Track track) : super( view: instrumentView(toDoView, (Todo state, Dispatch dispatch, ViewService viewService) { track.append('toDo-build', state.clone()); }), effect: instrumentEffect(toDoEffect, (Action action, Get getState) { if (action.type == ToDoAction.onEdit) { track.append('toDo-onEdit', getState().clone()); } else if (action.type == ToDoAction.broadcast) { track.append('toDo-onToDoBroadcast', getState().clone()); } else if (action.type == ToDoListAction.broadcast) { track.append('toDo-onPageBroadcast', getState().clone()); } }), shouldUpdate: shouldUpdate, key: (Todo toDo) => GlobalObjectKey(toDo.id), ); } Dependencies toDoListDependencies(final Track track, {bool noReducer = false}) => Dependencies( adapter: NoneConn() + TestDynamicFlowAdapter( pool: >{ 'toDo': ToDoComponentInstrument(track), 'toDoNoReducer': ToDoComponentNoReducer(track), }, connector: ConnOp>( get: (ToDoList toDoList) => toDoList.list .map((Todo toDo) => noReducer ? ItemBean('toDoNoReducer', toDo) : ItemBean('toDo', toDo)) .toList(), set: (ToDoList toDoList, List beans) { toDoList.list.clear(); toDoList.list.addAll(beans .map((ItemBean bean) => bean.data) .toList()); }), reducer: instrumentReducer(toDoListReducer, change: (ToDoList state, Action action) { track.append('adapter-onReduce', state.clone()); }), effect: instrumentEffect(toDoListEffect, (Action action, Get getState) { if (action.type == ToDoListAction.onAdd) { track.append('adapter-onAdd', getState().clone()); } else if (action.type == ToDoAction.broadcast) { track.append('adapter-onToDoBroadcast', getState().clone()); } else if (action.type == ToDoListAction.broadcast) { track.append('adapter-onPageBroadcast', getState().clone()); } }))); void main() { group('dynamic_flow_adapter', () { test('create', () { final Track track = Track(); final TestComponent component = ToDoComponentInstrument(track); expect(component, isNotNull); Widget page = TestPage( initState: initState, view: pageView, dependencies: toDoListDependencies(track)) .buildPage(pageInitParams); expect(page, isNotNull); }); testWidgets('build', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: initState, view: instrumentView(pageView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('page-build', state.clone()); }), reducer: toDoListReducer, effect: toDoListEffect, dependencies: toDoListDependencies(track)) .buildPage(pageInitParams))); expect(find.byKey(const ValueKey('Add')), findsOneWidget); expect(find.text('Add'), findsOneWidget); expect(find.byKey(const ValueKey('mark-0')), findsOneWidget); expect(find.byKey(const ValueKey('edit-0')), findsOneWidget); expect(find.text('desc-0'), findsOneWidget); expect(find.byKey(const ValueKey('remove-0')), findsOneWidget); expect(find.text('title-0'), findsOneWidget); expect(find.byKey(const ValueKey('mark-1')), findsOneWidget); expect(find.byKey(const ValueKey('edit-1')), findsOneWidget); expect(find.text('desc-1'), findsOneWidget); expect(find.byKey(const ValueKey('remove-1')), findsOneWidget); expect(find.text('title-1'), findsOneWidget); expect(find.byKey(const ValueKey('mark-2')), findsOneWidget); expect(find.byKey(const ValueKey('edit-2')), findsOneWidget); expect(find.text('desc-2'), findsOneWidget); expect(find.byKey(const ValueKey('remove-2')), findsOneWidget); expect(find.text('title-2'), findsOneWidget); expect(find.byKey(const ValueKey('mark-3')), findsOneWidget); expect(find.byKey(const ValueKey('edit-3')), findsOneWidget); expect(find.text('desc-3'), findsOneWidget); expect(find.byKey(const ValueKey('remove-3')), findsOneWidget); expect(find.text('title-3'), findsOneWidget); expect(find.text('mark\ndone'), findsNWidgets(3)); expect(find.text('done'), findsOneWidget); expect(track.countOfTag('page-build'), 1); expect(track.countOfTag('toDo-build'), 4); }); testWidgets('build-noReducer', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: initState, view: instrumentView(pageView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('page-build', state.clone()); }), reducer: toDoListReducer, effect: toDoListEffect, dependencies: toDoListDependencies(track, noReducer: true)) .buildPage(pageInitParams))); expect(find.byKey(const ValueKey('Add')), findsOneWidget); expect(find.text('Add'), findsOneWidget); expect(find.byKey(const ValueKey('mark-0')), findsOneWidget); expect(find.byKey(const ValueKey('edit-0')), findsOneWidget); expect(find.text('desc-0'), findsOneWidget); expect(find.byKey(const ValueKey('remove-0')), findsOneWidget); expect(find.text('title-0'), findsOneWidget); expect(find.byKey(const ValueKey('mark-1')), findsOneWidget); expect(find.byKey(const ValueKey('edit-1')), findsOneWidget); expect(find.text('desc-1'), findsOneWidget); expect(find.byKey(const ValueKey('remove-1')), findsOneWidget); expect(find.text('title-1'), findsOneWidget); expect(find.byKey(const ValueKey('mark-2')), findsOneWidget); expect(find.byKey(const ValueKey('edit-2')), findsOneWidget); expect(find.text('desc-2'), findsOneWidget); expect(find.byKey(const ValueKey('remove-2')), findsOneWidget); expect(find.text('title-2'), findsOneWidget); expect(find.byKey(const ValueKey('mark-3')), findsOneWidget); expect(find.byKey(const ValueKey('edit-3')), findsOneWidget); expect(find.text('desc-3'), findsOneWidget); expect(find.byKey(const ValueKey('remove-3')), findsOneWidget); expect(find.text('title-3'), findsOneWidget); expect(find.text('mark\ndone'), findsNWidgets(3)); expect(find.text('done'), findsOneWidget); expect(track.countOfTag('page-build'), 1); expect(track.countOfTag('toDo-build'), 4); }); testWidgets('reducer', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: initState, view: instrumentView(pageView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('page-build', state.clone()); }), effect: pageEffect, dependencies: toDoListDependencies(track)) .buildPage(pageInitParams))); await tester.tap(find.byKey(const ValueKey('mark-0'))); await tester.pump(); expect(find.text('mark\ndone'), findsNWidgets(2)); expect(find.text('done'), findsNWidgets(2)); await tester.tap(find.byKey(const ValueKey('mark-1'))); await tester.pump(); expect(find.text('mark\ndone'), findsNWidgets(1)); expect(find.text('done'), findsNWidgets(3)); await tester.tap(find.byKey(const ValueKey('remove-2'))); await tester.pump(); expect(find.text('title-2'), findsNothing); expect(find.text('desc-2'), findsNothing); await tester.tap(find.byKey(const ValueKey('remove-3'))); await tester.pump(); expect(find.text('title-3'), findsNothing); expect(find.text('desc-3'), findsNothing); print(track); ToDoList mockState = ToDoList.fromMap(pageInitParams); expect( track, Track.pins([ Pin('page-build', mockState.clone()), Pin('toDo-build', mockState.list[0].clone()), Pin('toDo-build', mockState.list[1].clone()), Pin('toDo-build', mockState.list[2].clone()), Pin('toDo-build', mockState.list[3].clone()), Pin('toDo-onReduce', () { mockState.list[0] = toDoReducer(mockState.list[0], Action(ToDoAction.markDone, payload: mockState.list[0])); return mockState.list[0].clone(); }), Pin('page-build', mockState.clone()), Pin('toDo-build', mockState.list[0].clone()), Pin('toDo-onReduce', () { mockState.list[1] = toDoReducer(mockState.list[1], Action(ToDoAction.markDone, payload: mockState.list[1])); return mockState.list[1].clone(); }), Pin('page-build', mockState.clone()), Pin('toDo-build', mockState.list[1].clone()), Pin('adapter-onReduce', () { mockState = toDoListReducer(mockState, Action(ToDoListAction.remove, payload: mockState.list[2])); return mockState.clone(); }), Pin('page-build', mockState.clone()), Pin('adapter-onReduce', () { mockState = toDoListReducer(mockState, Action(ToDoListAction.remove, payload: mockState.list[2])); return mockState.clone(); }), Pin('page-build', mockState.clone()), ])); }); testWidgets('effect', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: initState, view: instrumentView(pageView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('page-build', state.clone()); }), effect: pageEffect, dependencies: toDoListDependencies(track)) .buildPage(pageInitParams))); await tester.tap(find.byKey(const ValueKey('edit-0'))); await tester.pump(); expect(find.text('desc-0-effect'), findsNWidgets(1)); await tester.tap(find.byKey(const ValueKey('edit-1'))); await tester.pump(); expect(find.text('desc-1-effect'), findsNWidgets(1)); await tester.tap(find.byKey(const ValueKey('Add'))); await tester.pump(); expect(find.text('desc-mock', skipOffstage: false), findsNWidgets(1)); ToDoList mockState = ToDoList.fromMap(pageInitParams); expect( track, Track.pins([ Pin('page-build', mockState.clone()), Pin('toDo-build', mockState.list[0].clone()), Pin('toDo-build', mockState.list[1].clone()), Pin('toDo-build', mockState.list[2].clone()), Pin('toDo-build', mockState.list[3].clone()), Pin('toDo-onEdit', mockState.list[0].clone()), Pin('toDo-onReduce', () { final Todo toDo = mockState.list[0].clone(); toDo.desc = '${toDo.desc}-effect'; mockState.list[0] = toDoReducer(toDo, Action(ToDoAction.edit, payload: toDo)); return mockState.list[0].clone(); }), Pin('page-build', mockState.clone()), Pin('toDo-build', mockState.list[0].clone()), Pin('toDo-onEdit', mockState.list[1].clone()), Pin('toDo-onReduce', () { final Todo toDo = mockState.list[1].clone(); toDo.desc = '${toDo.desc}-effect'; mockState.list[1] = toDoReducer(toDo, Action(ToDoAction.edit, payload: toDo)); return mockState.list[1].clone(); }), Pin('page-build', mockState.clone()), Pin('toDo-build', mockState.list[1].clone()), Pin('adapter-onAdd', mockState.clone()), Pin('adapter-onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.add, payload: Todo.mock())); return mockState.clone(); }), Pin('page-build', mockState.clone()), Pin('toDo-build', Todo.mock()), ])); }); testWidgets('effect-noReducer', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: initState, view: instrumentView(pageView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('page-build', state.clone()); }), effect: pageEffect, dependencies: toDoListDependencies(track, noReducer: true)) .buildPage(pageInitParams))); await tester.tap(find.byKey(const ValueKey('Add'))); await tester.pump(); expect(find.text('desc-mock', skipOffstage: false), findsNWidgets(1)); ToDoList mockState = ToDoList.fromMap(pageInitParams); expect( track, Track.pins([ Pin('page-build', mockState.clone()), Pin('toDo-build', mockState.list[0].clone()), Pin('toDo-build', mockState.list[1].clone()), Pin('toDo-build', mockState.list[2].clone()), Pin('toDo-build', mockState.list[3].clone()), Pin('adapter-onAdd', mockState.clone()), Pin('adapter-onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.add, payload: Todo.mock())); return mockState.clone(); }), Pin('page-build', mockState.clone()), Pin('toDo-build', Todo.mock()), ])); }); testWidgets('broadcast', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: initState, view: instrumentView(pageView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('page-build', state.clone()); }), effect: pageEffect, dependencies: toDoListDependencies(track)) .buildPage(pageInitParams))); track.reset(); await tester.longPress(find.byKey(const ValueKey('mark-0'))); await tester.pump(Duration(seconds: 1)); print(track); expect(track.countOfTag('toDo-onToDoBroadcast'), 4); expect(track.countOfTag('adapter-onToDoBroadcast'), 1); track.reset(); await tester.longPress(find.byKey(const ValueKey('Add'))); await tester.pump(Duration(seconds: 1)); expect(track.countOfTag('toDo-onToDoBroadcast'), 4); expect(track.countOfTag('adapter-onToDoBroadcast'), 1); await tester.tap(find.byKey(const ValueKey('remove-1'))); await tester.pump(); track.reset(); await tester.longPress(find.byKey(const ValueKey('Add'))); await tester.pump(Duration(seconds: 1)); expect(track.countOfTag('toDo-onToDoBroadcast'), 3); expect(track.countOfTag('adapter-onToDoBroadcast'), 1); await tester.tap(find.byKey(const ValueKey('remove-2'))); await tester.pump(); track.reset(); await tester.longPress(find.byKey(const ValueKey('Add'))); await tester.pump(Duration(seconds: 1)); expect(track.countOfTag('toDo-onToDoBroadcast'), 2); expect(track.countOfTag('adapter-onToDoBroadcast'), 1); await tester.tap(find.byKey(const ValueKey('Add'))); await tester.pump(); track.reset(); await tester.longPress(find.byKey(const ValueKey('Add'))); await tester.pump(Duration(seconds: 1)); expect(track.countOfTag('toDo-onToDoBroadcast'), 3); expect(track.countOfTag('adapter-onToDoBroadcast'), 1); }); }); } ================================================ FILE: test/lib/redux_adapter/redux_adapter_test.dart ================================================ import 'package:test/test.dart'; import 'adapter_test.dart' as adapter; import 'dynamic_adapter_test.dart' as dynamic_flow_dapter; import 'source_adapter_test.dart' as source_flow_dapter; import 'static_flow_adapter_test.dart' as static_flow_adapter; void main() { group('redux_adapter_test', () { adapter.main(); dynamic_flow_dapter.main(); source_flow_dapter.main(); static_flow_adapter.main(); }); } ================================================ FILE: test/lib/redux_adapter/source_adapter_test.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import 'package:flutter_test/flutter_test.dart'; import 'package:test_widgets/source_flow_adapter/action.dart'; import 'package:test_widgets/source_flow_adapter/component.dart'; import 'package:test_widgets/source_flow_adapter/source_flow_adapter.dart'; import 'package:test_widgets/source_flow_adapter/page.dart'; import 'package:test_widgets/source_flow_adapter/state.dart'; import 'package:test_widgets/test_base.dart'; import '../instrument.dart'; import '../track.dart'; class ToDoComponentInstrument extends TestComponent { ToDoComponentInstrument(final Track track) : super( view: instrumentView(toDoView, (Todo state, Dispatch dispatch, ViewService viewService) { track.append('toDo-build', state.clone()); }), reducer: instrumentReducer(toDoReducer, change: (Todo state, Action action) { track.append('toDo-onReduce', state.clone()); }), effect: instrumentEffect(toDoEffect, (Action action, Get getState) { if (action.type == ToDoAction.onEdit) { track.append('toDo-onEdit', getState().clone()); } else if (action.type == ToDoAction.broadcast) { track.append('toDo-onToDoBroadcast', getState().clone()); } else if (action.type == ToDoListAction.broadcast) { track.append('toDo-onPageBroadcast', getState().clone()); } }), shouldUpdate: shouldUpdate, key: (Todo toDo) => GlobalObjectKey(toDo.id), ); } class ToDoComponentNoReducer extends TestComponent { ToDoComponentNoReducer(final Track track) : super( view: instrumentView(toDoView, (Todo state, Dispatch dispatch, ViewService viewService) { track.append('toDo-build', state.clone()); }), effect: instrumentEffect(toDoEffect, (Action action, Get getState) { if (action.type == ToDoAction.onEdit) { track.append('toDo-onEdit', getState().clone()); } else if (action.type == ToDoAction.broadcast) { track.append('toDo-onToDoBroadcast', getState().clone()); } else if (action.type == ToDoListAction.broadcast) { track.append('toDo-onPageBroadcast', getState().clone()); } }), shouldUpdate: shouldUpdate, key: (Todo toDo) => GlobalObjectKey(toDo.id), ); } Dependencies toDoListDependencies(final Track track, {bool noReducer = false}) => Dependencies( adapter: NoneConn() + TestSourceFlowAdapter( pool: >{ 'toDo': ToDoComponentInstrument(track), 'toDoNoReducer': ToDoComponentNoReducer(track), 'item': noReducer ? ToDoComponentNoReducer(track) : ToDoComponentInstrument(track), }, reducer: instrumentReducer(toDoListReducer, change: (ToDoList state, Action action) { track.append('adapter-onReduce', state.clone()); }), effect: instrumentEffect(toDoListEffect, (Action action, Get getState) { if (action.type == ToDoListAction.onAdd) { track.append('adapter-onAdd', getState().clone()); } else if (action.type == ToDoAction.broadcast) { track.append('adapter-onToDoBroadcast', getState().clone()); } else if (action.type == ToDoListAction.broadcast) { track.append('adapter-onPageBroadcast', getState().clone()); } }))); void main() { group('source_flow_adapter', () { test('create', () { final Track track = Track(); final TestComponent component = ToDoComponentInstrument(track); expect(component, isNotNull); Widget page = TestPage( initState: initState, view: pageView, dependencies: toDoListDependencies(track)) .buildPage(pageInitParams); expect(page, isNotNull); }); testWidgets('build', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: initState, view: instrumentView(pageView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('page-build', state.clone()); }), reducer: toDoListReducer, effect: toDoListEffect, dependencies: toDoListDependencies(track)) .buildPage(pageInitParams))); expect(find.byKey(const ValueKey('Add')), findsOneWidget); expect(find.text('Add'), findsOneWidget); expect(find.byKey(const ValueKey('mark-0')), findsOneWidget); expect(find.byKey(const ValueKey('edit-0')), findsOneWidget); expect(find.text('desc-0'), findsOneWidget); expect(find.byKey(const ValueKey('remove-0')), findsOneWidget); expect(find.text('title-0'), findsOneWidget); expect(find.byKey(const ValueKey('mark-1')), findsOneWidget); expect(find.byKey(const ValueKey('edit-1')), findsOneWidget); expect(find.text('desc-1'), findsOneWidget); expect(find.byKey(const ValueKey('remove-1')), findsOneWidget); expect(find.text('title-1'), findsOneWidget); expect(find.byKey(const ValueKey('mark-2')), findsOneWidget); expect(find.byKey(const ValueKey('edit-2')), findsOneWidget); expect(find.text('desc-2'), findsOneWidget); expect(find.byKey(const ValueKey('remove-2')), findsOneWidget); expect(find.text('title-2'), findsOneWidget); expect(find.byKey(const ValueKey('mark-3')), findsOneWidget); expect(find.byKey(const ValueKey('edit-3')), findsOneWidget); expect(find.text('desc-3'), findsOneWidget); expect(find.byKey(const ValueKey('remove-3')), findsOneWidget); expect(find.text('title-3'), findsOneWidget); expect(find.text('mark\ndone'), findsNWidgets(3)); expect(find.text('done'), findsOneWidget); expect(track.countOfTag('page-build'), 1); expect(track.countOfTag('toDo-build'), 4); }); testWidgets('build-noReducer', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: initState, view: instrumentView(pageView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('page-build', state.clone()); }), reducer: toDoListReducer, effect: toDoListEffect, dependencies: toDoListDependencies(track, noReducer: true)) .buildPage(pageInitParams))); expect(find.byKey(const ValueKey('Add')), findsOneWidget); expect(find.text('Add'), findsOneWidget); expect(find.byKey(const ValueKey('mark-0')), findsOneWidget); expect(find.byKey(const ValueKey('edit-0')), findsOneWidget); expect(find.text('desc-0'), findsOneWidget); expect(find.byKey(const ValueKey('remove-0')), findsOneWidget); expect(find.text('title-0'), findsOneWidget); expect(find.byKey(const ValueKey('mark-1')), findsOneWidget); expect(find.byKey(const ValueKey('edit-1')), findsOneWidget); expect(find.text('desc-1'), findsOneWidget); expect(find.byKey(const ValueKey('remove-1')), findsOneWidget); expect(find.text('title-1'), findsOneWidget); expect(find.byKey(const ValueKey('mark-2')), findsOneWidget); expect(find.byKey(const ValueKey('edit-2')), findsOneWidget); expect(find.text('desc-2'), findsOneWidget); expect(find.byKey(const ValueKey('remove-2')), findsOneWidget); expect(find.text('title-2'), findsOneWidget); expect(find.byKey(const ValueKey('mark-3')), findsOneWidget); expect(find.byKey(const ValueKey('edit-3')), findsOneWidget); expect(find.text('desc-3'), findsOneWidget); expect(find.byKey(const ValueKey('remove-3')), findsOneWidget); expect(find.text('title-3'), findsOneWidget); expect(find.text('mark\ndone'), findsNWidgets(3)); expect(find.text('done'), findsOneWidget); expect(track.countOfTag('page-build'), 1); expect(track.countOfTag('toDo-build'), 4); }); testWidgets('reducer', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: initState, view: instrumentView(pageView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('page-build', state.clone()); }), effect: pageEffect, dependencies: toDoListDependencies(track)) .buildPage(pageInitParams))); await tester.tap(find.byKey(const ValueKey('mark-0'))); await tester.pump(); expect(find.text('mark\ndone'), findsNWidgets(2)); expect(find.text('done'), findsNWidgets(2)); await tester.tap(find.byKey(const ValueKey('mark-1'))); await tester.pump(); expect(find.text('mark\ndone'), findsNWidgets(1)); expect(find.text('done'), findsNWidgets(3)); await tester.tap(find.byKey(const ValueKey('remove-2'))); await tester.pump(); expect(find.text('title-2'), findsNothing); expect(find.text('desc-2'), findsNothing); await tester.tap(find.byKey(const ValueKey('remove-3'))); await tester.pump(); expect(find.text('title-3'), findsNothing); expect(find.text('desc-3'), findsNothing); print(track); ToDoList mockState = ToDoList.fromMap(pageInitParams); expect( track, Track.pins([ Pin('page-build', mockState.clone()), Pin('toDo-build', mockState.list[0].clone()), Pin('toDo-build', mockState.list[1].clone()), Pin('toDo-build', mockState.list[2].clone()), Pin('toDo-build', mockState.list[3].clone()), Pin('toDo-onReduce', () { mockState.list[0] = toDoReducer(mockState.list[0], Action(ToDoAction.markDone, payload: mockState.list[0])); return mockState.list[0].clone(); }), Pin('page-build', mockState.clone()), Pin('toDo-build', mockState.list[0].clone()), Pin('toDo-onReduce', () { mockState.list[1] = toDoReducer(mockState.list[1], Action(ToDoAction.markDone, payload: mockState.list[1])); return mockState.list[1].clone(); }), Pin('page-build', mockState.clone()), Pin('toDo-build', mockState.list[1].clone()), Pin('adapter-onReduce', () { mockState = toDoListReducer(mockState, Action(ToDoListAction.remove, payload: mockState.list[2])); return mockState.clone(); }), Pin('page-build', mockState.clone()), Pin('adapter-onReduce', () { mockState = toDoListReducer(mockState, Action(ToDoListAction.remove, payload: mockState.list[2])); return mockState.clone(); }), Pin('page-build', mockState.clone()), ])); }); testWidgets('effect', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: initState, view: instrumentView(pageView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('page-build', state.clone()); }), effect: pageEffect, dependencies: toDoListDependencies(track)) .buildPage(pageInitParams))); await tester.tap(find.byKey(const ValueKey('edit-0'))); await tester.pump(); expect(find.text('desc-0-effect'), findsNWidgets(1)); await tester.tap(find.byKey(const ValueKey('edit-1'))); await tester.pump(); expect(find.text('desc-1-effect'), findsNWidgets(1)); await tester.tap(find.byKey(const ValueKey('Add'))); await tester.pump(); expect(find.text('desc-mock', skipOffstage: false), findsNWidgets(1)); ToDoList mockState = ToDoList.fromMap(pageInitParams); expect( track, Track.pins([ Pin('page-build', mockState.clone()), Pin('toDo-build', mockState.list[0].clone()), Pin('toDo-build', mockState.list[1].clone()), Pin('toDo-build', mockState.list[2].clone()), Pin('toDo-build', mockState.list[3].clone()), Pin('toDo-onEdit', mockState.list[0].clone()), Pin('toDo-onReduce', () { final Todo toDo = mockState.list[0].clone(); toDo.desc = '${toDo.desc}-effect'; mockState.list[0] = toDoReducer(toDo, Action(ToDoAction.edit, payload: toDo)); return mockState.list[0].clone(); }), Pin('page-build', mockState.clone()), Pin('toDo-build', mockState.list[0].clone()), Pin('toDo-onEdit', mockState.list[1].clone()), Pin('toDo-onReduce', () { final Todo toDo = mockState.list[1].clone(); toDo.desc = '${toDo.desc}-effect'; mockState.list[1] = toDoReducer(toDo, Action(ToDoAction.edit, payload: toDo)); return mockState.list[1].clone(); }), Pin('page-build', mockState.clone()), Pin('toDo-build', mockState.list[1].clone()), Pin('adapter-onAdd', mockState.clone()), Pin('adapter-onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.add, payload: Todo.mock())); return mockState.clone(); }), Pin('page-build', mockState.clone()), Pin('toDo-build', Todo.mock()), ])); }); testWidgets('effect-noReducer', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: initState, view: instrumentView(pageView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('page-build', state.clone()); }), effect: pageEffect, dependencies: toDoListDependencies(track, noReducer: true)) .buildPage(pageInitParams))); await tester.tap(find.byKey(const ValueKey('Add'))); await tester.pump(); expect(find.text('desc-mock', skipOffstage: false), findsNWidgets(1)); ToDoList mockState = ToDoList.fromMap(pageInitParams); expect( track, Track.pins([ Pin('page-build', mockState.clone()), Pin('toDo-build', mockState.list[0].clone()), Pin('toDo-build', mockState.list[1].clone()), Pin('toDo-build', mockState.list[2].clone()), Pin('toDo-build', mockState.list[3].clone()), Pin('adapter-onAdd', mockState.clone()), Pin('adapter-onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.add, payload: Todo.mock())); return mockState.clone(); }), Pin('page-build', mockState.clone()), Pin('toDo-build', Todo.mock()), ])); }); testWidgets('broadcast', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: initState, view: instrumentView(pageView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('page-build', state.clone()); }), effect: pageEffect, dependencies: toDoListDependencies(track)) .buildPage(pageInitParams))); track.reset(); await tester.longPress(find.byKey(const ValueKey('mark-0'))); await tester.pump(Duration(seconds: 1)); print(track); expect(track.countOfTag('toDo-onToDoBroadcast'), 4); expect(track.countOfTag('adapter-onToDoBroadcast'), 1); track.reset(); await tester.longPress(find.byKey(const ValueKey('Add'))); await tester.pump(Duration(seconds: 1)); expect(track.countOfTag('toDo-onToDoBroadcast'), 4); expect(track.countOfTag('adapter-onToDoBroadcast'), 1); await tester.tap(find.byKey(const ValueKey('remove-1'))); await tester.pump(); track.reset(); await tester.longPress(find.byKey(const ValueKey('Add'))); await tester.pump(Duration(seconds: 1)); expect(track.countOfTag('toDo-onToDoBroadcast'), 3); expect(track.countOfTag('adapter-onToDoBroadcast'), 1); await tester.tap(find.byKey(const ValueKey('remove-2'))); await tester.pump(); track.reset(); await tester.longPress(find.byKey(const ValueKey('Add'))); await tester.pump(Duration(seconds: 1)); expect(track.countOfTag('toDo-onToDoBroadcast'), 2); expect(track.countOfTag('adapter-onToDoBroadcast'), 1); await tester.tap(find.byKey(const ValueKey('Add'))); await tester.pump(); track.reset(); await tester.longPress(find.byKey(const ValueKey('Add'))); await tester.pump(Duration(seconds: 1)); expect(track.countOfTag('toDo-onToDoBroadcast'), 3); expect(track.countOfTag('adapter-onToDoBroadcast'), 1); }); }); } ================================================ FILE: test/lib/redux_adapter/static_flow_adapter_test.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import 'package:flutter_test/flutter_test.dart'; import 'package:test_widgets/static_flow_adapter/action.dart'; import 'package:test_widgets/static_flow_adapter/component.dart'; import 'package:test_widgets/static_flow_adapter/page.dart'; import 'package:test_widgets/static_flow_adapter/state.dart'; import 'package:test_widgets/static_flow_adapter/static_flow_adapter.dart'; import 'package:test_widgets/test_base.dart'; import '../instrument.dart'; import '../track.dart'; class ToDoComponentInstrument extends TestComponent { ToDoComponentInstrument(final Track track, int index, {bool hasReducer = true}) : super( view: instrumentView(toDoView, (Todo state, Dispatch dispatch, ViewService viewService) { track.append('toDo$index-build', state.clone()); }), reducer: hasReducer ? instrumentReducer(toDoReducer, change: (Todo state, Action action) { track.append('toDo$index-onReduce', state.clone()); }) : null, effect: instrumentEffect(toDoEffect, (Action action, Get getState) { if (action.type == ToDoAction.onEdit) { track.append('toDo$index-onEdit', getState().clone()); } else if (action.type == ToDoAction.broadcast) { track.append('toDo$index-onToDoBroadcast', getState().clone()); } }), shouldUpdate: shouldUpdate); } class ToDoAdapterInstrument extends TestAdapter { ToDoAdapterInstrument(final Track track, int index, {bool hasReducer = true}) : super( adapter: asAdapter(instrumentView(toDoView, (Todo state, Dispatch dispatch, ViewService viewService) { track.append('toDo$index-build', state.clone()); })), reducer: hasReducer ? instrumentReducer(toDoReducer, change: (Todo state, Action action) { track.append('toDo$index-onReduce', state.clone()); }) : null, effect: instrumentEffect(toDoEffect, (Action action, Get getState) { if (action.type == ToDoAction.onEdit) { track.append('toDo$index-onEdit', getState().clone()); } else if (action.type == ToDoAction.broadcast) { track.append('toDo$index-onToDoBroadcast', getState().clone()); } }), ); } class Component0 extends ToDoComponentInstrument { Component0(final Track track) : super(track, 0); } class Adapter1 extends ToDoAdapterInstrument { Adapter1(final Track track) : super(track, 1); } class Component2 extends ToDoComponentInstrument { Component2(final Track track) : super(track, 2, hasReducer: false); } class Adapter3 extends ToDoAdapterInstrument { Adapter3(final Track track) : super(track, 3, hasReducer: false); } Dependencies toDoListDependencies(final Track track) => Dependencies( adapter: NoneConn() + TestStaticFlowAdapter( slots: [ ConnOp( get: (ToDoList toDoList) => toDoList.list[0], set: (ToDoList toDoList, Todo toDo) => toDoList.list[0] = toDo) + Component0(track), ConnOp( get: (ToDoList toDoList) => toDoList.list[1], set: (ToDoList toDoList, Todo toDo) => toDoList.list[1] = toDo) + Adapter1(track), ConnOp( get: (ToDoList toDoList) => toDoList.list[2], set: (ToDoList toDoList, Todo toDo) => toDoList.list[2] = toDo) + Component2(track), ConnOp( get: (ToDoList toDoList) => toDoList.list[3], set: (ToDoList toDoList, Todo toDo) => toDoList.list[3] = toDo) + Adapter3(track) ], reducer: instrumentReducer(toDoListReducer, change: (ToDoList state, Action action) { track.append('adapter-onReduce', state.clone()); }), effect: instrumentEffect(toDoListEffect, (Action action, Get getState) { if (action.type == ToDoListAction.onAdd) { track.append('adapter-onAdd', getState().clone()); } else if (action.type == ToDoAction.broadcast) { track.append('adapter-onToDoBroadcast', getState().clone()); } }))); void main() { group('static_flow_adapter', () { test('create', () { final Track track = Track(); final TestComponent component = ToDoComponentInstrument(track, 0); expect(component, isNotNull); Widget page = TestPage( initState: initState, view: pageView, dependencies: toDoListDependencies(track)) .buildPage(pageInitParams); expect(page, isNotNull); }); testWidgets('build', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: initState, view: instrumentView(pageView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('page-build', state.clone()); }), dependencies: toDoListDependencies(track)) .buildPage(pageInitParams))); expect(find.byKey(const ValueKey('mark-0')), findsOneWidget); expect(find.byKey(const ValueKey('edit-0')), findsOneWidget); expect(find.text('desc-0'), findsOneWidget); expect(find.byKey(const ValueKey('remove-0')), findsOneWidget); expect(find.text('title-0'), findsOneWidget); expect(find.byKey(const ValueKey('mark-1')), findsOneWidget); expect(find.byKey(const ValueKey('edit-1')), findsOneWidget); expect(find.text('desc-1'), findsOneWidget); expect(find.byKey(const ValueKey('remove-1')), findsOneWidget); expect(find.text('title-1'), findsOneWidget); expect(find.byKey(const ValueKey('mark-2')), findsOneWidget); expect(find.byKey(const ValueKey('edit-2')), findsOneWidget); expect(find.text('desc-2'), findsOneWidget); expect(find.byKey(const ValueKey('remove-2')), findsOneWidget); expect(find.text('title-2'), findsOneWidget); expect(find.byKey(const ValueKey('mark-3')), findsOneWidget); expect(find.byKey(const ValueKey('edit-3')), findsOneWidget); expect(find.text('desc-3'), findsOneWidget); expect(find.byKey(const ValueKey('remove-3')), findsOneWidget); expect(find.text('title-3'), findsOneWidget); expect(find.text('mark\ndone'), findsNWidgets(3)); expect(find.text('done'), findsOneWidget); expect(track.countOfTag('page-build'), 1); expect(track.countOfTag('toDo0-build'), 1); expect(track.countOfTag('toDo1-build'), 1); expect(track.countOfTag('toDo2-build'), 1); expect(track.countOfTag('toDo3-build'), 1); track.reset(); }); testWidgets('reducer', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: initState, view: instrumentView(pageView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('page-build', state.clone()); }), dependencies: toDoListDependencies(track)) .buildPage(pageInitParams))); await tester.tap(find.byKey(const ValueKey('mark-0'))); await tester.pump(); expect(find.text('mark\ndone'), findsNWidgets(2)); expect(find.text('done'), findsNWidgets(2)); await tester.tap(find.byKey(const ValueKey('mark-1'))); await tester.pump(); expect(find.text('mark\ndone'), findsNWidgets(1)); expect(find.text('done'), findsNWidgets(3)); await tester.tap(find.byKey(const ValueKey('remove-2'))); await tester.pump(); expect(find.text('desc-2'), findsNothing); expect(find.byKey(const ValueKey('remove-2')), findsNothing); expect(find.text('title-2'), findsNothing); await tester.tap(find.byKey(const ValueKey('remove-3'))); await tester.pump(); expect(find.text('desc-3'), findsNothing); expect(find.byKey(const ValueKey('remove-3')), findsNothing); expect(find.text('title-3'), findsNothing); // ToDoList mockState = ToDoList.fromMap(pageInitParams); // expect( // track, // Track.pins([ // Pin('page-build', mockState.clone()), // Pin('toDo0-build', mockState.list[0].clone()), // Pin('toDo1-build', mockState.list[1].clone()), // Pin('toDo2-build', mockState.list[2].clone()), // Pin('toDo3-build', mockState.list[3].clone()), // Pin('toDo0-onReduce', () { // mockState.list[0] = toDoReducer(mockState.list[0], // Action(ToDoAction.markDone, payload: mockState.list[0])) // .clone(); // return mockState.list[0].clone(); // }), // Pin('page-build', mockState.clone()), // Pin('toDo0-build', mockState.list[0].clone()), // Pin('toDo1-build', mockState.list[1].clone()), // Pin('toDo3-build', mockState.list[3].clone()), // Pin('toDo1-onReduce', () { // mockState.list[1] = toDoReducer(mockState.list[1], // Action(ToDoAction.markDone, payload: mockState.list[1])) // .clone(); // return mockState.list[1].clone(); // }), // Pin('page-build', mockState.clone()), // Pin('toDo1-build', mockState.list[1].clone()), // Pin('toDo3-build', mockState.list[3].clone()), // Pin('adapter-onReduce', () { // mockState = toDoListReducer(mockState, // Action(ToDoListAction.remove, payload: mockState.list[2])); // return mockState.clone(); // }), // Pin('page-build', mockState.clone()), // Pin('toDo2-build', mockState.list[2].clone()), // Pin('adapter-onReduce', () { // mockState = toDoListReducer(mockState, // Action(ToDoListAction.remove, payload: mockState.list[3])); // return mockState.clone(); // }), // Pin('page-build', mockState.clone()), // Pin('toDo3-build', mockState.list[3].clone()), // ])); }); testWidgets('effect', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: initState, view: instrumentView(pageView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('page-build', state.clone()); }), dependencies: toDoListDependencies(track)) .buildPage(pageInitParams))); await tester.tap(find.byKey(const ValueKey('edit-0'))); await tester.pump(); expect(find.text('desc-0-effect'), findsNWidgets(1)); await tester.tap(find.byKey(const ValueKey('edit-1'))); await tester.pump(); expect(find.text('desc-1-effect'), findsNWidgets(1)); ToDoList mockState = ToDoList.fromMap(pageInitParams); expect( track, Track.pins([ Pin('page-build', mockState.clone()), Pin('toDo0-build', mockState.list[0].clone()), Pin('toDo1-build', mockState.list[1].clone()), Pin('toDo2-build', mockState.list[2].clone()), Pin('toDo3-build', mockState.list[3].clone()), Pin('toDo0-onEdit', mockState.list[0].clone()), Pin('toDo0-onReduce', () { String desc = '${mockState.list[0].desc}-effect'; mockState.list[0] = mockState.list[0].clone()..desc = desc; return mockState.list[0].clone(); }), Pin('page-build', mockState.clone()), Pin('toDo0-build', mockState.list[0].clone()), // Pin('toDo1-build', mockState.list[1].clone()), // Pin('toDo3-build', mockState.list[3].clone()), Pin('toDo1-onEdit', mockState.list[1].clone()), Pin('toDo1-onReduce', () { String desc = '${mockState.list[1].desc}-effect'; mockState.list[1] = mockState.list[1].clone()..desc = desc; return mockState.list[1].clone(); }), Pin('page-build', mockState.clone()), Pin('toDo1-build', mockState.list[1].clone()), // Pin('toDo3-build', mockState.list[3].clone()), ])); }); testWidgets('broadcast', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: initState, view: instrumentView(pageView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('page-build', state.clone()); }), dependencies: toDoListDependencies(track)) .buildPage(pageInitParams))); track.reset(); await tester.longPress(find.byKey(const ValueKey('mark-0'))); await tester.pump(Duration(seconds: 1)); print(track); expect(track.countOfTag('toDo0-onToDoBroadcast'), 1); expect(track.countOfTag('toDo1-onToDoBroadcast'), 1); expect(track.countOfTag('toDo2-onToDoBroadcast'), 1); expect(track.countOfTag('toDo3-onToDoBroadcast'), 1); expect(track.countOfTag('adapter-onToDoBroadcast'), 1); track.reset(); await tester.longPress(find.byKey(const ValueKey('mark-1'))); await tester.pump(Duration(seconds: 1)); expect(track.countOfTag('toDo0-onToDoBroadcast'), 1); expect(track.countOfTag('toDo1-onToDoBroadcast'), 1); expect(track.countOfTag('toDo2-onToDoBroadcast'), 1); expect(track.countOfTag('toDo3-onToDoBroadcast'), 1); expect(track.countOfTag('adapter-onToDoBroadcast'), 1); }); }); } ================================================ FILE: test/lib/redux_aop/memoize_test.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter_test/flutter_test.dart'; class _Pair { int value; String label; List children; _Pair(this.value, this.label); } _Pair mkPair(int value, String label) => _Pair(value, label); void main() { group('memoize_test', () { test('memoize_withTwo_test', () { final _Pair Function(int, String) memoize2 = AOP([memoize()]).withTwo(mkPair); final _Pair p0 = memoize2(8, 'hello'); expect(p0.value == 8, isTrue); expect(p0.label == 'hello', isTrue); final _Pair p1 = memoize2(8, 'hello'); expect(p0 == p1, isTrue); final _Pair p2 = memoize2(9, 'hello'); expect(p2.value == 9, isTrue); expect(p2.label == 'hello', isTrue); expect(p0 == p2, isFalse); }); }); } ================================================ FILE: test/lib/redux_aop/redux_aop_test.dart ================================================ import 'package:test/test.dart'; import 'memoize_test.dart' as memoize; void main() { group('redux_aop_test', () { memoize.main(); }); } ================================================ FILE: test/lib/redux_component/auto_dispose_test.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:test/test.dart'; void main() { group('auto_dispose', () { test('create', () { final AutoDispose autoDispose = AutoDispose(); expect(autoDispose, isNotNull); expect(autoDispose.isDisposed, isFalse); }); test('dispose', () { final AutoDispose autoDispose = AutoDispose(); expect(autoDispose, isNotNull); expect(autoDispose.isDisposed, isFalse); autoDispose.dispose(); expect(autoDispose.isDisposed, isTrue); }); test('follow', () { final AutoDispose parent = AutoDispose(); expect(parent.isDisposed, isFalse); final AutoDispose follow0 = AutoDispose(); follow0.setParent(parent); expect(follow0.isDisposed, isFalse); follow0.dispose(); expect(follow0.isDisposed, isTrue); expect(parent.isDisposed, isFalse); final AutoDispose follow1 = AutoDispose(); follow1.setParent(parent); expect(follow1.isDisposed, isFalse); parent.dispose(); expect(parent.isDisposed, isTrue); expect(follow1.isDisposed, isTrue); final AutoDispose follow2 = AutoDispose(); follow2.setParent(parent); expect(follow2.isDisposed, isTrue); }); test('refollow', () { final AutoDispose parent0 = AutoDispose(); final AutoDispose parent1 = AutoDispose(); expect(parent0.isDisposed, isFalse); expect(parent1.isDisposed, isFalse); final AutoDispose follow0 = AutoDispose(); follow0.setParent(parent0); expect(follow0.isDisposed, isFalse); follow0.setParent(parent1); expect(follow0.isDisposed, isFalse); parent0.dispose(); expect(parent0.isDisposed, isTrue); expect(follow0.isDisposed, isFalse); parent1.dispose(); expect(parent1.isDisposed, isTrue); expect(follow0.isDisposed, isTrue); }); test('follower', () { final AutoDispose parent = AutoDispose(); expect(parent.isDisposed, isFalse); final AutoDispose follow0 = parent.registerOnDisposed(null); expect(follow0.isDisposed, isFalse); follow0.dispose(); expect(follow0.isDisposed, isTrue); expect(parent.isDisposed, isFalse); final AutoDispose follow1 = parent.registerOnDisposed(null); expect(follow1.isDisposed, isFalse); parent.dispose(); expect(parent.isDisposed, isTrue); expect(follow1.isDisposed, isTrue); final AutoDispose follow2 = parent.registerOnDisposed(null); expect(follow2.isDisposed, isTrue); }); test('onDisposed', () { int pCount = 0; final AutoDispose parent = AutoDispose() ..onDisposed(() { pCount++; }); expect(parent.isDisposed, isFalse); expect(pCount, equals(0)); int fCount = 0; final AutoDispose follow = parent.registerOnDisposed(() { fCount++; }); expect(fCount, equals(0)); follow.dispose(); expect(fCount, equals(1)); expect(pCount, equals(0)); parent.dispose(); expect(fCount, equals(1)); expect(pCount, equals(1)); follow.dispose(); expect(fCount, equals(1)); expect(pCount, equals(1)); parent.dispose(); expect(fCount, equals(1)); expect(pCount, equals(1)); }); }); } ================================================ FILE: test/lib/redux_component/component_test.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import 'package:flutter_test/flutter_test.dart'; import 'package:test_widgets/component/action.dart'; import 'package:test_widgets/component/component.dart'; import 'package:test_widgets/component/page.dart'; import 'package:test_widgets/component/state.dart'; import 'package:test_widgets/test_base.dart'; import '../instrument.dart'; import '../track.dart'; class ToDoComponentInstrument extends TestComponent { ToDoComponentInstrument(final Track track, int index, {bool hasReducer = true}) : super( view: instrumentView(toDoView, (Todo state, Dispatch dispatch, ViewService viewService) { track.append('toDo$index-build', state.clone()); }), reducer: hasReducer ? instrumentReducer(toDoReducer, change: (Todo state, Action action) { track.append('toDo$index-onReduce', state.clone()); }) : null, effect: instrumentEffect(toDoEffect, (Action action, Get getState) { if (action.type == ToDoAction.onEdit) { track.append('toDo$index-onEdit', getState().clone()); } else if (action.type == ToDoAction.broadcast) { track.append('toDo$index-onToDoBroadcast', getState().clone()); } else if (action.type == ToDoListAction.broadcast) { track.append('toDo$index-onPageBroadcast', getState().clone()); } }), shouldUpdate: shouldUpdate); } class Component0 extends ToDoComponentInstrument { Component0(final Track track) : super(track, 0); } class Component1 extends ToDoComponentInstrument { Component1(final Track track) : super(track, 1); } class Component2 extends ToDoComponentInstrument { Component2(final Track track) : super(track, 2); } class Component3 extends ToDoComponentInstrument { Component3(final Track track) : super(track, 3, hasReducer: false); } Dependencies toDoListDependencies(final Track track) => Dependencies(slots: { 'toDo0': ConnOp( get: (ToDoList toDoList) => toDoList.list[0], set: (ToDoList toDoList, Todo toDo) => toDoList.list[0] = toDo) + Component0(track), 'toDo1': ConnOp( get: (ToDoList toDoList) => toDoList.list[1], set: (ToDoList toDoList, Todo toDo) => toDoList.list[1] = toDo) + Component1(track), 'toDo2': ConnOp( get: (ToDoList toDoList) => toDoList.list[2], set: (ToDoList toDoList, Todo toDo) => toDoList.list[2] = toDo) + Component2(track), 'toDo3': ConnOp( get: (ToDoList toDoList) => toDoList.list[3], set: (ToDoList toDoList, Todo toDo) => toDoList.list[3] = toDo) + Component3(track), }); Widget pageView( ToDoList state, Dispatch dispatch, ViewService viewService, ) { return Column( children: [ Expanded( child: ListView.builder( itemBuilder: (context, index) { if (index == 0) { return viewService.buildComponent('toDo0'); } else if (index == 1) { return viewService.buildComponent('toDo1'); } else if (index == 2) { return viewService.buildComponent('toDo2'); } else if (index == 3) { return viewService.buildComponent('toDo3'); } else { final Todo toDo = state.list[index]; return Container( padding: const EdgeInsets.all(8.0), margin: const EdgeInsets.all(8.0), color: Colors.grey, child: Text(toDo.desc), alignment: AlignmentDirectional.center, ); } }, itemCount: state.list.length, )), Row( children: [ Expanded( child: GestureDetector( child: Container( key: const ValueKey('Add'), height: 68.0, color: Colors.green, alignment: AlignmentDirectional.center, child: const Text('Add'), ), onTap: () { print('dispatch Add'); dispatch(const Action(ToDoListAction.onAdd)); }, onLongPress: () { print('dispatch broadcast'); dispatch(const Action(ToDoListAction.onBroadcast)); }, )), ], ) ], ); } void main() { group('component', () { test('create', () { final TestComponent component = TestComponent( view: toDoView, wrapper: (child) => ComponentWrapper(child)); expect(component, isNotNull); /// TODO final Widget componentWidget = component.buildComponent( createBatchStore(Todo.mock(), null), () => Todo.mock(), enhancer: EnhancerDefault(), bus: DispatchBusDefault(), ); expect(componentWidget, isNotNull); expect( const TypeMatcher().check(componentWidget), isTrue); }); testWidgets('build', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: initState, view: instrumentView(pageView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('page-build', state.clone()); }), reducer: toDoListReducer, effect: toDoListEffect, dependencies: toDoListDependencies(track)) .buildPage(pageInitParams))); expect(find.byKey(const ValueKey('Add')), findsOneWidget); expect(find.text('Add'), findsOneWidget); expect(find.byKey(const ValueKey('mark-0')), findsOneWidget); expect(find.byKey(const ValueKey('edit-0')), findsOneWidget); expect(find.text('desc-0'), findsOneWidget); expect(find.byKey(const ValueKey('remove-0')), findsOneWidget); expect(find.text('title-0'), findsOneWidget); expect(find.byKey(const ValueKey('mark-1')), findsOneWidget); expect(find.byKey(const ValueKey('edit-1')), findsOneWidget); expect(find.text('desc-1'), findsOneWidget); expect(find.byKey(const ValueKey('remove-1')), findsOneWidget); expect(find.text('title-1'), findsOneWidget); expect(find.byKey(const ValueKey('mark-2')), findsOneWidget); expect(find.byKey(const ValueKey('edit-2')), findsOneWidget); expect(find.text('desc-2'), findsOneWidget); expect(find.byKey(const ValueKey('remove-2')), findsOneWidget); expect(find.text('title-2'), findsOneWidget); expect(find.byKey(const ValueKey('mark-3')), findsOneWidget); expect(find.byKey(const ValueKey('edit-3')), findsOneWidget); expect(find.text('desc-3'), findsOneWidget); expect(find.byKey(const ValueKey('remove-3')), findsOneWidget); expect(find.text('title-3'), findsOneWidget); expect(find.text('mark\ndone'), findsNWidgets(3)); expect(find.text('done'), findsOneWidget); expect(track.countOfTag('page-build'), 1); expect(track.countOfTag('toDo0-build'), 1); expect(track.countOfTag('toDo1-build'), 1); expect(track.countOfTag('toDo2-build'), 1); expect(track.countOfTag('toDo3-build'), 1); track.reset(); }); testWidgets('reducer', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: initState, view: instrumentView(pageView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('page-build', state.clone()); }), reducer: instrumentReducer(toDoListReducer, change: (ToDoList state, Action action) { track.append('page-onReduce', state.clone()); }), effect: instrumentEffect(toDoListEffect, (Action action, Get getState) { if (action.type == ToDoListAction.onAdd) { track.append('page-onAdd', getState().clone()); } }), dependencies: toDoListDependencies(track)) .buildPage(pageInitParams))); await tester.tap(find.byKey(const ValueKey('mark-0'))); await tester.pump(); expect(find.text('mark\ndone'), findsNWidgets(2)); expect(find.text('done'), findsNWidgets(2)); await tester.tap(find.byKey(const ValueKey('mark-1'))); await tester.pump(); expect(find.text('mark\ndone'), findsNWidgets(1)); expect(find.text('done'), findsNWidgets(3)); await tester.tap(find.byKey(const ValueKey('remove-2'))); await tester.pump(); expect(find.text('removed'), findsNWidgets(1)); await tester.tap(find.byKey(const ValueKey('remove-3'))); await tester.pump(); expect(find.text('removed'), findsNWidgets(2)); ToDoList mockState = ToDoList.fromMap(pageInitParams); expect( track, Track.pins([ Pin('page-build', mockState.clone()), Pin('toDo0-build', mockState.list[0].clone()), Pin('toDo1-build', mockState.list[1].clone()), Pin('toDo2-build', mockState.list[2].clone()), Pin('toDo3-build', mockState.list[3].clone()), Pin('toDo0-onReduce', () { mockState.list[0] = mockState.list[0].clone()..isDone = true; return mockState.list[0].clone(); }), Pin('page-build', mockState.clone()), Pin('toDo0-build', mockState.list[0].clone()), Pin('toDo1-onReduce', () { mockState.list[1] = mockState.list[1].clone()..isDone = true; return mockState.list[1].clone(); }), Pin('page-build', mockState.clone()), Pin('toDo1-build', mockState.list[1].clone()), Pin('page-onReduce', () { mockState.list[2] = mockState.list[2].clone()..desc = 'removed'; return mockState.clone(); }), Pin('page-build', mockState.clone()), Pin('toDo2-build', mockState.list[2].clone()), Pin('page-onReduce', () { mockState.list[3] = mockState.list[3].clone()..desc = 'removed'; return mockState.clone(); }), Pin('page-build', mockState.clone()), Pin('toDo3-build', mockState.list[3].clone()), ])); }); testWidgets('effect', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: initState, view: instrumentView(pageView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('page-build', state.clone()); }), reducer: instrumentReducer(toDoListReducer, change: (ToDoList state, Action action) { track.append('page-onReduce', state.clone()); }), effect: instrumentEffect(toDoListEffect, (Action action, Get getState) { if (action.type == ToDoListAction.onAdd) { track.append('page-onAdd', getState().clone()); } }), dependencies: toDoListDependencies(track)) .buildPage(pageInitParams))); await tester.tap(find.byKey(const ValueKey('edit-0'))); await tester.pump(); expect(find.text('desc-0-effect'), findsNWidgets(1)); await tester.tap(find.byKey(const ValueKey('edit-1'))); await tester.pump(); expect(find.text('desc-1-effect'), findsNWidgets(1)); await tester.tap(find.byKey(const ValueKey('Add'))); await tester.pump(); expect(find.text('desc-mock', skipOffstage: false), findsNWidgets(1)); await tester.tap(find.byKey(const ValueKey('Add'))); await tester.pump(); expect(find.text('desc-mock', skipOffstage: false), findsNWidgets(2)); ToDoList mockState = ToDoList.fromMap(pageInitParams); expect( track, Track.pins([ Pin('page-build', mockState.clone()), Pin('toDo0-build', mockState.list[0].clone()), Pin('toDo1-build', mockState.list[1].clone()), Pin('toDo2-build', mockState.list[2].clone()), Pin('toDo3-build', mockState.list[3].clone()), Pin('toDo0-onEdit', mockState.list[0].clone()), Pin('toDo0-onReduce', () { String desc = '${mockState.list[0].desc}-effect'; mockState.list[0] = mockState.list[0].clone()..desc = desc; return mockState.list[0].clone(); }), Pin('page-build', mockState.clone()), Pin('toDo0-build', mockState.list[0].clone()), Pin('toDo1-onEdit', mockState.list[1].clone()), Pin('toDo1-onReduce', () { String desc = '${mockState.list[1].desc}-effect'; mockState.list[1] = mockState.list[1].clone()..desc = desc; return mockState.list[1].clone(); }), Pin('page-build', mockState.clone()), Pin('toDo1-build', mockState.list[1].clone()), Pin('page-onAdd', mockState.clone()), Pin('page-onReduce', () { mockState.list.add(Todo.mock()); return mockState.clone(); }), Pin('page-build', mockState.clone()), Pin('page-onAdd', mockState.clone()), Pin('page-onReduce', () { mockState.list.add(Todo.mock()); return mockState.clone(); }), Pin('page-build', mockState.clone()), ])); }); testWidgets('broadcast', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: initState, view: instrumentView(pageView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('page-build', state.clone()); }), reducer: instrumentReducer(toDoListReducer, change: (ToDoList state, Action action) { track.append('page-onReduce', state.clone()); }), effect: instrumentEffect(toDoListEffect, (Action action, Get getState) { if (action.type == ToDoListAction.onAdd) { track.append('page-onAdd', getState().clone()); } else if (action.type == ToDoAction.broadcast) { track.append('page-onToDoBroadcast', getState().clone()); } else if (action.type == ToDoListAction.broadcast) { track.append('page-onPageBroadcast', getState().clone()); } }), dependencies: toDoListDependencies(track)) .buildPage(pageInitParams))); track.reset(); await tester.longPress(find.byKey(const ValueKey('mark-0'))); await tester.pump(Duration(seconds: 1)); print(track); expect(track.countOfTag('toDo0-onToDoBroadcast'), 1); expect(track.countOfTag('toDo1-onToDoBroadcast'), 1); expect(track.countOfTag('toDo2-onToDoBroadcast'), 1); expect(track.countOfTag('toDo3-onToDoBroadcast'), 1); expect(track.countOfTag('page-onToDoBroadcast'), 1); track.reset(); await tester.longPress(find.byKey(const ValueKey('mark-1'))); await tester.pump(Duration(seconds: 1)); expect(track.countOfTag('toDo0-onToDoBroadcast'), 1); expect(track.countOfTag('toDo1-onToDoBroadcast'), 1); expect(track.countOfTag('toDo2-onToDoBroadcast'), 1); expect(track.countOfTag('toDo3-onToDoBroadcast'), 1); expect(track.countOfTag('page-onToDoBroadcast'), 1); track.reset(); await tester.longPress(find.byKey(const ValueKey('Add'))); await tester.pump(Duration(seconds: 1)); expect(track.countOfTag('toDo0-onPageBroadcast'), 1); expect(track.countOfTag('toDo1-onPageBroadcast'), 1); expect(track.countOfTag('toDo2-onPageBroadcast'), 1); expect(track.countOfTag('toDo3-onPageBroadcast'), 1); expect(track.countOfTag('page-onPageBroadcast'), 1); track.reset(); await tester.longPress(find.byKey(const ValueKey('Add'))); await tester.pump(Duration(seconds: 1)); expect(track.countOfTag('toDo0-onPageBroadcast'), 1); expect(track.countOfTag('toDo1-onPageBroadcast'), 1); expect(track.countOfTag('toDo2-onPageBroadcast'), 1); expect(track.countOfTag('toDo3-onPageBroadcast'), 1); expect(track.countOfTag('page-onPageBroadcast'), 1); }); }); } ================================================ FILE: test/lib/redux_component/lifecycle_test.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import 'package:flutter_test/flutter_test.dart'; import 'package:test_widgets/component/action.dart'; import 'package:test_widgets/component/component.dart'; import 'package:test_widgets/component/page.dart'; import 'package:test_widgets/component/state.dart'; import 'package:test_widgets/test_base.dart'; import '../instrument.dart'; import '../track.dart'; class ToDoComponentInstrument extends TestComponent { ToDoComponentInstrument(final Track track) : super( view: toDoView, reducer: toDoReducer, effect: instrumentEffect(toDoEffect, (Action action, Get getState) { if (action.type == ToDoAction.onEdit) { track.append('toDo-onEdit'); print('toDo-onEdit'); } else if (action.type == Lifecycle.initState) { track.append('toDo-initState'); print('toDo-initState'); } else if (action.type == Lifecycle.build) { track.append('toDo-build'); print('toDo-build'); } else if (action.type == Lifecycle.deactivate) { track.append('toDo-deactivate'); print('toDo-deactivate'); } else if (action.type == Lifecycle.didChangeDependencies) { track.append('toDo-didChangeDependencies'); print('toDo-didChangeDependencies'); } else if (action.type == Lifecycle.didUpdateWidget) { track.append('toDo-didUpdateWidget'); print('toDo-didUpdateWidget'); } else if (action.type == Lifecycle.dispose) { track.append('toDo-dispose'); print('toDo-dispose'); } }), shouldUpdate: shouldUpdate); } Dependencies toDoListDependencies(final Track track) => Dependencies(slots: { 'toDo': ConnOp( get: (ToDoList toDoList) => toDoList.list.isNotEmpty ? toDoList.list[0] : Todo.mock(), set: (ToDoList toDoList, Todo toDo) => toDoList.list.isNotEmpty ? toDoList.list[0] = toDo : toDoList) + ToDoComponentInstrument(track) }); Widget pageView( ToDoList state, Dispatch dispatch, ViewService viewService, ) { return Column( children: [ Expanded( child: ListView.builder( itemBuilder: (context, index) => viewService.buildComponent('toDo'), itemCount: state.list.length, )), Row( children: [ Expanded( child: GestureDetector( child: Container( key: const ValueKey('Add'), height: 68.0, color: Colors.green, alignment: AlignmentDirectional.center, child: const Text('Add'), ), onTap: () { print('dispatch Add'); dispatch(const Action(ToDoListAction.onAdd)); }, onLongPress: () { print('dispatch broadcast'); dispatch(const Action(ToDoListAction.onBroadcast)); }, )), ], ) ], ); } void main() { group('component', () { test('create', () { final TestComponent component = TestComponent( view: toDoView, wrapper: (child) => ComponentWrapper(child)); expect(component, isNotNull); /// TODO final Widget componentWidget = component.buildComponent( createBatchStore(Todo.mock(), null), () => Todo.mock(), bus: DispatchBusDefault(), enhancer: EnhancerDefault(), ); expect(componentWidget, isNotNull); expect( const TypeMatcher().check(componentWidget), isTrue); }); testWidgets('cycleLife', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: (Map map) { final ToDoList toDoList = initState(map); final ToDoList state = ToDoList(); state.list.add(toDoList.list[0]); return state; }, view: pageView, reducer: (ToDoList state, Action action) { if (action.type == ToDoListAction.remove) { final ToDoList newState = state.clone(); newState.list.clear(); return newState; } else { return toDoListReducer(state, action); } }, effect: toDoListEffect, dependencies: toDoListDependencies(track)) .buildPage(pageInitParams))); expect(find.byKey(const ValueKey('Add')), findsOneWidget); expect(find.text('Add'), findsOneWidget); expect(find.byKey(const ValueKey('mark-0')), findsOneWidget); expect(find.byKey(const ValueKey('edit-0')), findsOneWidget); expect(find.text('desc-0'), findsOneWidget); expect(find.byKey(const ValueKey('remove-0')), findsOneWidget); await tester.tap(find.byKey(const ValueKey('edit-0'))); await tester.pump(); await tester.tap(find.byKey(const ValueKey('remove-0'))); await tester.pump(); expect(find.byKey(const ValueKey('mark-0')), findsNothing); expect(find.byKey(const ValueKey('edit-0')), findsNothing); expect(find.byKey(const ValueKey('remove-0')), findsNothing); expect( track, Track.pins([ Pin('toDo-initState'), Pin('toDo-didChangeDependencies'), Pin('toDo-build'), Pin('toDo-onEdit'), Pin('toDo-didUpdateWidget'), Pin('toDo-build'), Pin('toDo-deactivate'), Pin('toDo-dispose') ])); }); }); } ================================================ FILE: test/lib/redux_component/page_test.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import 'package:flutter_test/flutter_test.dart'; import 'package:test_widgets/page/action.dart'; import 'package:test_widgets/page/page.dart'; import 'package:test_widgets/page/state.dart'; import 'package:test_widgets/test_base.dart'; import '../instrument.dart'; import '../track.dart'; void main() { group('page', () { test('create', () { TestPage page = TestPage( initState: initState, view: toDoListView, wrapper: (Widget child) => PageWrapper(child)); expect(page, isNotNull); /// TODO final Widget pageWidget = page.buildPage(pageInitParams); expect(pageWidget, isNotNull); expect(const TypeMatcher().check(pageWidget), isTrue); //expect(pageWidget, TypeMatcher()); }); testWidgets('build', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: instrumentInitState(initState, pre: (map) { track.append('initState', map); }), view: instrumentView(toDoListView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('build', state.clone()); })).buildPage(pageInitParams))); expect(find.byKey(const ValueKey('Add')), findsOneWidget); expect(find.text('Add'), findsOneWidget); expect(find.byKey(const ValueKey('mark-0')), findsOneWidget); expect(find.byKey(const ValueKey('edit-0')), findsOneWidget); expect(find.text('desc-0'), findsOneWidget); expect(find.byKey(const ValueKey('remove-0')), findsOneWidget); expect(find.text('title-0'), findsOneWidget); expect(find.byKey(const ValueKey('mark-1')), findsOneWidget); expect(find.byKey(const ValueKey('edit-1')), findsOneWidget); expect(find.text('desc-1'), findsOneWidget); expect(find.byKey(const ValueKey('remove-1')), findsOneWidget); expect(find.text('title-1'), findsOneWidget); expect(find.byKey(const ValueKey('mark-2')), findsOneWidget); expect(find.byKey(const ValueKey('edit-2')), findsOneWidget); expect(find.text('desc-2'), findsOneWidget); expect(find.byKey(const ValueKey('remove-2')), findsOneWidget); expect(find.text('title-2'), findsOneWidget); expect(find.byKey(const ValueKey('mark-3')), findsOneWidget); expect(find.byKey(const ValueKey('edit-3')), findsOneWidget); expect(find.text('desc-3'), findsOneWidget); expect(find.byKey(const ValueKey('remove-3')), findsOneWidget); expect(find.text('title-3'), findsOneWidget); expect(find.text('mark\ndone'), findsNWidgets(3)); expect(find.text('done'), findsOneWidget); expect( track, Track.pins([ Pin('initState', pageInitParams), Pin('build', ToDoList.fromMap(pageInitParams)) ])); }); testWidgets('reducer', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: instrumentInitState(initState, pre: (map) { track.append('initState', map); }), view: instrumentView(toDoListView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('build', state.clone()); }), reducer: instrumentReducer(toDoListReducer, suf: (ToDoList state, Action action) { track.append('onReduce', state.clone()); })).buildPage(pageInitParams))); await tester.tap(find.byKey(const ValueKey('mark-0'))); await tester.pump(); expect(find.text('mark\ndone'), findsNWidgets(2)); expect(find.text('done'), findsNWidgets(2)); await tester.tap(find.byKey(const ValueKey('mark-1'))); await tester.pump(); expect(find.text('mark\ndone'), findsNWidgets(1)); expect(find.text('done'), findsNWidgets(3)); await tester.tap(find.byKey(const ValueKey('remove-2'))); await tester.pump(); expect(find.byKey(const ValueKey('remove-2')), findsNothing); expect(find.text('desc-2'), findsNothing); expect(find.text('title-2'), findsNothing); await tester.tap(find.byKey(const ValueKey('remove-3'))); await tester.pump(); expect(find.byKey(const ValueKey('remove-3')), findsNothing); expect(find.text('desc-3'), findsNothing); expect(find.text('title-3'), findsNothing); ToDoList mockState = ToDoList.fromMap(pageInitParams); expect( track, Track.pins([ Pin('initState', pageInitParams), Pin('build', mockState.clone()), Pin('onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.markDone, payload: mockState.list.firstWhere((i) => i.id == '0'))); return mockState.clone(); }), Pin('build', mockState.clone()), Pin('onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.markDone, payload: mockState.list.firstWhere((i) => i.id == '1'))); return mockState.clone(); }), Pin('build', mockState.clone()), Pin('onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.remove, payload: mockState.list.firstWhere((i) => i.id == '2'))); return mockState.clone(); }), Pin('build', mockState.clone()), Pin('onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.remove, payload: mockState.list.firstWhere((i) => i.id == '3'))); return mockState.clone(); }), Pin('build', mockState.clone()), ])); }); testWidgets('effect', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: instrumentInitState(initState, pre: (map) { track.append('initState', map); }), view: instrumentView(toDoListView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('build', state.clone()); }), reducer: instrumentReducer(toDoListReducer, suf: (ToDoList state, Action action) { track.append('onReduce', state.clone()); }), effect: instrumentEffect(toDoListEffect, (Action action, Get getState) { if (action.type == ToDoListAction.onAdd) { track.append('onAdd', getState().clone()); } else if (action.type == ToDoListAction.onEdit) { track.append('onEdit', getState().clone()); } })).buildPage(pageInitParams))); await tester.tap(find.byKey(const ValueKey('Add'))); await tester.pump(); expect(find.text('title-mock', skipOffstage: false), findsNWidgets(1)); expect(find.text('desc-mock', skipOffstage: false), findsNWidgets(1)); await tester.tap(find.byKey(const ValueKey('Add'))); await tester.pump(); expect(find.text('title-mock', skipOffstage: false), findsNWidgets(2)); expect(find.text('desc-mock', skipOffstage: false), findsNWidgets(2)); await tester.tap(find.byKey(const ValueKey('edit-0'))); await tester.pump(); expect(find.text('title-0', skipOffstage: false), findsOneWidget); expect(find.text('desc-0-effect', skipOffstage: false), findsOneWidget); ToDoList mockState = ToDoList.fromMap(pageInitParams); expect( track, Track.pins([ Pin('initState', pageInitParams), Pin('build', mockState.clone()), Pin('onAdd', mockState.clone()), Pin('onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.add, payload: Todo.mock())); return mockState.clone(); }), Pin('build', mockState.clone()), Pin('onAdd', mockState.clone()), Pin('onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.add, payload: Todo.mock())); return mockState.clone(); }), Pin('build', mockState.clone()), Pin('onEdit', mockState.clone()), Pin('onReduce', () { Todo toDo = mockState.list.firstWhere((i) => i.id == '0'); toDo = toDo.clone(); toDo.desc = '${toDo.desc}-effect'; mockState = toDoListReducer( mockState, Action(ToDoListAction.edit, payload: toDo)); return mockState.clone(); }), Pin('build', mockState.clone()), ])); }); testWidgets('effectAsync', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: instrumentInitState(initState, pre: (map) { track.append('initState', map); }), view: instrumentView(toDoListView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('build', state.clone()); }), reducer: instrumentReducer(toDoListReducer, suf: (ToDoList state, Action action) { track.append('onReduce', state.clone()); }), effect: instrumentEffect(toDoListEffectAsync, (Action action, Get getState) { if (action.type == ToDoListAction.onAdd) { track.append('onAdd', getState().clone()); } else if (action.type == ToDoListAction.onEdit) { track.append('onEdit', getState().clone()); } })).buildPage(pageInitParams))); await tester.tap(find.byKey(const ValueKey('Add'))); await tester.pump(Duration(seconds: 3)); expect(find.text('title-mock', skipOffstage: false), findsNWidgets(1)); expect(find.text('desc-mock', skipOffstage: false), findsNWidgets(1)); await tester.tap(find.byKey(const ValueKey('Add'))); await tester.pump(Duration(seconds: 3)); expect(find.text('title-mock', skipOffstage: false), findsNWidgets(2)); expect(find.text('desc-mock', skipOffstage: false), findsNWidgets(2)); await tester.tap(find.byKey(const ValueKey('edit-0'))); await tester.pump(Duration(seconds: 3)); expect(find.text('title-0', skipOffstage: false), findsOneWidget); expect(find.text('desc-0-effect', skipOffstage: false), findsOneWidget); ToDoList mockState = ToDoList.fromMap(pageInitParams); expect( track, Track.pins([ Pin('initState', pageInitParams), Pin('build', mockState.clone()), Pin('onAdd', mockState.clone()), Pin('onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.add, payload: Todo.mock())); return mockState.clone(); }), Pin('build', mockState.clone()), Pin('onAdd', mockState.clone()), Pin('onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.add, payload: Todo.mock())); return mockState.clone(); }), Pin('build', mockState.clone()), Pin('onEdit', mockState.clone()), Pin('onReduce', () { Todo toDo = mockState.list.firstWhere((i) => i.id == '0'); toDo = toDo.clone(); toDo.desc = '${toDo.desc}-effect'; mockState = toDoListReducer( mockState, Action(ToDoListAction.edit, payload: toDo)); return mockState.clone(); }), Pin('build', mockState.clone()), ])); }); testWidgets('effect', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: instrumentInitState(initState, pre: (map) { track.append('initState', map); }), view: instrumentView(toDoListView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('build', state.clone()); }), reducer: instrumentReducer(toDoListReducer, suf: (ToDoList state, Action action) { track.append('onReduce', state.clone()); }), effect: (Action action, Context ctx) => instrumentEffect( toDoListEffect, (Action action, Get getState) { if (action.type == ToDoListAction.onAdd) { track.append('onAdd', getState().clone()); } else if (action.type == ToDoListAction.onEdit) { track.append('onEdit', getState().clone()); } })(action, ctx)).buildPage(pageInitParams))); expect(find.byKey(const ValueKey('Add')), findsOneWidget); await tester.tap(find.byKey(const ValueKey('Add'))); await tester.pump(); expect(find.text('title-mock', skipOffstage: false), findsNWidgets(1)); expect(find.text('desc-mock', skipOffstage: false), findsNWidgets(1)); expect(find.byKey(const ValueKey('Add')), findsOneWidget); await tester.tap(find.byKey(const ValueKey('Add'))); await tester.pump(); expect(find.text('title-mock', skipOffstage: false), findsNWidgets(2)); expect(find.text('desc-mock', skipOffstage: false), findsNWidgets(2)); expect(find.byKey(const ValueKey('edit-0')), findsOneWidget); await tester.tap(find.byKey(const ValueKey('edit-0'))); await tester.pump(); expect(find.text('title-0'), findsOneWidget); expect(find.text('desc-0-effect'), findsOneWidget); ToDoList mockState = ToDoList.fromMap(pageInitParams); expect( track, Track.pins([ Pin('initState', pageInitParams), Pin('build', mockState.clone()), Pin('onAdd', mockState.clone()), Pin('onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.add, payload: Todo.mock())); return mockState.clone(); }), Pin('build', mockState.clone()), Pin('onAdd', mockState.clone()), Pin('onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.add, payload: Todo.mock())); return mockState.clone(); }), Pin('build', mockState.clone()), Pin('onEdit', mockState.clone()), Pin('onReduce', () { Todo toDo = mockState.list.firstWhere((i) => i.id == '0'); toDo = toDo.clone(); toDo.desc = '${toDo.desc}-effect'; mockState = toDoListReducer( mockState, Action(ToDoListAction.edit, payload: toDo)); return mockState.clone(); }), Pin('build', mockState.clone()), ])); }); testWidgets('shouldUpdate', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: instrumentInitState(initState, pre: (map) { track.append('initState', map); }), view: instrumentView(toDoListView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('build', state.clone()); }), reducer: instrumentReducer(toDoListReducer, suf: (ToDoList state, Action action) { track.append('onReduce', state.clone()); }), effect: (Action action, Context ctx) => instrumentEffect(toDoListEffect, (Action action, Get getState) { if (action.type == ToDoListAction.onAdd) { track.append('onAdd', getState().clone()); } })(action, ctx), shouldUpdate: forbidRefreshUI) .buildPage(pageInitParams))); await tester.tap(find.byKey(const ValueKey('Add'))); await tester.pump(); expect(find.text('title-mock'), findsNothing); expect(find.text('desc-mock'), findsNothing); await tester.tap(find.byKey(const ValueKey('mark-0'))); await tester.pump(); expect(find.text('mark\ndone'), findsNWidgets(3)); expect(find.text('done'), findsOneWidget); await tester.tap(find.byKey(const ValueKey('remove-1'))); await tester.pump(); expect(find.byKey(const ValueKey('remove-1')), findsOneWidget); expect(find.text('desc-1'), findsOneWidget); expect(find.text('title-1'), findsOneWidget); ToDoList mockState = ToDoList.fromMap(pageInitParams); expect( track, Track.pins([ Pin('initState', pageInitParams), Pin('build', mockState.clone()), Pin('onAdd', mockState.clone()), Pin('onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.add, payload: Todo.mock())); return mockState.clone(); }), Pin('onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.markDone, payload: mockState.list.firstWhere((i) => i.id == '0'))); return mockState.clone(); }), Pin('onReduce', () { mockState = toDoListReducer( mockState, Action(ToDoListAction.remove, payload: mockState.list.firstWhere((i) => i.id == '1'))); return mockState.clone(); }), ])); }); /// TODO testWidgets('middleware', (WidgetTester tester) async { final Track track = Track(); await tester.pumpWidget(TestStub(TestPage( initState: instrumentInitState(initState, pre: (map) { track.append('initState', map); }), view: instrumentView(toDoListView, (ToDoList state, Dispatch dispatch, ViewService viewService) { track.append('build', state.clone()); }), reducer: instrumentReducer(toDoListReducer, suf: (ToDoList state, Action action) { track.append('onReduce', state.clone()); }), effect: toDoListEffect, middleware: >[ instrumentMiddleware(toDoListMiddleware, pre: (action, getState) { if (action.type == ToDoListAction.middlewareEdit) { track.append('onMiddleware', getState().clone()); } }) ]).buildPage(pageInitParams))); expect(find.byKey(const ValueKey('edit-0')), findsOneWidget); await tester.longPress(find.byKey(const ValueKey('edit-0'))); await tester.pump(); expect(find.text('desc-0-middleware'), findsOneWidget); expect(find.byKey(const ValueKey('edit-0')), findsOneWidget); await tester.longPress(find.byKey(const ValueKey('edit-0'))); await tester.pump(); expect(find.text('desc-0-middleware-middleware'), findsOneWidget); ToDoList mockState = ToDoList.fromMap(pageInitParams); expect( track, Track.pins([ Pin('initState', pageInitParams), Pin('build', mockState.clone()), Pin('onMiddleware', mockState.clone()), Pin('onReduce', mockState.clone()), Pin('onReduce', () { Todo toDo = mockState.list.firstWhere((i) => i.id == '0'); toDo = toDo.clone(); toDo.desc = '${toDo.desc}-middleware'; mockState = toDoListReducer( mockState, Action(ToDoListAction.edit, payload: toDo)); return mockState.clone(); }), Pin('build', mockState.clone()), Pin('onMiddleware', mockState.clone()), Pin('onReduce', mockState.clone()), Pin('onReduce', () { Todo toDo = mockState.list.firstWhere((i) => i.id == '0'); toDo = toDo.clone(); toDo.desc = '${toDo.desc}-middleware'; mockState = toDoListReducer( mockState, Action(ToDoListAction.edit, payload: toDo)); return mockState.clone(); }), Pin('build', mockState.clone()), ])); }); // testWidgets('error', (WidgetTester tester) async { // final Track track = Track(); // await tester.pumpWidget(TestStub(TestPage( // initState: initState, // view: toDoListView, // reducer: toDoListReducer, // higherEffect: toDoListHigherEffect, // onError: instrumentError(toDoListErrorHandler, (exp, ctx) { // track.append('onErr', exp); // })).buildPage(pageInitParams))); // await tester.tap(find.byKey(const ValueKey('Error'))); // await tester.pump(); // expect( // track, // Track.pins([ // Pin('onErr', KnowException()), // ])); // //expect(exception, UnKnowException()); // }); // testWidgets('errorAsync', (WidgetTester tester) async { // final Track track = Track(); // await tester.pumpWidget(TestStub(TestPage( // initState: initState, // view: toDoListView, // reducer: toDoListReducer, // effect: toDoListEffectAsync, // onError: instrumentError(toDoListErrorHandler, (exp, ctx) { // track.append('onErr', exp); // })).buildPage(pageInitParams))); // await tester.tap(find.byKey(const ValueKey('Error'))); // await tester.pump(Duration(seconds: 3)); // expect( // track, // Track.pins([ // Pin('onErr', KnowException()), // ])); // //expect(exception, UnKnowException()); // }); }); } ================================================ FILE: test/lib/redux_component/redux_component_test.dart ================================================ import 'package:test/test.dart'; import 'auto_dispose_test.dart' as auto_dispose; import 'component_test.dart' as component; import 'lifecycle_test.dart' as lifecycle; import 'page_test.dart' as page; void main() { group('redux_component_test', () { auto_dispose.main(); component.main(); lifecycle.main(); page.main(); }); } ================================================ FILE: test/lib/redux_connector/map_like_test.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter_test/flutter_test.dart'; class _Info extends MapLike { String name; int age = 0; _Info(this.name); } void main() { group('map_like_test', () { test('map_like_with_key', () { final _Info info = _Info('Tom'); final AutoInitConnector<_Info, String> nameConnector = AutoInitConnector<_Info, String>((_Info info) => info.name, key: 'name'); expect(nameConnector.get(info), equals('Tom')); nameConnector.set(info, 'John'); expect(nameConnector.get(info), equals('John')); final AutoInitConnector<_Info, int> ageConnector = AutoInitConnector<_Info, int>((_Info info) => info.age, key: 'age'); expect(ageConnector.get(info), equals(0)); }); test('map_like_without_key', () { final _Info info = _Info('Tom'); final AutoInitConnector<_Info, int> generatedKeyConnector = AutoInitConnector<_Info, int>((_Info info) => info.age); expect(generatedKeyConnector.get(info), equals(0)); generatedKeyConnector.set(info, 1); expect(generatedKeyConnector.get(info), equals(1)); }); test('map_like_with_hook', () { final _Info info = _Info('Tom'); String newValue = ''; final AutoInitConnector<_Info, String> nameConnector = AutoInitConnector<_Info, String>((_Info info) => info.name, key: 'name', set: (_, String value) => newValue = value); expect(newValue, equals('')); nameConnector.set(info, 'John'); expect(newValue, equals('John')); }); }); } ================================================ FILE: test/lib/redux_connector/redux_connector_test.dart ================================================ import 'package:test/test.dart'; import 'map_like_test.dart' as mapLike; import 'reselect_test.dart' as reselect; void main() { group('redux_connector_test', () { reselect.main(); mapLike.main(); }); } ================================================ FILE: test/lib/redux_connector/reselect_test.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter_test/flutter_test.dart'; class _Parent { int value; String label; List children; _Parent(this.value, this.label, this.children); } class _Info { int value; String label; _Info(this.value, this.label); } class _InfoConn extends Reselect2<_Parent, _Info, int, String> { @override _Info computed(int sub0, String sub1) => _Info(sub0, sub1); @override int getSub0(_Parent state) => state.value; @override String getSub1(_Parent state) => state.label; @override void set(_Parent state, _Info subState) { state.value = subState.value; state.label = subState.label; } } void main() { group('reselect_test', () { final _Parent parent = _Parent(1, 'tag', null); final AbstractConnector<_Parent, _Info> r2 = _InfoConn(); _Info i0, i1, i2, i3; i0 = r2.get(parent); test('reselect_init', () { expect(i0.value == parent.value, isTrue); expect(i0.label == parent.label, isTrue); }); test('reselect_nochange', () { parent.children = []; i1 = r2.get(parent); expect(i0 == i1, isTrue); }); test('reselect_change', () { parent.value = 2; i2 = r2.get(parent); expect(i0 == i2, isFalse); i3 = r2.get(parent); expect(i2 == i3, isTrue); }); test('reselect_nochange2', () { parent.children = []; i3 = r2.get(parent); expect(i2 == i3, isTrue); }); }); } ================================================ FILE: test/lib/redux_middleware/redux_middleware_test.dart ================================================ import 'package:test/test.dart'; void main() { group('redux_middleware_test', () { /// todo }); } ================================================ FILE: test/lib/redux_routes/redux_routes_test.dart ================================================ import 'package:test/test.dart'; void main() { group('redux_routes_test', () { /// todo }); } ================================================ FILE: test/lib/track.dart ================================================ class Track { final List _pins = []; Track(); factory Track.tags(List tags) { tags ??= []; final Track tracer = Track(); tags.forEach((String tag) => tracer.append(tag)); return tracer; } factory Track.pins(List tags) { tags ??= []; final Track tracer = Track(); tags.forEach((Pin pin) => tracer.append(pin.tag, pin.value)); return tracer; } void append(String tag, [Object value]) { _pins.add(Pin(tag, value)); } int countOfTag(String tag) => _pins.fold(0, (count, pin) => pin.tag == tag ? count + 1 : count); void remove(String tag) => _pins.retainWhere((pin)=>pin.tag == tag); String toString() => _pins .map((node) => node.toString()) .fold('', (prev, now) => '$prev\n=>$now'); @override bool operator ==(dynamic other) { if (!(other is Track)) return false; if (_pins.length != other._pins.length) return false; for (int index = 0; index < _pins.length; index++) { if (_pins[index] != other._pins[index]) return false; } return true; } void reset() { _pins.clear(); } } class Pin { String tag; Object value; DateTime timeStamp; Pin(this.tag, [Object value]) : timeStamp = DateTime.now(), value = value is Function ? value() : value; @override String toString() => '$tag<${value?.toString()}>'; @override bool operator ==(dynamic other) { if (!(other is Pin)) return false; return other.tag == tag && other.value == value; } } ================================================ FILE: test/lib/utils/collections_test.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('collections_test', () { test('collections_reduce', () { expect(Collections.reduce(null, (int v, int p) => p + v), isNull); expect( Collections.reduce([1, 2, 3, 4], (int v, int p) => p + v) == 10, isTrue); }); test('collections_flatten', () { final List a = ['a', 'b']; final List b = ['1', '2']; final List> list = >[a, b]; final List listFlatten = Collections.flatten(list); expect(listFlatten, orderedEquals(['a', 'b', '1', '2'])); }); test('collections_merge', () { final List a = ['1', '2']; final List b = ['3', '4']; final List merge = Collections.merge(a, b); expect(merge, orderedEquals(['1', '2', '3', '4'])); }); test('collections_clone', () { final List list = ['hello', 'world']; expect( Collections.clone(list), orderedEquals(['hello', 'world'])); }); test('collections_castMapToList', () { final Map map = { 'name': 'John', 'gender': 'male', 'age': '25' }; final List list = Collections.castMapToList(map, (String value, String key) => value); expect(Collections.clone(list), orderedEquals(['John', 'male', '25'])); }); test('collections_isEmpty', () { expect(Collections.isEmpty(null), isTrue); expect(Collections.isEmpty([]), isTrue); expect(Collections.isEmpty(['v']), isFalse); expect(Collections.isNotEmpty(['v']), isTrue); expect(Collections.isEmpty(''), isTrue); expect(Collections.isEmpty({'name': 'Tom'}), isFalse); }); }); } ================================================ FILE: test/lib/utils/utils_test.dart ================================================ import 'package:test/test.dart'; import 'collections_test.dart' as collections; void main() { group('utils_test', () { collections.main(); /// todo }); } ================================================ FILE: test/pubspec.yaml ================================================ name: fish_redux_test description: A new Flutter package. version: 0.0.1 author: homepage: environment: sdk: '>=2.0.0-dev.68.0 <3.0.0' dependencies: flutter: sdk: flutter test: ^1.5.1 # mockito: ^4.0.0 dev_dependencies: # test: ^1.5.1 flutter_test: sdk: flutter test_widgets: path: test_widgets # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec # The following section is specific to Flutter. flutter: # To add assets to your package, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # # For details regarding assets in packages, see # https://flutter.io/assets-and-images/#from-packages # # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.io/assets-and-images/#resolution-aware. # To add custom fonts to your package, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts in packages, see # https://flutter.io/custom-fonts/#from-packages ================================================ FILE: test/test_widgets/.gitignore ================================================ .DS_Store .dart_tool/ .packages .pub/ build/ .flutter-plugins pubspec.lock ================================================ FILE: test/test_widgets/lib/adapter/action.dart ================================================ enum ToDoListAction { onAdd, add, onEdit, edit, markDone, remove, onKnowException, onUnKnowException } ================================================ FILE: test/test_widgets/lib/adapter/adapter.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import 'action.dart'; import 'state.dart'; Widget toDoView( Todo toDo, Dispatch dispatch, ViewService viewService, ) { return Container( margin: const EdgeInsets.all(8.0), color: Colors.grey, child: Row( children: [ Expanded( child: Container( child: Column( children: [ GestureDetector( child: Container( key: ValueKey('remove-${toDo.id}'), padding: const EdgeInsets.all(8.0), height: 28.0, color: Colors.yellow, child: Text( toDo.title, style: TextStyle(fontSize: 16.0), ), alignment: AlignmentDirectional.centerStart, ), onTap: () { print('dispatch remove'); dispatch(Action(ToDoListAction.remove, payload: toDo)); }, ), GestureDetector( child: Container( key: ValueKey('edit-${toDo.id}'), padding: const EdgeInsets.all(8.0), height: 60.0, color: Colors.grey, child: Text(toDo.desc, style: TextStyle(fontSize: 14.0)), alignment: AlignmentDirectional.centerStart, ), onTap: () { print('dispatch onEdit'); dispatch(Action(ToDoListAction.onEdit, payload: toDo)); }, ) ], ), )), GestureDetector( child: Container( key: ValueKey('mark-${toDo.id}'), color: toDo.isDone ? Colors.green : Colors.red, width: 88.0, height: 88.0, child: Text(toDo.isDone ? 'done' : 'mark\ndone'), alignment: AlignmentDirectional.center, ), onTap: () { if (!toDo.isDone) { print('dispatch markDone'); dispatch(Action(ToDoListAction.markDone, payload: toDo)); } }, onLongPress: () { print('dispatch Add'); dispatch(Action(ToDoListAction.onAdd, payload: toDo)); }) ], ), ); } bool toDoListEffect(Action action, Context ctx) { if (action.type == ToDoListAction.onAdd) { print('onAdd'); ctx.dispatch(Action(ToDoListAction.add, payload: Todo.mock())); return true; } else if (action.type == ToDoListAction.onEdit) { print('onEdit'); assert(action.payload is Todo); Todo toDo = ctx.state.list .firstWhere((i) => i.id == action.payload.id, orElse: () => null) .clone(); toDo.desc = '${toDo.desc}-effect'; ctx.dispatch(Action(ToDoListAction.edit, payload: toDo)); return true; } return false; } dynamic toDoListEffectAsync(Action action, Context ctx) { if (action.type == ToDoListAction.onAdd || action.type == ToDoListAction.onEdit || action.type == ToDoListAction.onKnowException || action.type == ToDoListAction.onUnKnowException) { return Future.delayed( Duration(seconds: 1), () => toDoListEffect(action, ctx)); } return null; } Dispatch toDoListHigherEffect(Context ctx) => (Action action) => toDoListEffect(action, ctx); ToDoList toDoListReducer(ToDoList state, Action action) { print('onReduce:${action.type}'); if (!(action.payload is Todo)) return state; Todo item = action.payload as Todo; if (action.type == ToDoListAction.add) { return state.clone()..list.add(item); } else if (action.type == ToDoListAction.markDone) { return state.clone() ..list .firstWhere((toDo) => toDo.id == item.id, orElse: () => null) ?.isDone = true; } else if (action.type == ToDoListAction.remove) { return state.clone()..list.removeWhere((toDo) => toDo.id == item.id); } else if (action.type == ToDoListAction.edit) { Todo toDo = state.list.firstWhere((toDo) => toDo.id == item.id); int index = state.list.indexOf(toDo); toDo = toDo.clone()..desc = item.desc; return state.clone()..list[index] = toDo; } else { return state; } } ListAdapter toDoListAdapter( ToDoList state, Dispatch dispatch, ViewService viewService, ) { return ListAdapter((context, index) { Todo toDo = state.list[index]; return toDoView(toDo, dispatch, viewService); }, state.list.length); } ================================================ FILE: test/test_widgets/lib/adapter/page.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import '../test_base.dart'; import 'adapter.dart'; import 'state.dart'; Widget pageView( ToDoList state, Dispatch dispatch, ViewService viewService, ) { print('build pageView'); ListAdapter listAdapter = viewService.buildAdapter(); return ListView.builder( itemBuilder: listAdapter.itemBuilder, itemCount: listAdapter.itemCount); } const Map pageInitParams = { 'list': [ { 'id': '0', 'title': 'title-0', 'desc': 'desc-0', 'isDone': false }, { 'id': '1', 'title': 'title-1', 'desc': 'desc-1', 'isDone': false }, { 'id': '2', 'title': 'title-2', 'desc': 'desc-2', 'isDone': false }, { 'id': '3', 'title': 'title-3', 'desc': 'desc-3', 'isDone': true } ] }; ToDoList initState(Map map) => ToDoList.fromMap(map); class PageWrapper extends StatelessWidget { final Widget child; PageWrapper(this.child); @override Widget build(BuildContext context) { return child; } } Widget createAdapterWidget(BuildContext context) { return TestPage( initState: initState, view: pageView, dependencies: Dependencies( adapter: NoneConn() + TestAdapter( adapter: toDoListAdapter, reducer: toDoListReducer, effect: toDoListEffect))) .buildPage(pageInitParams); } ================================================ FILE: test/test_widgets/lib/adapter/state.dart ================================================ import 'package:fish_redux/fish_redux.dart'; class Todo implements Cloneable { String id; String title; String desc; bool isDone = false; Todo(); factory Todo.mock() => Todo() ..id = 'id-mock' ..title = 'title-mock' ..desc = 'desc-mock' ..isDone = false; @override Todo clone() => Todo() ..id = this.id ..title = this.title ..desc = this.desc ..isDone = this.isDone; factory Todo.fromMap(Map map) { return Todo() ..id = map['id'] ?? 'uniq' ..title = map['title'] ?? '' ..desc = map['desc'] ?? '' ..isDone = map['isDone'] ?? false; } @override bool operator ==(dynamic other) { if (!(other is Todo)) return false; return id == other.id && title == other.title && desc == other.desc && isDone == other.isDone; } @override String toString() { return 'Todo{id: $id, title: $title, desc: $desc, isDone: $isDone}'; } } class ToDoList implements Cloneable { List list = []; ToDoList(); @override ToDoList clone() => ToDoList()..list.addAll(this.list); factory ToDoList.fromMap(Map map) { return ToDoList() ..list.addAll( map['list']?.map((Map map) => Todo.fromMap(map))?.toList()); } @override bool operator ==(dynamic other) { if (!(other is ToDoList)) return false; if (list.length != other.list.length) return false; for (int index = 0; index < list.length; index++) { if (list[index] != other.list[index]) return false; } return true; } @override String toString() { return '{list: $list}'; } } ================================================ FILE: test/test_widgets/lib/component/action.dart ================================================ enum ToDoListAction { onAdd, add, remove, onBroadcast, broadcast } enum ToDoAction { onEdit, edit, markDone, onBroadcast, broadcast } ================================================ FILE: test/test_widgets/lib/component/component.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import 'action.dart'; import 'state.dart'; Widget toDoView(Todo toDo, Dispatch dispatch, ViewService viewService) { return Container( margin: const EdgeInsets.all(8.0), color: Colors.grey, child: Row( children: [ Expanded( child: Container( child: Column( children: [ GestureDetector( child: Container( key: ValueKey('remove-${toDo.id}'), padding: const EdgeInsets.all(8.0), height: 28.0, color: Colors.yellow, child: Text( toDo.title, style: TextStyle(fontSize: 16.0), ), alignment: AlignmentDirectional.centerStart, ), onTap: () { print('dispatch remove'); dispatch( Action(ToDoListAction.remove, payload: toDo.clone())); }, ), GestureDetector( child: Container( key: ValueKey('edit-${toDo.id}'), padding: const EdgeInsets.all(8.0), height: 60.0, color: Colors.grey, child: Text(toDo.desc, style: TextStyle(fontSize: 14.0)), alignment: AlignmentDirectional.centerStart, ), onTap: () { print('dispatch onEdit'); dispatch(Action(ToDoAction.onEdit, payload: toDo.clone())); }, ) ], ), )), GestureDetector( child: Container( key: ValueKey('mark-${toDo.id}'), color: toDo.isDone ? Colors.green : Colors.red, width: 88.0, height: 88.0, child: Text(toDo.isDone ? 'done' : 'mark\ndone'), alignment: AlignmentDirectional.center, ), onTap: () { if (!toDo.isDone) { print('dispatch markDone'); dispatch(Action(ToDoAction.markDone, payload: toDo.clone())); } }, onLongPress: () { print('dispatch broadcast'); dispatch(Action(ToDoAction.onBroadcast)); }, ) ], ), ); } bool toDoEffect(Action action, Context ctx) { if (action.type == ToDoAction.onEdit) { print('onEdit'); Todo toDo = ctx.state.clone(); toDo.desc = '${toDo.desc}-effect'; ctx.dispatch(Action(ToDoAction.edit, payload: toDo)); return true; } else if (action.type == ToDoAction.onBroadcast) { ctx.broadcastEffect(Action(ToDoAction.broadcast)); return true; } else if (action.type == Lifecycle.initState) { print('!!! initState ${ctx.state}'); return true; } else if (action.type == Lifecycle.dispose) { print('!!! dispose ${ctx.state}'); return true; } return false; } dynamic toDoEffectAsync(Action action, Context ctx) { if (action.type == ToDoAction.onEdit) { return Future.delayed(Duration(seconds: 1), () => toDoEffect(action, ctx)); } return null; } Dispatch toDoHigherEffect(Context ctx) => (Action action) => toDoEffect(action, ctx); Todo toDoReducer(Todo state, Action action) { if (!(action.payload is Todo) || state.id != action.payload.id) return state; print('onReduce:${action.type}'); if (action.type == ToDoAction.markDone) { return state.clone()..isDone = true; } else if (action.type == ToDoAction.edit) { return state.clone()..desc = action.payload.desc; } else { return state.clone(); } } bool shouldUpdate(Todo old, Todo now) => old != now; bool reducerFilter(Todo toDo, Action action) { return action.type == ToDoAction.edit || action.type == ToDoAction.markDone; } class ToDoComponent extends Component { ToDoComponent() : super( view: toDoView, effect: toDoEffect, reducer: toDoReducer, shouldUpdate: shouldUpdate, filter: reducerFilter); } class ComponentWrapper extends StatelessWidget { final Widget child; ComponentWrapper(this.child); @override Widget build(BuildContext context) { return child; } } ================================================ FILE: test/test_widgets/lib/component/page.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import '../test_base.dart'; import 'action.dart'; import 'component.dart'; import 'state.dart'; Widget toDoListView( ToDoList state, Dispatch dispatch, ViewService viewService, ) { print('build toDoListView'); return Column( children: [ Expanded( child: ListView.builder( itemBuilder: (context, index) { if (index == 0) { return viewService.buildComponent('toDo0'); } else if (index == 1) { return viewService.buildComponent('toDo1'); } else if (index == 2) { return viewService.buildComponent('toDo2'); } else if (index == 3) { return viewService.buildComponent('toDo3'); } else { Todo toDo = state.list[index]; return Container( padding: const EdgeInsets.all(8.0), margin: const EdgeInsets.all(8.0), color: Colors.grey, child: Text(toDo.desc), alignment: AlignmentDirectional.center, ); } }, itemCount: state.list.length, )), Row( children: [ Expanded( child: GestureDetector( child: Container( key: ValueKey('Add'), height: 68.0, color: Colors.green, alignment: AlignmentDirectional.center, child: Text('Add'), ), onTap: () { print('dispatch Add'); dispatch(Action(ToDoListAction.onAdd)); }, )), ], ) ], ); } bool toDoListEffect(Action action, Context ctx) { if (action.type == ToDoListAction.onAdd) { print('onAdd'); ctx.dispatch(Action(ToDoListAction.add, payload: Todo.mock())); return true; } else if (action.type == ToDoListAction.onBroadcast) { ctx.broadcastEffect(Action(ToDoListAction.broadcast)); return true; } return false; } dynamic toDoListEffectAsync(Action action, Context ctx) { if (action.type == ToDoListAction.onAdd) { return Future.delayed( Duration(seconds: 1), () => toDoListEffect(action, ctx)); } return null; } Dispatch toDoListHigherEffect(Context ctx) => (Action action) => toDoListEffect(action, ctx); ToDoList toDoListReducer(ToDoList state, Action action) { print('onReduce:${action.type}'); if (!(action.payload is Todo)) return state; if (action.type == ToDoListAction.add) { return state.clone()..list.add(action.payload); } else if (action.type == ToDoListAction.remove) { Todo toDo = state.list.firstWhere((toDo) => toDo.id == action.payload.id); int index = state.list.indexOf(toDo); toDo = toDo.clone()..desc = 'removed'; return state.clone()..list[index] = toDo; } else { return state.clone(); } } const Map pageInitParams = { 'list': [ { 'id': '0', 'title': 'title-0', 'desc': 'desc-0', 'isDone': false }, { 'id': '1', 'title': 'title-1', 'desc': 'desc-1', 'isDone': false }, { 'id': '2', 'title': 'title-2', 'desc': 'desc-2', 'isDone': false }, { 'id': '3', 'title': 'title-3', 'desc': 'desc-3', 'isDone': true } ] }; ToDoList initState(Map map) => ToDoList.fromMap(map); class ToDoComponent0 extends ToDoComponent {} class ToDoComponent1 extends ToDoComponent {} class ToDoComponent2 extends ToDoComponent {} class ToDoComponent3 extends ToDoComponent {} final toDoListDependencies = Dependencies(slots: { 'toDo0': ConnOp( get: (toDoList) => toDoList.list[0], set: (toDoList, toDo) => toDoList.list[0] = toDo) + ToDoComponent0(), 'toDo1': ConnOp( get: (toDoList) => toDoList.list[1], set: (toDoList, toDo) => toDoList.list[1] = toDo) + ToDoComponent1(), 'toDo2': ConnOp( get: (toDoList) => toDoList.list[2], set: (toDoList, toDo) => toDoList.list[2] = toDo) + ToDoComponent2(), 'toDo3': ConnOp( get: (toDoList) => toDoList.list[3], set: (toDoList, toDo) => toDoList.list[3] = toDo) + ToDoComponent3(), }); Widget createComponentWidget(BuildContext context) { return TestPage( initState: initState, view: toDoListView, reducer: toDoListReducer, effect: toDoListEffect, dependencies: toDoListDependencies) .buildPage(pageInitParams); } ================================================ FILE: test/test_widgets/lib/component/state.dart ================================================ import 'package:fish_redux/fish_redux.dart'; class Todo implements Cloneable { String id; String title; String desc; bool isDone = false; Todo(); factory Todo.mock() => Todo() ..id = 'id-mock' ..title = 'title-mock' ..desc = 'desc-mock' ..isDone = false; @override Todo clone() => Todo() ..id = this.id ..title = this.title ..desc = this.desc ..isDone = this.isDone; factory Todo.fromMap(Map map) { return Todo() ..id = map['id'] ?? 'uniq' ..title = map['title'] ?? '' ..desc = map['desc'] ?? '' ..isDone = map['isDone'] ?? false; } @override bool operator ==(dynamic other) { if (!(other is Todo)) return false; return id == other.id && title == other.title && desc == other.desc && isDone == other.isDone; } @override String toString() { return 'Todo{id: $id, title: $title, desc: $desc, isDone: $isDone}'; } } class ToDoList implements Cloneable { List list = []; ToDoList(); @override ToDoList clone() => ToDoList()..list.addAll(this.list); factory ToDoList.fromMap(Map map) { return ToDoList() ..list.addAll( map['list']?.map((Map map) => Todo.fromMap(map))?.toList()); } @override bool operator ==(dynamic other) { if (!(other is ToDoList)) return false; if (list.length != other.list.length) return false; for (int index = 0; index < list.length; index++) { if (list[index] != other.list[index]) return false; } return true; } @override String toString() { return '{list: $list}'; } } ================================================ FILE: test/test_widgets/lib/dynamic_flow_adapter/action.dart ================================================ enum ToDoListAction { onAdd, add, remove, broadcast } enum ToDoAction { onEdit, edit, markDone, onBroadcast, broadcast } enum PageAction { onAdd, } ================================================ FILE: test/test_widgets/lib/dynamic_flow_adapter/adapter.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import 'action.dart'; import 'state.dart'; Widget toDoView(Todo toDo, Dispatch dispatch, ViewService viewService) { return Container( margin: const EdgeInsets.all(8.0), color: Colors.grey, child: Row( children: [ Expanded( child: Container( child: Column( children: [ GestureDetector( child: Container( key: ValueKey('remove-${toDo.id}'), padding: const EdgeInsets.all(8.0), height: 28.0, color: Colors.yellow, child: Text( toDo.title, style: TextStyle(fontSize: 16.0), ), alignment: AlignmentDirectional.centerStart, ), onTap: () { print('dispatch remove'); dispatch( Action(ToDoListAction.remove, payload: toDo.clone())); }, ), GestureDetector( child: Container( key: ValueKey('edit-${toDo.id}'), padding: const EdgeInsets.all(8.0), height: 60.0, color: Colors.grey, child: Text(toDo.desc, style: TextStyle(fontSize: 14.0)), alignment: AlignmentDirectional.centerStart, ), onTap: () { print('dispatch onEdit'); dispatch(Action(ToDoAction.onEdit, payload: toDo.clone())); }, ) ], ), )), GestureDetector( child: Container( key: ValueKey('mark-${toDo.id}'), color: toDo.isDone ? Colors.green : Colors.red, width: 88.0, height: 88.0, child: Text(toDo.isDone ? 'done' : 'mark\ndone'), alignment: AlignmentDirectional.center, ), onTap: () { if (!toDo.isDone) { print('dispatch markDone'); dispatch(Action(ToDoAction.markDone, payload: toDo.clone())); } }, onLongPress: () { print('dispatch broadcast'); dispatch(Action(ToDoAction.onBroadcast)); }, ) ], ), ); } bool toDoEffect(Action action, Context ctx) { if (action.type == ToDoAction.onEdit) { print('onEdit'); Todo toDo = ctx.state.clone(); toDo.desc = '${toDo.desc}-effect'; ctx.dispatch(Action(ToDoAction.edit, payload: toDo)); return true; } else if (action.type == ToDoAction.onBroadcast) { ctx.broadcastEffect(Action(ToDoAction.broadcast)); } else if (action.type == Lifecycle.initState) { //print('!!! initState ${ctx.state}'); return true; } else if (action.type == Lifecycle.dispose) { //print('!!! dispose ${ctx.state}'); return true; } return false; } dynamic toDoEffectAsync(Action action, Context ctx) { if (action.type == ToDoAction.onEdit) { return Future.delayed(Duration(seconds: 1), () => toDoEffect(action, ctx)); } return null; } Dispatch toDoHigherEffect(Context ctx) => (Action action) => toDoEffect(action, ctx); Todo toDoReducer(Todo state, Action action) { if (!(action.payload is Todo) || state.id != action.payload.id) return state; print('onReduce:${action.type}'); if (action.type == ToDoAction.markDone) { return state.clone()..isDone = true; } else if (action.type == ToDoAction.edit) { return state.clone()..desc = action.payload.desc; } else { return state.clone(); } } bool shouldUpdate(Todo old, Todo now) { return old != now; } bool reducerFilter(Todo toDo, Action action) { return action.type == ToDoAction.edit || action.type == ToDoAction.markDone; } ================================================ FILE: test/test_widgets/lib/dynamic_flow_adapter/component.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import 'action.dart'; import 'state.dart'; Widget toDoView(Todo toDo, Dispatch dispatch, ViewService viewService) { return Container( margin: const EdgeInsets.all(8.0), color: Colors.grey, child: Row( children: [ Expanded( child: Container( child: Column( children: [ GestureDetector( child: Container( key: ValueKey('remove-${toDo.id}'), padding: const EdgeInsets.all(8.0), height: 28.0, color: Colors.yellow, child: Text( toDo.title, style: TextStyle(fontSize: 16.0), ), alignment: AlignmentDirectional.centerStart, ), onTap: () { print('dispatch remove'); dispatch( Action(ToDoListAction.remove, payload: toDo.clone())); }, ), GestureDetector( child: Container( key: ValueKey('edit-${toDo.id}'), padding: const EdgeInsets.all(8.0), height: 60.0, color: Colors.grey, child: Text(toDo.desc, style: TextStyle(fontSize: 14.0)), alignment: AlignmentDirectional.centerStart, ), onTap: () { print('dispatch onEdit'); dispatch(Action(ToDoAction.onEdit, payload: toDo.clone())); }, ) ], ), )), GestureDetector( child: Container( key: ValueKey('mark-${toDo.id}'), color: toDo.isDone ? Colors.green : Colors.red, width: 88.0, height: 88.0, child: Text(toDo.isDone ? 'done' : 'mark\ndone'), alignment: AlignmentDirectional.center, ), onTap: () { if (!toDo.isDone) { print('dispatch markDone'); dispatch(Action(ToDoAction.markDone, payload: toDo.clone())); } }, onLongPress: () { print('dispatch broadcast'); dispatch(Action(ToDoAction.onBroadcast)); }, ) ], ), ); } bool toDoEffect(Action action, Context ctx) { if (action.type == ToDoAction.onEdit) { print('onEdit'); Todo toDo = ctx.state.clone(); toDo.desc = '${toDo.desc}-effect'; ctx.dispatch(Action(ToDoAction.edit, payload: toDo)); return true; } else if (action.type == ToDoAction.onBroadcast) { ctx.broadcastEffect(Action(ToDoAction.broadcast)); return true; } else if (action.type == Lifecycle.initState) { //print('!!! initState ${ctx.state}'); return true; } else if (action.type == Lifecycle.dispose) { //print('!!! dispose ${ctx.state}'); return true; } return false; } dynamic toDoEffectAsync(Action action, Context ctx) { if (action.type == ToDoAction.onEdit) { return Future.delayed(Duration(seconds: 1), () => toDoEffect(action, ctx)); } return null; } Dispatch toDoHigherEffect(Context ctx) => (Action action) => toDoEffect(action, ctx); Todo toDoReducer(Todo state, Action action) { if (!(action.payload is Todo) || state.id != action.payload.id) return state; print('onReduce:${action.type}'); if (action.type == ToDoAction.markDone) { return state.clone()..isDone = true; } else if (action.type == ToDoAction.edit) { return state.clone()..desc = action.payload.desc; } else { return state.clone(); } } bool shouldUpdate(Todo old, Todo now) { return old != now; } bool reducerFilter(Todo toDo, Action action) { return action.type == ToDoAction.edit || action.type == ToDoAction.markDone; } class ToDoComponent extends Component { ToDoComponent() : super( view: toDoView, effect: toDoEffect, reducer: toDoReducer, shouldUpdate: shouldUpdate, filter: reducerFilter); } class ComponentWrapper extends StatelessWidget { final Widget child; ComponentWrapper(this.child); @override Widget build(BuildContext context) { return child; } } ================================================ FILE: test/test_widgets/lib/dynamic_flow_adapter/dynamic_flow_adapter.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import '../test_base.dart'; import 'action.dart'; import 'component.dart'; import 'state.dart'; bool toDoListEffect(Action action, Context ctx) { if (action.type == ToDoListAction.onAdd) { print('adapter onAdd'); ctx.dispatch(Action(ToDoListAction.add, payload: Todo.mock())); return true; } return false; } ToDoList toDoListReducer(ToDoList state, Action action) { print('onReduce:${action.type}'); if (!(action.payload is Todo)) return state; if (action.type == ToDoListAction.add) { return state.clone()..list.add(action.payload); } else if (action.type == ToDoListAction.remove) { return state.clone() ..list.removeWhere((Todo toDo) => toDo.id == action.payload.id); } else { return state.clone(); } } final TestDynamicFlowAdapter testAdapter = TestDynamicFlowAdapter( pool: {'toDo': ToDoComponent()}, connector: ConnOp>( get: (ToDoList toDoList) => toDoList.list .map((Todo toDo) => ItemBean('toDo', toDo)) .toList(), set: (ToDoList toDoList, List beans) { toDoList.list.clear(); toDoList.list.addAll( beans.map((ItemBean bean) => bean.data).toList()); }), reducer: toDoListReducer, effect: toDoListEffect); ================================================ FILE: test/test_widgets/lib/dynamic_flow_adapter/page.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import '../test_base.dart'; import 'action.dart'; import 'dynamic_flow_adapter.dart'; import 'state.dart'; Widget pageView( ToDoList state, Dispatch dispatch, ViewService viewService, ) { print('build pageView'); final ListAdapter listAdapter = viewService.buildAdapter(); return Column( children: [ Expanded( child: ListView.builder( itemBuilder: listAdapter.itemBuilder, itemCount: listAdapter.itemCount)), Row( children: [ Expanded( child: GestureDetector( child: Container( key: ValueKey('Add'), height: 68.0, color: Colors.green, alignment: AlignmentDirectional.center, child: Text('Add'), ), onTap: () { print('dispatch Add'); dispatch(Action(PageAction.onAdd)); }, onLongPress: () { print('dispatch broadcast'); viewService.broadcastEffect(const Action(ToDoAction.broadcast)); }, )), ], ) ], ); } const Map pageInitParams = { 'list': [ { 'id': '0', 'title': 'title-0', 'desc': 'desc-0', 'isDone': false }, { 'id': '1', 'title': 'title-1', 'desc': 'desc-1', 'isDone': false }, { 'id': '2', 'title': 'title-2', 'desc': 'desc-2', 'isDone': false }, { 'id': '3', 'title': 'title-3', 'desc': 'desc-3', 'isDone': true } ] }; bool pageEffect(Action action, Context ctx) { if (action.type == PageAction.onAdd) { print('page onAdd'); ctx.broadcastEffect(Action(ToDoListAction.onAdd, payload: Todo.mock())); return true; } return false; } ToDoList initState(Map map) => ToDoList.fromMap(map); Widget createDynamicAdapterWidget(BuildContext context) { return TestPage( view: pageView, initState: initState, effect: pageEffect, dependencies: Dependencies( adapter: NoneConn() + testAdapter)) .buildPage(pageInitParams); } ================================================ FILE: test/test_widgets/lib/dynamic_flow_adapter/state.dart ================================================ import 'package:fish_redux/fish_redux.dart'; class Todo implements Cloneable { String id; String title; String desc; bool isDone = false; Todo(); factory Todo.mock() => Todo() ..id = 'id-mock' ..title = 'title-mock' ..desc = 'desc-mock' ..isDone = false; @override Todo clone() => Todo() ..id = this.id ..title = this.title ..desc = this.desc ..isDone = this.isDone; factory Todo.fromMap(Map map) { return Todo() ..id = map['id'] ?? 'uniq' ..title = map['title'] ?? '' ..desc = map['desc'] ?? '' ..isDone = map['isDone'] ?? false; } @override bool operator ==(dynamic other) { if (!(other is Todo)) return false; return id == other.id && title == other.title && desc == other.desc && isDone == other.isDone; } @override String toString() { return 'Todo{id: $id, title: $title, desc: $desc, isDone: $isDone}'; } } class ToDoList implements Cloneable { List list = []; ToDoList(); @override ToDoList clone() => ToDoList()..list.addAll(this.list); factory ToDoList.fromMap(Map map) { return ToDoList() ..list.addAll( map['list']?.map((Map map) => Todo.fromMap(map))?.toList()); } @override bool operator ==(dynamic other) { if (!(other is ToDoList)) return false; if (list.length != other.list.length) return false; for (int index = 0; index < list.length; index++) { if (list[index] != other.list[index]) return false; } return true; } @override String toString() { return '{list: $list}'; } } ================================================ FILE: test/test_widgets/lib/main.dart ================================================ import 'package:flutter/material.dart' hide Action, Page; import 'package:test_widgets/adapter/page.dart'; import 'package:test_widgets/component/page.dart'; import 'package:test_widgets/dynamic_flow_adapter/page.dart'; import 'package:test_widgets/page/page.dart'; import 'package:test_widgets/static_flow_adapter/page.dart'; import 'test_base.dart'; final Map cases = { 'buildPage': createPageWidget, 'buildComponent': createComponentWidget, 'buildAdapter': createAdapterWidget, 'buildStaticAdapter': createStaticAdapterWidget, 'buildDynamicAdapter': createDynamicAdapterWidget }; void main() { runApp(TestStub(ListView.builder( itemBuilder: (BuildContext context, int index) { final String name = cases.keys.toList()[index]; final WidgetBuilder builder = cases.values.toList()[index]; return GestureDetector( child: Container( height: 86.0, margin: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0), alignment: AlignmentDirectional.center, color: Colors.grey, child: Text( name, style: const TextStyle(fontSize: 16, color: Colors.black), ), ), onTap: () { Navigator.of(context).push(MaterialPageRoute( builder: (BuildContext context) => TestStub(builder(context)))); }, ); }, itemCount: cases.length))); } ================================================ FILE: test/test_widgets/lib/page/action.dart ================================================ enum ToDoListAction { onAdd, add, onEdit, middlewareEdit, edit, markDone, remove, onKnowException, onUnKnowException } ================================================ FILE: test/test_widgets/lib/page/exception.dart ================================================ class KnowException implements Exception{ @override bool operator ==(dynamic other) => other is KnowException; } class UnKnowException implements Exception{ @override bool operator ==(dynamic other) => other is UnKnowException; } ================================================ FILE: test/test_widgets/lib/page/page.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import '../test_base.dart'; import 'action.dart'; import 'exception.dart'; import 'state.dart'; Widget toDoView(Todo toDo, BuildContext context, Dispatch dispatch) { return Container( margin: const EdgeInsets.all(8.0), color: Colors.grey, child: Row( children: [ Expanded( child: Container( child: Column( children: [ GestureDetector( child: Container( key: ValueKey('remove-${toDo.id}'), padding: const EdgeInsets.all(8.0), height: 28.0, color: Colors.yellow, child: Text( toDo.title, style: TextStyle(fontSize: 16.0), ), alignment: AlignmentDirectional.centerStart, ), onTap: () { print('dispatch remove'); dispatch(Action(ToDoListAction.remove, payload: toDo)); }, ), GestureDetector( child: Container( key: ValueKey('edit-${toDo.id}'), padding: const EdgeInsets.all(8.0), height: 60.0, color: Colors.grey, child: Text(toDo.desc, style: TextStyle(fontSize: 14.0)), alignment: AlignmentDirectional.centerStart, ), onTap: () { print('dispatch onEdit'); dispatch(Action(ToDoListAction.onEdit, payload: toDo)); }, onLongPress: () { print('dispatch middlewareEdit'); dispatch( Action(ToDoListAction.middlewareEdit, payload: toDo)); }, ) ], ), )), GestureDetector( child: Container( key: ValueKey('mark-${toDo.id}'), color: toDo.isDone ? Colors.green : Colors.red, width: 88.0, height: 88.0, child: Text(toDo.isDone ? 'done' : 'mark\ndone'), alignment: AlignmentDirectional.center, ), onTap: () { if (!toDo.isDone) { print('dispatch markDone'); dispatch(Action(ToDoListAction.markDone, payload: toDo)); } }, ) ], ), ); } Widget toDoListView( ToDoList state, Dispatch dispatch, ViewService viewService, ) { print('build toDoListView'); return Column( children: [ Expanded( child: ListView.builder( itemBuilder: (context, index) { Todo toDo = state.list[index]; return toDoView(toDo, context, dispatch); }, itemCount: state.list.length, )), Row( children: [ Expanded( child: GestureDetector( child: Container( key: ValueKey('Add'), height: 68.0, color: Colors.green, alignment: AlignmentDirectional.center, child: Text('Add'), ), onTap: () { print('dispatch Add'); dispatch(Action(ToDoListAction.onAdd)); }, )), Expanded( child: GestureDetector( child: Container( key: ValueKey('Error'), height: 68.0, color: Colors.red, alignment: AlignmentDirectional.center, child: Text('Error'), ), onTap: () { print('dispatch KnowException'); dispatch(Action(ToDoListAction.onKnowException)); }, onLongPress: () { print('dispatch UnKnowException'); dispatch(Action(ToDoListAction.onUnKnowException)); })) ], ) ], ); } bool toDoListEffect(Action action, Context ctx) { if (action.type == ToDoListAction.onAdd) { print('onAdd'); ctx.dispatch(Action(ToDoListAction.add, payload: Todo.mock())); return true; } else if (action.type == ToDoListAction.onEdit) { print('onEdit'); assert(action.payload is Todo); Todo toDo = ctx.state.list .firstWhere((i) => i.id == action.payload.id, orElse: () => null); assert(toDo != null); toDo = toDo.clone(); toDo.desc = '${toDo.desc}-effect'; ctx.dispatch(Action(ToDoListAction.edit, payload: toDo)); return true; } else if (action.type == ToDoListAction.onKnowException) { throw KnowException(); } else if (action.type == ToDoListAction.onUnKnowException) { throw UnKnowException(); } return false; } dynamic toDoListEffectAsync(Action action, Context ctx) { if (action.type == ToDoListAction.onAdd || action.type == ToDoListAction.onEdit || action.type == ToDoListAction.onKnowException || action.type == ToDoListAction.onUnKnowException) { return Future.delayed( Duration(seconds: 1), () => toDoListEffect(action, ctx)); } return null; } ToDoList toDoListReducer(ToDoList state, Action action) { print('onReduce:${action.type}'); if (!(action.payload is Todo)) return state; Todo item = action.payload as Todo; if (action.type == ToDoListAction.add) { return state.clone()..list.add(item); } else if (action.type == ToDoListAction.markDone) { return state.clone() ..list .firstWhere((toDo) => toDo.id == item.id, orElse: () => null) ?.isDone = true; } else if (action.type == ToDoListAction.remove) { return state.clone()..list.removeWhere((toDo) => toDo.id == item.id); } else if (action.type == ToDoListAction.edit) { return state.clone() ..list .firstWhere((toDo) => toDo.id == item.id, orElse: () => null) ?.desc = item.desc; } else { return state; } } bool forbidRefreshUI(ToDoList old, ToDoList now) { return false; } bool toDoListErrorHandler(Exception exception, Context ctx) { print('onErr:$exception'); if (exception is KnowException) { return true; } return false; } Composable toDoListMiddleware({ Dispatch dispatch, Get getState, }) => (Dispatch next) => (Action action) { if (action.type == ToDoListAction.middlewareEdit) { assert(action.payload is Todo); Todo toDo = getState().list.firstWhere( (i) => i.id == action.payload.id, orElse: () => null); assert(toDo != null); toDo = toDo.clone(); toDo.desc = '${toDo.desc}-middleware'; dispatch(Action(ToDoListAction.edit, payload: toDo)); } next(action); }; const Map pageInitParams = { 'list': [ { 'id': '0', 'title': 'title-0', 'desc': 'desc-0', 'isDone': false }, { 'id': '1', 'title': 'title-1', 'desc': 'desc-1', 'isDone': false }, { 'id': '2', 'title': 'title-2', 'desc': 'desc-2', 'isDone': false }, { 'id': '3', 'title': 'title-3', 'desc': 'desc-3', 'isDone': true } ] }; ToDoList initState(Map map) => ToDoList.fromMap(map); class PageWrapper extends StatelessWidget { final Widget child; PageWrapper(this.child); @override Widget build(BuildContext context) { return child; } } Widget createPageWidget(BuildContext context) { return TestPage( initState: initState, view: toDoListView, reducer: toDoListReducer, effect: toDoListEffectAsync, // shouldUpdate: forbidRefreshWhenAddOrRemove, // onError: toDoListErrorHandler, middleware: [toDoListMiddleware]).buildPage(pageInitParams); } ================================================ FILE: test/test_widgets/lib/page/state.dart ================================================ import 'package:fish_redux/fish_redux.dart'; class Todo implements Cloneable { String id; String title; String desc; bool isDone = false; Todo(); factory Todo.mock() => Todo() ..id = 'id-mock' ..title = 'title-mock' ..desc = 'desc-mock' ..isDone = false; @override Todo clone() => Todo() ..id = this.id ..title = this.title ..desc = this.desc ..isDone = this.isDone; factory Todo.fromMap(Map map) { return Todo() ..id = map['id'] ?? 'uniq' ..title = map['title'] ?? '' ..desc = map['desc'] ?? '' ..isDone = map['isDone'] ?? false; } @override bool operator ==(dynamic other) { if (!(other is Todo)) return false; return id == other.id && title == other.title && desc == other.desc && isDone == other.isDone; } @override String toString() { return 'Todo{id: $id, title: $title, desc: $desc, isDone: $isDone}'; } } class ToDoList implements Cloneable { List list = []; ToDoList(); @override ToDoList clone() => ToDoList()..list.addAll(this.list); factory ToDoList.fromMap(Map map) { return ToDoList() ..list.addAll( map['list']?.map((Map map) => Todo.fromMap(map))?.toList()); } @override bool operator ==(dynamic other) { if (!(other is ToDoList)) return false; if (list.length != other.list.length) return false; for (int index = 0; index < list.length; index++) { if (list[index] != other.list[index]) return false; } return true; } @override String toString() { return '{list: $list}'; } } ================================================ FILE: test/test_widgets/lib/source_flow_adapter/action.dart ================================================ enum ToDoListAction { onAdd, add, remove, broadcast } enum ToDoAction { onEdit, edit, markDone, onBroadcast, broadcast } enum PageAction { onAdd, } ================================================ FILE: test/test_widgets/lib/source_flow_adapter/adapter.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import 'action.dart'; import 'state.dart'; Widget toDoView(Todo toDo, Dispatch dispatch, ViewService viewService) { return Container( margin: const EdgeInsets.all(8.0), color: Colors.grey, child: Row( children: [ Expanded( child: Container( child: Column( children: [ GestureDetector( child: Container( key: ValueKey('remove-${toDo.id}'), padding: const EdgeInsets.all(8.0), height: 28.0, color: Colors.yellow, child: Text( toDo.title, style: TextStyle(fontSize: 16.0), ), alignment: AlignmentDirectional.centerStart, ), onTap: () { print('dispatch remove'); dispatch( Action(ToDoListAction.remove, payload: toDo.clone())); }, ), GestureDetector( child: Container( key: ValueKey('edit-${toDo.id}'), padding: const EdgeInsets.all(8.0), height: 60.0, color: Colors.grey, child: Text(toDo.desc, style: TextStyle(fontSize: 14.0)), alignment: AlignmentDirectional.centerStart, ), onTap: () { print('dispatch onEdit'); dispatch(Action(ToDoAction.onEdit, payload: toDo.clone())); }, ) ], ), )), GestureDetector( child: Container( key: ValueKey('mark-${toDo.id}'), color: toDo.isDone ? Colors.green : Colors.red, width: 88.0, height: 88.0, child: Text(toDo.isDone ? 'done' : 'mark\ndone'), alignment: AlignmentDirectional.center, ), onTap: () { if (!toDo.isDone) { print('dispatch markDone'); dispatch(Action(ToDoAction.markDone, payload: toDo.clone())); } }, onLongPress: () { print('dispatch broadcast'); dispatch(Action(ToDoAction.onBroadcast)); }, ) ], ), ); } bool toDoEffect(Action action, Context ctx) { if (action.type == ToDoAction.onEdit) { print('onEdit'); Todo toDo = ctx.state.clone(); toDo.desc = '${toDo.desc}-effect'; ctx.dispatch(Action(ToDoAction.edit, payload: toDo)); return true; } else if (action.type == ToDoAction.onBroadcast) { ctx.broadcastEffect(Action(ToDoAction.broadcast)); } else if (action.type == Lifecycle.initState) { //print('!!! initState ${ctx.state}'); return true; } else if (action.type == Lifecycle.dispose) { //print('!!! dispose ${ctx.state}'); return true; } return false; } dynamic toDoEffectAsync(Action action, Context ctx) { if (action.type == ToDoAction.onEdit) { return Future.delayed(Duration(seconds: 1), () => toDoEffect(action, ctx)); } return null; } Dispatch toDoHigherEffect(Context ctx) => (Action action) => toDoEffect(action, ctx); Todo toDoReducer(Todo state, Action action) { if (!(action.payload is Todo) || state.id != action.payload.id) return state; print('onReduce:${action.type}'); if (action.type == ToDoAction.markDone) { return state.clone()..isDone = true; } else if (action.type == ToDoAction.edit) { return state.clone()..desc = action.payload.desc; } else { return state.clone(); } } bool shouldUpdate(Todo old, Todo now) { return old != now; } bool reducerFilter(Todo toDo, Action action) { return action.type == ToDoAction.edit || action.type == ToDoAction.markDone; } ================================================ FILE: test/test_widgets/lib/source_flow_adapter/component.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import 'action.dart'; import 'state.dart'; Widget toDoView(Todo toDo, Dispatch dispatch, ViewService viewService) { return Container( margin: const EdgeInsets.all(8.0), color: Colors.grey, child: Row( children: [ Expanded( child: Container( child: Column( children: [ GestureDetector( child: Container( key: ValueKey('remove-${toDo.id}'), padding: const EdgeInsets.all(8.0), height: 28.0, color: Colors.yellow, child: Text( toDo.title, style: TextStyle(fontSize: 16.0), ), alignment: AlignmentDirectional.centerStart, ), onTap: () { print('dispatch remove'); dispatch( Action(ToDoListAction.remove, payload: toDo.clone())); }, ), GestureDetector( child: Container( key: ValueKey('edit-${toDo.id}'), padding: const EdgeInsets.all(8.0), height: 60.0, color: Colors.grey, child: Text(toDo.desc, style: TextStyle(fontSize: 14.0)), alignment: AlignmentDirectional.centerStart, ), onTap: () { print('dispatch onEdit'); dispatch(Action(ToDoAction.onEdit, payload: toDo.clone())); }, ) ], ), )), GestureDetector( child: Container( key: ValueKey('mark-${toDo.id}'), color: toDo.isDone ? Colors.green : Colors.red, width: 88.0, height: 88.0, child: Text(toDo.isDone ? 'done' : 'mark\ndone'), alignment: AlignmentDirectional.center, ), onTap: () { if (!toDo.isDone) { print('dispatch markDone'); dispatch(Action(ToDoAction.markDone, payload: toDo.clone())); } }, onLongPress: () { print('dispatch broadcast'); dispatch(Action(ToDoAction.onBroadcast)); }, ) ], ), ); } bool toDoEffect(Action action, Context ctx) { if (action.type == ToDoAction.onEdit) { print('onEdit'); Todo toDo = ctx.state.clone(); toDo.desc = '${toDo.desc}-effect'; ctx.dispatch(Action(ToDoAction.edit, payload: toDo)); return true; } else if (action.type == ToDoAction.onBroadcast) { ctx.broadcastEffect(Action(ToDoAction.broadcast)); return true; } else if (action.type == Lifecycle.initState) { //print('!!! initState ${ctx.state}'); return true; } else if (action.type == Lifecycle.dispose) { //print('!!! dispose ${ctx.state}'); return true; } return false; } dynamic toDoEffectAsync(Action action, Context ctx) { if (action.type == ToDoAction.onEdit) { return Future.delayed(Duration(seconds: 1), () => toDoEffect(action, ctx)); } return null; } Dispatch toDoHigherEffect(Context ctx) => (Action action) => toDoEffect(action, ctx); Todo toDoReducer(Todo state, Action action) { if (!(action.payload is Todo) || state.id != action.payload.id) return state; print('onReduce:${action.type}'); if (action.type == ToDoAction.markDone) { return state.clone()..isDone = true; } else if (action.type == ToDoAction.edit) { return state.clone()..desc = action.payload.desc; } else { return state.clone(); } } bool shouldUpdate(Todo old, Todo now) { return old != now; } bool reducerFilter(Todo toDo, Action action) { return action.type == ToDoAction.edit || action.type == ToDoAction.markDone; } class ToDoComponent extends Component { ToDoComponent() : super( view: toDoView, effect: toDoEffect, reducer: toDoReducer, shouldUpdate: shouldUpdate, filter: reducerFilter); } class ComponentWrapper extends StatelessWidget { final Widget child; ComponentWrapper(this.child); @override Widget build(BuildContext context) { return child; } } ================================================ FILE: test/test_widgets/lib/source_flow_adapter/page.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import '../test_base.dart'; import 'action.dart'; import 'source_flow_adapter.dart'; import 'state.dart'; Widget pageView( ToDoList state, Dispatch dispatch, ViewService viewService, ) { print('build pageView'); final ListAdapter listAdapter = viewService.buildAdapter(); return Column( children: [ Expanded( child: ListView.builder( itemBuilder: listAdapter.itemBuilder, itemCount: listAdapter.itemCount)), Row( children: [ Expanded( child: GestureDetector( child: Container( key: ValueKey('Add'), height: 68.0, color: Colors.green, alignment: AlignmentDirectional.center, child: Text('Add'), ), onTap: () { print('dispatch Add'); dispatch(Action(PageAction.onAdd)); }, onLongPress: () { print('dispatch broadcast'); viewService.broadcastEffect(const Action(ToDoAction.broadcast)); }, )), ], ) ], ); } const Map pageInitParams = { 'list': [ { 'id': '0', 'title': 'title-0', 'desc': 'desc-0', 'isDone': false }, { 'id': '1', 'title': 'title-1', 'desc': 'desc-1', 'isDone': false }, { 'id': '2', 'title': 'title-2', 'desc': 'desc-2', 'isDone': false }, { 'id': '3', 'title': 'title-3', 'desc': 'desc-3', 'isDone': true } ] }; bool pageEffect(Action action, Context ctx) { if (action.type == PageAction.onAdd) { print('page onAdd'); ctx.broadcastEffect(Action(ToDoListAction.onAdd, payload: Todo.mock())); return true; } return false; } ToDoList initState(Map map) => ToDoList.fromMap(map); Widget createDynamicAdapterWidget(BuildContext context) { return TestPage( view: pageView, initState: initState, effect: pageEffect, dependencies: Dependencies( adapter: NoneConn() + testAdapter)) .buildPage(pageInitParams); } ================================================ FILE: test/test_widgets/lib/source_flow_adapter/source_flow_adapter.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import '../test_base.dart'; import 'action.dart'; import 'component.dart'; import 'state.dart'; bool toDoListEffect(Action action, Context ctx) { if (action.type == ToDoListAction.onAdd) { print('adapter onAdd'); ctx.dispatch(Action(ToDoListAction.add, payload: Todo.mock())); return true; } return false; } ToDoList toDoListReducer(ToDoList state, Action action) { print('onReduce:${action.type}'); if (!(action.payload is Todo)) return state; if (action.type == ToDoListAction.add) { return state.clone()..list.add(action.payload); } else if (action.type == ToDoListAction.remove) { return state.clone() ..list.removeWhere((Todo toDo) => toDo.id == action.payload.id); } else { return state.clone(); } } final TestSourceFlowAdapter testAdapter = TestSourceFlowAdapter( pool: {'toDo': ToDoComponent()}, reducer: toDoListReducer, effect: toDoListEffect, ); ================================================ FILE: test/test_widgets/lib/source_flow_adapter/state.dart ================================================ import 'package:fish_redux/fish_redux.dart'; class Todo implements Cloneable { String id; String title; String desc; bool isDone = false; Todo(); factory Todo.mock() => Todo() ..id = 'id-mock' ..title = 'title-mock' ..desc = 'desc-mock' ..isDone = false; @override Todo clone() => Todo() ..id = this.id ..title = this.title ..desc = this.desc ..isDone = this.isDone; factory Todo.fromMap(Map map) { return Todo() ..id = map['id'] ?? 'uniq' ..title = map['title'] ?? '' ..desc = map['desc'] ?? '' ..isDone = map['isDone'] ?? false; } @override bool operator ==(dynamic other) { if (!(other is Todo)) return false; return id == other.id && title == other.title && desc == other.desc && isDone == other.isDone; } @override String toString() { return 'Todo{id: $id, title: $title, desc: $desc, isDone: $isDone}'; } } class ToDoList extends ItemListLike implements Cloneable { List list = []; ToDoList(); @override ToDoList clone() => ToDoList()..list.addAll(this.list); factory ToDoList.fromMap(Map map) { return ToDoList() ..list.addAll( map['list']?.map((Map map) => Todo.fromMap(map))?.toList()); } @override bool operator ==(dynamic other) { if (!(other is ToDoList)) return false; if (list.length != other.list.length) return false; for (int index = 0; index < list.length; index++) { if (list[index] != other.list[index]) return false; } return true; } @override String toString() { return '{list: $list}'; } @override Object getItemData(int index) => list[index]; @override String getItemType(int index) => 'item'; @override int get itemCount => list?.length ?? 0; @override ItemListLike updateItemData(int index, Object data, bool isStateCopied) { list[index] = data; return this; } } // connector: ConnOp>( // get: (ToDoList toDoList) => toDoList.list // .map((Todo toDo) => noReducer // ? ItemBean('toDoNoReducer', toDo) // : ItemBean('toDo', toDo)) // .toList(), // set: (ToDoList toDoList, List beans) { // toDoList.list.clear(); // toDoList.list.addAll(beans // .map((ItemBean bean) => bean.data) // .toList()); // }), ================================================ FILE: test/test_widgets/lib/static_flow_adapter/action.dart ================================================ enum ToDoListAction { onAdd, add, remove, } enum ToDoAction { onEdit, edit, markDone, onBroadcast, broadcast } ================================================ FILE: test/test_widgets/lib/static_flow_adapter/component.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import 'action.dart'; import 'state.dart'; Widget toDoView(Todo toDo, Dispatch dispatch, ViewService viewService) { return Container( margin: const EdgeInsets.all(8.0), color: Colors.grey, child: Row( children: [ Expanded( child: Container( child: Column( children: [ GestureDetector( child: Container( key: ValueKey('remove-${toDo.id}'), padding: const EdgeInsets.all(8.0), height: 28.0, color: Colors.yellow, child: Text( toDo.title, style: TextStyle(fontSize: 16.0), ), alignment: AlignmentDirectional.centerStart, ), onTap: () { print('dispatch remove'); dispatch( Action(ToDoListAction.remove, payload: toDo.clone())); }, ), GestureDetector( child: Container( key: ValueKey('edit-${toDo.id}'), padding: const EdgeInsets.all(8.0), height: 60.0, color: Colors.grey, child: Text(toDo.desc, style: TextStyle(fontSize: 14.0)), alignment: AlignmentDirectional.centerStart, ), onTap: () { print('dispatch onEdit'); dispatch(Action(ToDoAction.onEdit, payload: toDo.clone())); }, ) ], ), )), GestureDetector( child: Container( key: ValueKey('mark-${toDo.id}'), color: toDo.isDone ? Colors.green : Colors.red, width: 88.0, height: 88.0, child: Text(toDo.isDone ? 'done' : 'mark\ndone'), alignment: AlignmentDirectional.center, ), onTap: () { if (!toDo.isDone) { print('dispatch markDone'); dispatch(Action(ToDoAction.markDone, payload: toDo.clone())); } }, onLongPress: () { print('dispatch broadcast'); dispatch(Action(ToDoAction.onBroadcast, payload: toDo.clone())); }, ) ], ), ); } bool toDoEffect(Action action, Context ctx) { if (action.type == ToDoAction.onEdit) { print('onEdit'); Todo toDo = ctx.state.clone(); toDo.desc = '${toDo.desc}-effect'; ctx.dispatch(Action(ToDoAction.edit, payload: toDo)); return true; } else if (action.type == ToDoAction.onBroadcast) { ctx.broadcastEffect(Action(ToDoAction.broadcast)); return true; } return false; } dynamic toDoEffectAsync(Action action, Context ctx) { if (action.type == ToDoAction.onEdit) { return Future.delayed(Duration(seconds: 1), () => toDoEffect(action, ctx)); } return null; } Dispatch toDoHigherEffect(Context ctx) => (Action action) => toDoEffect(action, ctx); Todo toDoReducer(Todo state, Action action) { if (!(action.payload is Todo) || state.id != action.payload.id) return state; print('onReduce:${action.type}'); if (action.type == ToDoAction.markDone) { return state.clone()..isDone = true; } else if (action.type == ToDoAction.edit) { return state.clone()..desc = action.payload.desc; } else { return state.clone(); } } bool shouldUpdate(Todo old, Todo now) => old != now; bool reducerFilter(Todo toDo, Action action) { return action.type == ToDoAction.edit || action.type == ToDoAction.markDone; } class ToDoComponent extends Component { ToDoComponent() : super( view: toDoView, effect: toDoEffect, reducer: toDoReducer, shouldUpdate: shouldUpdate, filter: reducerFilter); } ================================================ FILE: test/test_widgets/lib/static_flow_adapter/page.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; import '../test_base.dart'; import 'state.dart'; import 'static_flow_adapter.dart'; Widget pageView( ToDoList state, Dispatch dispatch, ViewService viewService, ) { print('build pageView'); ListAdapter listAdapter = viewService.buildAdapter(); return ListView.builder( itemBuilder: listAdapter.itemBuilder, itemCount: listAdapter.itemCount); } const Map pageInitParams = { 'list': [ { 'id': '0', 'title': 'title-0', 'desc': 'desc-0', 'isDone': false }, { 'id': '1', 'title': 'title-1', 'desc': 'desc-1', 'isDone': false }, { 'id': '2', 'title': 'title-2', 'desc': 'desc-2', 'isDone': false }, { 'id': '3', 'title': 'title-3', 'desc': 'desc-3', 'isDone': true } ] }; ToDoList initState(Map map) => ToDoList.fromMap(map); Widget createStaticAdapterWidget(BuildContext context) { return TestPage( initState: initState, view: pageView, dependencies: Dependencies( adapter: NoneConn() + testAdapter)) .buildPage(pageInitParams); } ================================================ FILE: test/test_widgets/lib/static_flow_adapter/state.dart ================================================ import 'package:fish_redux/fish_redux.dart'; class Todo implements Cloneable { String id; String title; String desc; bool isDone = false; Todo(); factory Todo.mock() => Todo() ..id = 'id-mock' ..title = 'title-mock' ..desc = 'desc-mock' ..isDone = false; @override Todo clone() => Todo() ..id = this.id ..title = this.title ..desc = this.desc ..isDone = this.isDone; factory Todo.fromMap(Map map) { return Todo() ..id = map['id'] ?? 'uniq' ..title = map['title'] ?? '' ..desc = map['desc'] ?? '' ..isDone = map['isDone'] ?? false; } @override bool operator ==(dynamic other) { if (!(other is Todo)) return false; return id == other.id && title == other.title && desc == other.desc && isDone == other.isDone; } @override String toString() { return 'Todo{id: $id, title: $title, desc: $desc, isDone: $isDone}'; } } class ToDoList implements Cloneable { List list = []; ToDoList(); @override ToDoList clone() => ToDoList()..list.addAll(this.list); factory ToDoList.fromMap(Map map) { return ToDoList() ..list.addAll( map['list']?.map((Map map) => Todo.fromMap(map))?.toList()); } @override bool operator ==(dynamic other) { if (!(other is ToDoList)) return false; if (list.length != other.list.length) return false; for (int index = 0; index < list.length; index++) { if (list[index] != other.list[index]) return false; } return true; } @override String toString() { return '{list: $list}'; } } ================================================ FILE: test/test_widgets/lib/static_flow_adapter/static_flow_adapter.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import '../test_base.dart'; import 'action.dart'; import 'component.dart'; import 'state.dart'; bool toDoListEffect(Action action, Context ctx) { if (action.type == ToDoListAction.onAdd) { print('onAdd'); ctx.dispatch(Action(ToDoListAction.add, payload: Todo.mock())); return true; } return false; } dynamic toDoListEffectAsync(Action action, Context ctx) { if (action.type == ToDoListAction.onAdd) { return Future.delayed( Duration(seconds: 1), () => toDoListEffect(action, ctx)); } return null; } Dispatch toDoListHigherEffect(Context ctx) => (Action action) => toDoListEffect(action, ctx); ToDoList toDoListReducer(ToDoList state, Action action) { print('onReduce:${action.type}'); if (!(action.payload is Todo)) return state; if (action.type == ToDoListAction.add) { return state.clone()..list.add(action.payload); } else if (action.type == ToDoListAction.remove) { Todo toDo = state.list.firstWhere((toDo) => toDo?.id == action.payload.id); int index = state.list.indexOf(toDo); return state.clone()..list[index] = null; } else { return state.clone(); } } class ToDoComponent0 extends ToDoComponent {} class ToDoComponent1 extends ToDoComponent {} class ToDoComponent2 extends ToDoComponent {} class ToDoComponent3 extends ToDoComponent {} final testAdapter = TestStaticFlowAdapter(slots: [ ConnOp( get: (toDoList) => toDoList.list[0], set: (toDoList, toDo) => toDoList.list[0] = toDo) + ToDoComponent0(), ConnOp( get: (toDoList) => toDoList.list[1], set: (toDoList, toDo) => toDoList.list[1] = toDo) + ToDoComponent1(), ConnOp( get: (toDoList) => toDoList.list[2], set: (toDoList, toDo) => toDoList.list[2] = toDo) + ToDoComponent2(), ConnOp( get: (toDoList) => toDoList.list[3], set: (toDoList, toDo) => toDoList.list[3] = toDo) + ToDoComponent3() ], reducer: toDoListReducer, effect: toDoListEffect); ================================================ FILE: test/test_widgets/lib/test_base.dart ================================================ import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action, Page; @immutable class TestStub extends StatefulWidget { final Widget testWidget; final String title; const TestStub(this.testWidget, {this.title = 'FlutterTest'}); @override _StubState createState() => _StubState(); } class _StubState extends State { @override Widget build(BuildContext context) { return MaterialApp( title: widget.title, home: Scaffold( appBar: AppBar(title: Text(widget.title)), body: widget.testWidget)); } } class TestPage, P> extends Page { TestPage({ @required InitState initState, List> middleware, @required ViewBuilder view, Reducer reducer, ReducerFilter filter, Effect effect, Dependencies dependencies, ShouldUpdate shouldUpdate, WidgetWrapper wrapper, Key Function(T) key, }) : super( initState: initState, middleware: middleware, view: view, reducer: reducer, filter: filter, effect: effect, dependencies: dependencies, shouldUpdate: shouldUpdate, wrapper: wrapper, key: key, ); } class TestComponent> extends Component { TestComponent({ @required ViewBuilder view, Reducer reducer, ReducerFilter filter, Effect effect, Dependencies dependencies, ShouldUpdate shouldUpdate, WidgetWrapper wrapper, Key Function(T) key, }) : super( view: view, reducer: reducer, filter: filter, effect: effect, dependencies: dependencies, shouldUpdate: shouldUpdate, wrapper: wrapper, key: key); } class TestAdapter> extends Adapter { TestAdapter({ AdapterBuilder adapter, Reducer reducer, Effect effect, ReducerFilter filter, Dependencies dependencies, }) : super( adapter: adapter, reducer: reducer, effect: effect, filter: filter, dependencies: dependencies); } class TestStaticFlowAdapter> extends StaticFlowAdapter { TestStaticFlowAdapter({ @required List> slots, Reducer reducer, Effect effect, ReducerFilter filter, }) : super(slots: slots, reducer: reducer, effect: effect, filter: filter); } class TestDynamicFlowAdapter> extends DynamicFlowAdapter { TestDynamicFlowAdapter({ @required Map> pool, @required ConnOp> connector, ReducerFilter filter, Reducer reducer, Effect effect, }) : super( pool: pool, connector: connector, reducer: reducer, effect: effect, filter: filter); } class TestSourceFlowAdapter extends SourceFlowAdapter { TestSourceFlowAdapter({ @required Map> pool, ReducerFilter filter, Reducer reducer, Effect effect, }) : super( pool: pool, reducer: reducer, effect: effect, filter: filter, ); } ================================================ FILE: test/test_widgets/pubspec.yaml ================================================ name: test_widgets description: widgets for redux test... environment: sdk: '>=2.0.0-dev.28.0 <3.0.0' dependencies: flutter: sdk: flutter fish_redux: path: ../../ dev_dependencies: flutter_driver: sdk: flutter flutter_test: sdk: flutter