From a644e1981fbbfc34d812422f52cfd42eaf46f9eb Mon Sep 17 00:00:00 2001 From: Chen Peng Date: Mon, 22 Apr 2024 09:08:29 +0800 Subject: [PATCH] publish:pub.dev (#134) you_dart 0.0.3 you_flutter 0.0.3 --- bake | 327 +++++----- .../you_dart/example/you_dart_example.dart | 6 - packages/you_dart/lib/src/core.dart | 108 ++++ packages/you_dart/lib/src/state.dart | 594 ++++++++++++++++++ packages/you_dart/lib/src/state_json.dart | 233 +++++++ packages/you_dart/lib/src/you_dart_base.dart | 6 - packages/you_dart/lib/state.dart | 6 + packages/you_dart/lib/state_json.dart | 6 + packages/you_dart/lib/you_dart.dart | 8 - packages/you_dart/pubspec.yaml | 15 +- packages/you_dart/test/core_types_test.dart | 65 ++ .../test/state_json_all_types_test.dart | 117 ++++ .../test/state_json_new_api_test.dart | 338 ++++++++++ packages/you_dart/test/state_store_test.dart | 48 ++ packages/you_dart/test/state_test.dart | 138 ++++ packages/you_dart/test/you_dart_test.dart | 16 - .../state/state_example_1_counter.dart | 26 + .../example/state/state_example_2_store.dart | 51 ++ .../example/state/state_example_3_list.dart | 40 ++ .../example/state/state_example_4_map.dart | 40 ++ .../example/state/state_example_5_set.dart | 40 ++ .../state/state_example_6_complex_data.dart | 106 ++++ .../example/state/state_example_limit_1.dart | 57 ++ .../example/state/state_example_limit_2.dart | 30 + .../you_flutter/lib/src/state_widget.dart | 72 +++ packages/you_flutter/lib/you_state.dart | 7 +- packages/you_flutter/pubspec.yaml | 4 +- packages/you_flutter/test/you_test.dart | 5 - 28 files changed, 2308 insertions(+), 201 deletions(-) delete mode 100644 packages/you_dart/example/you_dart_example.dart create mode 100644 packages/you_dart/lib/src/core.dart create mode 100644 packages/you_dart/lib/src/state.dart create mode 100644 packages/you_dart/lib/src/state_json.dart delete mode 100644 packages/you_dart/lib/src/you_dart_base.dart create mode 100644 packages/you_dart/lib/state.dart create mode 100644 packages/you_dart/lib/state_json.dart delete mode 100644 packages/you_dart/lib/you_dart.dart create mode 100644 packages/you_dart/test/core_types_test.dart create mode 100644 packages/you_dart/test/state_json_all_types_test.dart create mode 100644 packages/you_dart/test/state_json_new_api_test.dart create mode 100644 packages/you_dart/test/state_store_test.dart create mode 100644 packages/you_dart/test/state_test.dart delete mode 100644 packages/you_dart/test/you_dart_test.dart create mode 100644 packages/you_flutter/example/state/state_example_1_counter.dart create mode 100644 packages/you_flutter/example/state/state_example_2_store.dart create mode 100644 packages/you_flutter/example/state/state_example_3_list.dart create mode 100644 packages/you_flutter/example/state/state_example_4_map.dart create mode 100644 packages/you_flutter/example/state/state_example_5_set.dart create mode 100644 packages/you_flutter/example/state/state_example_6_complex_data.dart create mode 100644 packages/you_flutter/example/state/state_example_limit_1.dart create mode 100644 packages/you_flutter/example/state/state_example_limit_2.dart create mode 100644 packages/you_flutter/lib/src/state_widget.dart diff --git a/bake b/bake index ac9568e7..c9570786 100755 --- a/bake +++ b/bake @@ -1,78 +1,119 @@ #!/usr/bin/env bash # On Mac OS, readlink -f doesn't work, so use._real_path get the real path of the file -_real_path() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" ; } -SCRIPT_PATH="$(_real_path "${BASH_SOURCE[0]}")" -SCRIPT_DIR="$(dirname "$SCRIPT_PATH")" -SCRIPT_FILE="$(basename "$SCRIPT_PATH")" +#_real_path() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" ; } +#_ROOT_BAKE_PATH="$(_real_path "${BASH_SOURCE[0]}")" +#_ROOT_DIR="$(dirname "$_ROOT_BAKE_PATH")" +_ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +_ROOT_BAKE_PATH="${_ROOT_DIR}/bake" # 进入脚本所在目录,这样上下文就是本项目了 -cd "$SCRIPT_DIR" || exit 200 +cd "$_ROOT_DIR" || exit 200 # include common script source "packages/you_bake/bake.bash" -########################################## -# 应用的命令脚本 , 公共函数和全局变量 -########################################## - declare -A _pkgs=( - ["bake"]="$SCRIPT_DIR/packages/you_bake" - ["learn_dart"]="$SCRIPT_DIR/notes/learn_dart" - ["you"]="$SCRIPT_DIR" - ["you_note_dart"]="$SCRIPT_DIR/packages/you_note_dart" - ["flutter_web"]="$SCRIPT_DIR/notes/flutter_web" - ["qwik"]="$SCRIPT_DIR/notes/qwik" - ["shell"]="$SCRIPT_DIR/notes/shell" + ["bake"]="$_ROOT_DIR/packages/you_bake" + ["you_dart"]="$_ROOT_DIR/packages/you_dart" + ["you_flutter"]="$_ROOT_DIR/packages/you_flutter" + ["you_note_dart"]="$_ROOT_DIR/packages/you_note_dart" + ["learn_dart"]="$_ROOT_DIR/notes/learn_dart" + ["root"]="$_ROOT_DIR" + ["flutter_web"]="$_ROOT_DIR/notes/flutter_web" + ["qwik"]="$_ROOT_DIR/notes/qwik" + ["shell"]="$_ROOT_DIR/notes/shell" ) -# 运行所用项目的某子命令 -# Usage: _run_all_package -# Example: _run_all_package clean +#################################################### +# app entry script & _root cmd +#################################################### +bake.cmd --cmd ls --desc " mono project manage: ./bake pkgs " +bake.cmd --cmd run --desc " run cmd on all pkg,Usage: ./bake pkgs run [any cmd]" + +bake.cmd --cmd root --desc "$( cat <<-EOF + + ___ _ _ _ _ _ _ +| __|| | _ _ | |_ | |_ ___ _ _ | \| | ___ | |_ ___ +| _| | || || || _|| _|/ -_)| '_| | . |/ _ \| _|/ -_) +|_| |_| \_._| \__| \__|\___||_| |_|\_|\___/ \__|\___| + +https://github.com/chen56/you/flutter_web + +PWD: $PWD + +Usage: + ./bake [cmd] [opts] [args...] + +Examples: + ./bake # same as './bake -h' + ./bake -h # show all commands help + ./bake -h --debug # show all commands help , include internal function + + ./bake flutter dev # == cd notes/flutter_web && flutter run --device-id macos + + ./bake test # test all projects + ./bake build # defalut build == flutter build web --web-renderer html + ./bake preview # defalut preview == start server at web build + ./bake test # test all projects + + ./bake pkgs ls # show all mono project + ./bake pkgs run pwd # run "pwd" on all mono project dir + ./bake pkgs run flutter pub get # run "flutter pub get" on all mono project dir +EOF +)" + +########################################## +# 公共函数区,初始化语句和变量只放脚本头尾 +########################################## +_is_cmd() { + command -v "$1" >/dev/null 2>&1 +} + + +# 在所有项目上运行某命令 +# Usage: _run_all_pkgs +# Example: _run_all_pkgs clean # run => note.clean # mate.clean # flutter_web.clean # ... -_run_all_package() { +_run_all() { local subcmd="$1" if [[ "$subcmd" == "" ]] ; then - echo "缺subcmd参数 Usage:_run_all_package "; + echo "缺subcmd参数 Usage:_run_all "; return 100; fi for pkg in "${!_pkgs[@]}"; do - if _func_exists "$pkg.$subcmd" ; then "$pkg.$subcmd" ; fi + if _is_cmd "$pkg.$subcmd" ; then "$pkg.$subcmd" ; fi done } -# run一条命令,先print上下文信息,再执行 -# Usage: _run -# Example: -# ------------------------------------ -# $ _run pwd -# /Users/x/git/younpc/bake:733 -> bake.go() ▶︎【pwd】 -# /Users/x/git/younpc -# ------------------------------------ +# 先cd到项目目录,再print上下文信息,最后执行 +# Usage: _run +# Example: _run flutter_web pwd _run() { + local pkg=${1:?required project arg,Usage: _run_at_pkg } + shift + local cmd="${1}" + [[ "$cmd" != "" ]] # 如果有cmd,去掉第1个cmd,剩余的是它的参数,如果没有,就类似于在控制台上打了个会车一样任他去吧 + + # 进入工作目录 + local workdir="${_pkgs[$pkg]}" + pushd "$workdir" > /dev/null || { echo "pushd failure"; exit 200; } + local caller_line; caller_line=$(caller 0 | awk '{print $1}') - # home目录替换为"~"符号 - # ${PWD#$HOME}可以删掉home目录前缀, 类似$(echo $PWD | sed "s|^$HOME||"),比sed简单高效,而且sed处理变量总有特殊字符问题 - local current="~${PWD/#$HOME}" - echo "bake:$caller_line -> ${FUNCNAME[1]} ▶︎ 【$current$ $*】" - "$@" - return $? -} -## if function not exist return 1; -# Usage: _func_exists -# Example: _func_exists app.build -# => return 0 -_func_exists(){ - local func="$1" - if ! ( declare -F "$func" | grep "$func" &>/dev/null 2>&1; ) then - return 1; + if ! _is_cmd "$cmd" ; then + echo "$_ROOT_BAKE_PATH:$caller_line ⚪️ ▶︎${FUNCNAME[1]}() ▶︎【$workdir$ $*】" + else + echo "$_ROOT_BAKE_PATH:$caller_line 🔵 ▶︎${FUNCNAME[1]}() ▶︎【$workdir$ $*】" + "$@" fi + # 退出工作目录,不弄脏环境,不需要打印popd执行结果 + popd > /dev/null || { echo "popd failure"; exit 200; } } ########################################## @@ -81,123 +122,107 @@ _func_exists(){ ########################################## -# 根项目,主要是bin/辅助工具等 -you.run() { _run "$@";} -you.install() ( you.run dart pub get;) -you.clean() ( you.run rm -rf build; you.run rm -rf .dart_tool; ) -you.upgrade() ( you.run dart pub upgrade ;) -you.test() ( you.run dart test;) -you.build() ( you.run dart compile exe bin/notecli.dart ;) - -# bash common script lib -bake.run() { _run "$@";} -bake.test() ( packages/you_bake/test.bash test;) - -you_note_dart.run() ( cd packages/you_note_dart || return 200 && _run "$@") -you_note_dart.install() ( you_note_dart.run flutter pub get) -you_note_dart.clean() ( you_note_dart.run flutter clean; rm -rf build;) -you_note_dart.upgrade() ( you_note_dart.run flutter pub upgrade ;) -you_note_dart.test() ( you_note_dart.run flutter test;) - -learn_dart.run() ( cd notes/learn_dart || return 200 && _run "$@") -learn_dart.install() ( learn_dart.run dart pub get) -learn_dart.clean() ( learn_dart.run rm -rf build;learn_dart.run rm -rf .dart_tool;) -learn_dart.upgrade() ( learn_dart.run dart pub upgrade ;) -learn_dart.study() ( learn_dart.run dart test;) +ls() { for pkg in ${!_pkgs[*]} ; do echo "$pkg:${_pkgs[$pkg]}"; done; } +run() { for pkg in ${!_pkgs[*]} ; do _run "$pkg" "$@" ; done } +install() { _run_all install; } +get() { _run_all install; } +build() { _run_all build; } +upgrade() { _run_all upgrade; } +clean() { _run_all clean; } +test() { _run_all test; } +gen() { _run_all gen; } +# 根项目,主要是bin/辅助工具等 +root.run() { _run root "$@"; } +root.install() { _run root dart pub get; } +root.upgrade() { _run root dart pub upgrade ; } +root.test() { _run root dart test; } +root.build() { _run root dart compile exe bin/notecli.dart ; } +root.clean() { _run root rm -rf build; + _run root rm -rf .dart_tool; } +# bash。bash is1 common scri +bake.run() { _run bake "$@"; } +bake.test() { _run bake ./test.bash test; } + + +you_dart.run() { _run you_dart "$@"; } +you_dart.install() { _run you_dart flutter pub get; } +you_dart.clean() { _run you_dart flutter clean; rm -rf build; } +you_dart.upgrade() { _run you_dart flutter pub upgrade ; } +you_dart.test() { _run you_dart flutter test; } + +you_flutter.run() { _run you_flutter "$@"; } +you_flutter.install() { _run you_flutter flutter pub get; } +you_flutter.clean() { _run you_flutter flutter clean; rm -rf build; } +you_flutter.upgrade() { _run you_flutter flutter pub upgrade ; } +you_flutter.test() { _run you_flutter flutter test; } + + +you_note_dart.run() { _run root_note_dart "$@"; } +you_note_dart.install() { _run root_note_dart flutter pub get; } +you_note_dart.clean() { _run root_note_dart flutter clean; rm -rf build; } +you_note_dart.upgrade() { _run root_note_dart flutter pub upgrade ; } +you_note_dart.test() { _run root_note_dart flutter test; } + +learn_dart.run() { _run learn_dart "$@"; } +learn_dart.install() { _run learn_dart dart pub get ; } +learn_dart.clean() { _run learn_dart rm -rf + _run learn_dart rm -rf .dart_tool; } +learn_dart.upgrade() { _run learn_dart dart pub upgrade ; } +learn_dart.study() { _run learn_dart dart test; } # skwasm无法运行 # http-server 不支持base href设置,所以单独build,并设置base-href为"/",而github-pages的base-href必须是repository名 # npx http-server ./flutter_web/build/web --port 8000 # flutter pub global activate dhttpd # run p.flutter_web dhttpd --path ./build/web --port 8080 '--headers=Cross-Origin-Embedder-Policy=credentialless;Cross-Origin-Opener-Policy=same-origin' -flutter_web.run() ( cd notes/flutter_web && _run "$@") -flutter_web.install() ( flutter_web.run flutter pub get) -flutter_web.clean() ( flutter_web.run flutter clean; rm -rf build;) -flutter_web.upgrade() ( flutter_web.run flutter pub upgrade ;) -flutter_web.build_macos() { flutter_web.run flutter build macos -v --release --tree-shake-icons "$@"; } -flutter_web.build() { flutter_web.run flutter build web -v --release --tree-shake-icons --web-renderer html --output build/web/you/flutter_web --base-href "/you/flutter_web/" "$@" ;} -flutter_web.build_web_skwasm() { flutter_web.run flutter build web -v --release --tree-shake-icons --web-renderer skwasm "$@" ; } -flutter_web.build_web_canvaskit() { flutter_web.run flutter build web -v --release --tree-shake-icons --web-renderer canvaskit "$@" ; } -flutter_web.dev() { flutter_web.run flutter run --device-id macos "$@"; } -flutter_web.dev_web() { flutter_web.run flutter run --web-port 8888 --web-renderer html --device-id chrome "$@"; } -flutter_web.preview() { flutter_web.run deno run --allow-env --allow-read --allow-sys --allow-net npm:http-server ./build/web --port 8000 -g --brotli; } -flutter_web.gen() ( dart run bin/notecli.dart gen --dir notes/flutter_web/ ; ) # mate_flutter_web.gen;暂时不用了 - -install() { _run_all_package install; } -get() { _run_all_package install; } -upgrade() { _run_all_package upgrade;} -clean() { _run_all_package clean; } -test() { _run_all_package test; } -gen() { _run_all_package gen; } -ls(){ - for pkg in ${!_pkgs[*]} ; do - echo "$pkg:${_pkgs[$pkg]}" - done -} -run(){ - for pkg in ${!_pkgs[*]} ; do - # 子shell内执行,防止环境感染 - ( cd "${_pkgs[$pkg]}" || exit 200 ; _run "$@" ; ) - done -} +# install: +# - flutter pub global activate dhttpd +# - deno +flutter_web.run() { _run flutter_web "$@" ; } +flutter_web.install() { _run flutter_web flutter pub get ; } +flutter_web.clean() { _run flutter_web flutter clean; + rm -rf build; } +flutter_web.upgrade() { _run flutter_web flutter pub upgrade ; } +flutter_web.gen() { dart run bin/notecli.dart gen --dir notes/flutter_web/; } +flutter_web.dev() { flutter_web.dev_macos "$@"; } +flutter_web.build() { flutter_web.build_html "$@" ; } +flutter_web.preview() { flutter_web.build_html; + flutter_web.preview_html_deno; } + +flutter_web.dev_macos() { _run flutter_web flutter run --device-id macos "$@"; } +flutter_web.dev_html() { _run flutter_web flutter run --web-port 8888 --web-renderer html --device-id chrome "$@"; } +flutter_web.build_macos() { _run flutter_web flutter build macos -v --release --tree-shake-icons "$@"; } +flutter_web.build_wasm() { _run flutter_web flutter build web -v --release --tree-shake-icons --wasm "$@" ;} +flutter_web.build_html() { _run flutter_web flutter build web -v --release --tree-shake-icons --web-renderer html --source-maps --output build/web/you/flutter_web --base-href "/you/flutter_web/" --no-web-resources-cdn "$@" ;} +flutter_web.build_web_skwasm() { _run flutter_web flutter build web -v --release --tree-shake-icons --web-renderer skwasm "$@" ; } +flutter_web.build_web_canvaskit() { _run flutter_web flutter build web -v --release --tree-shake-icons --web-renderer canvaskit "$@" ; } +flutter_web.preview_html_run() { echo "http://localhost:8080/you/flutter_web"; + _run flutter_web dhttpd --path ./build/web --port 8080 '--headers=Cross-Origin-Embedder-Policy=credentialless;Cross-Origin-Opener-Policy=same-origin'; } +flutter_web.preview_html_deno() { echo "http://localhost:8080/you/flutter_web"; + _run flutter_web deno run --allow-env --allow-read --allow-sys --allow-net npm:http-server ./build/web --port 8080 -g --brotli; } +flutter_web.preview_wasm() { echo "http://localhost:8080/you/flutter_web"; + _run flutter_web dhttpd '--headers=Cross-Origin-Embedder-Policy=credentialless;Cross-Origin-Opener-Policy=same-origin'; } + # github 发布时使用,参考[.github/workflows/*.yaml] -docker.build() ( - _run docker build --progress plain --build-arg test=on --tag chen56/you:latest . ; - _run mkdir -p build; - _run sh -c "docker run --rm --workdir /usr/share/nginx/html/you chen56/you tar cf - ./ | ( cd build; tar xf -)"; -) -docker.preview() { _run echo "note preview http://localhost:8888/you/flutter_web"; _run docker run --rm --name you -p 8888:80 -u root:root chen56/you; } -docker.debug() { _run docker run -v "$PWD:/home/flutter/you" --workdir /home/flutter/you --rm -it fischerscode/flutter:3.19.0 bash ; } -docker.exec() { _run docker exec -it --workdir /usr/share/nginx/html/you/ you bash ; } -docker.push() { _run docker image push chen56/you:latest ; } +docker.build() { _run root docker build --progress plain --build-arg test=on --tag chen56/you:latest . ; + _run root mkdir -p build; + _run root sh -c "docker run --rm --workdir /usr/share/nginx/html/you chen56/you tar cf - ./ | ( cd build; tar xf -)"; } +docker.preview() { _run root echo "note preview http://localhost:8888/you/flutter_web"; + _run root docker run --rm --name you -p 8888:80 -u root:root chen56/you; } +docker.debug() { _run root docker run -v "$PWD:/home/flutter/you" --workdir /home/flutter/you --rm -it fischerscode/flutter:3.19.0 bash ; } +docker.exec() { _run root docker exec -it --workdir /usr/share/nginx/html/you/ you bash ; } +docker.push() { _run root docker image push chen56/you:latest ; } + info() { - echo "\$PWD : $PWD" - echo "\bake: bake" - echo "\$SCRIPT_DIR : $SCRIPT_DIR" - echo "\$SCRIPT_FILE: $SCRIPT_FILE" + echo "PWD : $PWD" + echo "_ROOT_DIR : $_ROOT_DIR" + echo + echo "## pkgs" + ls echo } -#################################################### -# app entry script & _root cmd -#################################################### -bake.cmd --cmd ls --desc " mono project manage: ./$SCRIPT_FILE pkgs " -bake.cmd --cmd run --desc " run cmd on all pkg,Usage: ./$SCRIPT_FILE pkgs run [any cmd]" - -bake.cmd --cmd root --desc "$( cat <<-EOF - - ___ _ _ _ _ _ _ -| __|| | _ _ | |_ | |_ ___ _ _ | \| | ___ | |_ ___ -| _| | || || || _|| _|/ -_)| '_| | . |/ _ \| _|/ -_) -|_| |_| \_._| \__| \__|\___||_| |_|\_|\___/ \__|\___| - -https://github.com/chen56/you/flutter_web - -PWD: $PWD - -Usage: - ./bake [cmd] [opts] [args...] - -Examples: - ./bake # same as './bake -h' - ./bake -h # show all commands help - ./bake -h --debug # show all commands help , include internal function - - ./bake flutter dev # == cd notes/flutter_web && flutter run --device-id macos - - ./bake test # test all projects - ./bake build # defalut build == flutter build web --web-renderer html - ./bake preview # defalut preview == start server at web build - ./bake test # test all projects - - ./bake pkgs ls # show all mono project - ./bake pkgs run pwd # run "pwd" on all mono project dir - ./bake pkgs run flutter pub get # run "flutter pub get" on all mono project dir -EOF -)" - -bake.go "$@" - +bake.go "$@" \ No newline at end of file diff --git a/packages/you_dart/example/you_dart_example.dart b/packages/you_dart/example/you_dart_example.dart deleted file mode 100644 index 68d5e975..00000000 --- a/packages/you_dart/example/you_dart_example.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:you_dart/you_dart.dart'; - -void main() { - var awesome = Awesome(); - print('awesome: ${awesome.isAwesome}'); -} diff --git a/packages/you_dart/lib/src/core.dart b/packages/you_dart/lib/src/core.dart new file mode 100644 index 00000000..82ef24ab --- /dev/null +++ b/packages/you_dart/lib/src/core.dart @@ -0,0 +1,108 @@ +import 'package:meta/meta.dart'; + +/// 基础包,不依赖其他业务代码 + +Types types = Types(); + + +@experimental +typedef Convert = TO Function(FROM from); + +@experimental +base mixin class Types { +// 集合的直接类型转换会报错: +// exception : return params.map((e)=>e.build()).toList() as T +// 可以利用原类型集合复制出来做基础,再填充,然后转型就不会错了。 + R castList({required Iterable from, required List to}) { + assert(to is R, "arg to:$to , type should be $R"); +//copy same type list + var result = to.sublist(0, 0); +// or +// var result = to.toList()..clear(); + +// fill + for (var e in from) { + result.add(e); + } + +//cast, no exception ,because : to is R == true + return result as R; + } + + @experimental + R castSet({required Iterable from, required Set to}) { + assert(to is R, "arg to:$to , type should be $R"); +//copy same type list + var result = to.toSet(); +// or +// var result = to.toList()..clear(); + +// fill + for (var e in from) { + result.add(e); + } + +//cast, no exception ,because : to is R == true + return result as R; + } + + @experimental + String simpleName(Type type) { + String str = type.toString(); + int index = str.indexOf("<"); + if (index < 0) return str; + return str.substring(0, index); + } +} + +extension type StringExt(String str) { + String safeSubstring(int start, [int? end]) { + end ??= str.length; + end = end <= str.length ? end : str.length; + try { + return str.substring(start, end); + } catch (e) { + throw Exception("$e, string $this"); + } + } +} + +/// 范型参数只有在类型内才能看见,比如Map Signal内的key, value类型外部是无法看见的, +/// 只能通过钩子传出来 +@experimental +final class TypeHook { + const TypeHook(); + + bool isType() { + return [] is List || [] is List; + } + + bool isSuper() { + return [] is List || [] is List; + } + + /// 判断类型T是否是nullable的: + /// expect(isNullable(), isFalse); + /// expect(isNullable(), isTrue); + bool isNullable() { + return null is T; + } + + bool isTypeOf(obj) { + return obj is T; + } +} + +@experimental +class Unique { + final String name; + Unique(this.name); + + String shortHash(Object? object) { + return object.hashCode.toUnsigned(20).toRadixString(16).padLeft(5, '0'); + } + + @override + String toString() => '[#$name:${shortHash(this)}]'; + +} diff --git a/packages/you_dart/lib/src/state.dart b/packages/you_dart/lib/src/state.dart new file mode 100644 index 00000000..c8fb7e30 --- /dev/null +++ b/packages/you_dart/lib/src/state.dart @@ -0,0 +1,594 @@ +import 'dart:collection'; + +import 'package:collection/collection.dart'; + +// TODO 去除flutter依赖,这玩意应该是个纯dart lib ,比如json工具等 +import 'package:flutter/foundation.dart'; + +import 'package:meta/meta.dart'; +import 'package:you_dart/src/core.dart'; + +_defaultOnSignalDoNothing(Signal signal) {} + +typedef FromJson = Object Function(Object input); + +_defaultOnConnectedImmediatelyClose(SignalSubscription conn) { + conn.cancel(); +} + +/// State是对数据的加强,增加了监听功能 +/// class RootStore extends Store{ +/// final username="".npc.signal(name:"username",at:RootStore); +/// final SubStore subStore=Store().asField(name:"subStore",at:RootStore) +/// } +/// class SubStore extends Store{} +/// TODO 缺少批更新函数 batch(), 可以一次性更新后再notify +/// +extension StoreExtension on T { + /// 在自定义Store时需提供[field] [at],会注册字段 + T signal({String field = "", Type at = Null, String debugLabel = ""}) { + this._debugLabel = debugLabel; + return this; + } +} + +extension ListExtension on List { + /// 在自定义Store时需提供[field] [at],会注册字段 + List signal({String field = "", Type at = Null}) => _List(this); +} + +extension QueueExtension on Queue { + /// 在自定义Store时需提供[field] [at],会注册字段 + Queue signal({String field = "", Type at = Null, String debugLabel = ""}) { + var data = _Queue(this, debugLabel: debugLabel); + return data; + } +} + +extension SetExtension on Set { + /// 在自定义Store时需提供[field] [at],会注册字段 + Set signal({String field = "", Type at = Null, String debugLabel = ""}) { + var data = _Set(this, debugLabel: debugLabel); + return data; + } +} + +extension MapExtension on Map { + /// 在自定义Store时需提供[field] [at],会注册字段 + Map signal({String field = "", Type at = Null, String debugLabel = ""}) { + var data = _Map(this, debugLabel: debugLabel); + return data; + } +} + +extension ValueExtension on T { + /// 在自定义Store时需提供[field] [at],会注册字段 + Value signal({String field = "", Type at = Null, String debugLabel = ""}) { + var data = Value(this, debugLabel: debugLabel); + return data; + } +} + +// 没有代码生成,具备字段反射能力,需要一些手工指定配置 +// 可反射的对象 + +/// Store 是自定义状态类的基类,自定义类型的复杂状态应该继承它。 +/// +/// flutter无法使用mirror库的反射能力,导致很多库的编写成为问题,比如json解析,没代码生成就不好搞。 +/// 而Store类基于简单原则,试图不用代码,尽力完成类似功能。 +/// - [JsonMixin] json 自动生成和填充,不用手工一个个字段设置了 +/// - todo State树层级事件传播 +/// +/// 举例: +/// 一般你可能会这样组织一大堆状态: +/// YouStore{ +/// final theme=ValueState("dark"); +/// final users=_MapState({}); +/// } +/// YouStore state_store.dart=YouStore(); +/// state_store.dart.theme.value="light"; +/// class SomeWidget{ +/// build(Context){ +/// return Watch((context)=>Text("current theme: ${state_store.dart.theme.value}")); +/// } +/// } +/// +/// 但也可以这样: +/// YouStore extends Store with JsonStoreMixin { +/// final theme=ValueState("dark",name="theme",at=YouStore); +/// final users=_MapState({}, name="users",at=YouStore); +/// } +/// +/// 注意到了吗,多了2个参数:name="users",at=UserStore,即告诉Store字段的name和所属,然后就可以这样: +/// +/// YouStore state_store.dart=YouStore(); +/// print(state_store.dart.toJson()); +/// => {"theme":"dark"......} +/// +/// +/// +/// + +final class _Notifier with ChangeNotifier { + void fireChanged() { + notifyListeners(); + } + @override + String toString() { + return "${runtimeType} "; + } +} +class _Watcher { + final ValueChanged onChanged; + final ValueChanged onListen; + + _Watcher({required this.onChanged, required this.onListen}); +} + +/// StateBase 继承关系: +/// Singal +/// - Store +/// - Value +/// - _List +/// - _Set +/// - _Map +abstract base class Signal { + /// name: The field name + /// at: The declared type of this field + /// - if at != Null then: it's a state_store.dart field + /// - if at == Null then: Independent variable, not belong to any Store, + Signal({String debugLabel = ""}) : _debugLabel = debugLabel; + + String _name = ""; + + String _debugLabel; + + // : _data = data, + // assert(data is! Signal, "already asSignal() on $data"); + + // YouState不直接扩展ChangeNotifier的原因是: + // 1. 不想给核心类添加很多客户可见方法,干扰别人代码提示 + // 2. 暴露出的addListener,客户代码就要自己dispose,还没想好如何处理,暂时隐藏实现 + @nonVirtual + final _Notifier _notifier = _Notifier(); + + // TODO _notifier 和_registeredWatchers可以合并!而且notifier内部用List实现的,删除性能貌似不咋地 + // 这些是在read scope时,发现需要观测write事件的watcher,他们需要在生命期结束时来自己removeWatcher,Signal不知道什么时候结束观察 + final Map<_Watcher, SignalSubscription> _subscriptions = {}; + + /// 这个栈类似多重梦境,因为观测者会一层嵌套一层,最上方是当前梦境,即当前Watcher + /// 不用担心这个stack的长度,这是在[track]包裹下瞬间使用的临时stack,用作嵌套的watchRead,瞬间用完,瞬间清理 + /// Multi-layered dreams + static final List<_Watcher> _listenerTempStack = List.empty(growable: true); + + /// 在客户程序Widget.build时,会有读操作,比如 Text("username: ${username.value}") + /// Signal.watchRead在[useSignal]callback执行期间打开一个封闭的瞬时作用域, + /// 期间的Signal读操作会通知[onRead],并用[onChanged]为key注册监听器 + /// + /// * [onChanged] 当watchRead阶段发现有signal的read操作,会把注册[onChanged]注册到Signal的监听池 + /// * [onConnected] 当注册[onChanged]到的监听池时,会调用此方法,并提供一个dispose钩子给外部程序 + /// 外部程序生命周期结束时,可以用dispose移除监听器s + static T track({ + required T Function() useSignal, + ValueChanged onChanged = _defaultOnSignalDoNothing, + ValueChanged onConnected = _defaultOnConnectedImmediatelyClose, + }) { + /// TODO watchRead时应关闭写通知,因为watchRead是在Widget.build期间调用的,写通知会触发Watcher的setState,不符合flutter规范 + _listenerTempStack.add(_Watcher(onChanged: onChanged, onListen: onConnected)); + try { + return useSignal(); + } finally { + _listenerTempStack.removeLast(); + } + } + + @nonVirtual + _fireRead() { + var listener = _listenerTempStack.lastOrNull; + // watcher为空表示未在[tryConnect]范围内执行,比如不关注Signal变化的地方,或命令行工具场景。 + if (listener == null) return; + + if (_subscriptions.containsKey(listener)) { + // alread connected + return; + } + listen() { + listener.onChanged(this); + } + + dispose() { + _subscriptions.remove(listener); + _notifier.removeListener(listen); + } + + // to connect + var subscription = SignalSubscription._(this, dispose); + _subscriptions[listener] = subscription; + _notifier.addListener(listen); + + // 通知调用方 + listener.onListen(subscription); + } + + // @visibleForOverriding + @nonVirtual + _fireChanged() { + _notifier.fireChanged(); + } + + /// 为了解决外部json程序无法获知Signal内部范型的问题,实验了一个类型钩子, + /// 貌似可以解决此问题,目前还没想到其他更简单的方案。 + /// json处理比较复杂,而且和Signal核心逻辑无关,还是把json、yaml等处理放在state.dart外部, + /// 这样就需要一些机制可以拿到Map List的范型类型,比如: + /// Value 返回[TypeHook] + /// _MapSignal 返回[TypeHook,TypeHook] + /// Store 返回[TypeHook] Store自己没范型,返回自己 + /// ......其他类似 + @experimental + List get typeArgs; +} + +/// 本包是无代码生成反射的低级抽象,本质是自行注册字段元数据,而后获得类似反射的能力 +/// [Store]是本基于本包的状态管理工具,使用更容易。 +/// 希望获得反射能力的类,需要按如下方法实现: +/// base class X extends Store{ +/// int i = Field(name:"",at:X,data:1).value; +/// } +/// print(X().fields) +/// base Class modifiers 限定子类只能继承,不能implements +/// ref: +abstract base class Store extends Signal { + Store({super.debugLabel}); + + final _fields = {}; + + Map get fields => UnmodifiableMapView(_fields); + + @visibleForTesting + @protected + T field(String name, T value, {FromJson? fromJson}) { + assert(!_fields.containsKey(name), "name:$name : value:$value"); + var s = value as Signal; + s._name = name; + _fields[name] = s; + return value; + } + + @override + List get typeArgs => [const TypeHook()]; + + @override + String toString() { + return 'type:$runtimeType,name:$_name, debugLabel:$_debugLabel, fields:${fields.toString()}'; + } +} + +@experimental +class FieldBuilder { + final _fields = {}; + + void field(String name, T value, {FromJson? fromJson}) { + assert(!_fields.containsKey(name), "name:$name : value:$value"); + _fields[name] = value as Signal; + } + + Map get fields => _fields; + + static void build(Store store, void Function(FieldBuilder) updates) { + FieldBuilder builder = FieldBuilder(); + updates(builder); + for (var MapEntry(key: key, value: value) in builder._fields.entries) { + store.field(key, value); + } + } +} + + +@experimental +final class SignalSubscription { + final Signal signal; + + final VoidCallback dispose; + + SignalSubscription._(this.signal, VoidCallback close) : dispose = close; + + /// 取消订阅Signal事件 + void cancel() { + dispose(); + } +} + +/// ref: ObserverList +final class Value extends Signal { + Value(T data, {super.debugLabel}) : _data = data; + T _data; + + @useResult + T get value { + _fireRead(); + return _data; + } + + set value(T newValue) { + if (_data == newValue) return; + _data = newValue; + _fireChanged(); + } + + /// 更短的用法 + @useResult + T call() { + return value; + } + + T peek() { + return _data; + } + + @override + List get typeArgs => [TypeHook()]; + + @override + String toString() { + return 'type:Value<$T>,name:$_name, debugLabel:$_debugLabel, value:$value'; + } +} + +/// ref: [DelegatingList] [ListMixin] +final class _List extends _Iterable with ListMixin { + // 复制一份data,要不然外面可能是const集合 + _List(List data, {super.debugLabel}) : _data = List.from(data); + final List _data; + + @override + List get typeArgs => [TypeHook()]; + + // ############################################################# + // ## 下面为ListMixin的实现 + // ############################################################# + + @override + int get length { + _fireRead(); + return _data.length; + } + + @override + operator [](int index) { + _fireRead(); + return _data[index]; + } + + @override + set length(int newLength) { + _data.length = newLength; + _fireChanged(); + } + + @override + void operator []=(int index, value) { + _data[index] = value; + _fireChanged(); + } + + @override + void add(E value) { + _data.add(value); + _fireChanged(); + } + + @override + void addAll(Iterable iterable) { + _data.addAll(iterable); + _fireChanged(); + } +} + +final class _Queue extends _Iterable with ListMixin implements Queue { + // 复制一份data,要不然外面可能是只读集合 + _Queue(Queue data, {super.debugLabel}) : _data = QueueList.from(data); + final QueueList _data; + + @override + List get typeArgs => [TypeHook()]; + + @override + String toString() { + return super.toSignalString(); + } + // ############################################################# + // ## 下面为ListMixin的实现 + // ############################################################# + @override + QueueList cast() { + _fireRead(); + return _data.cast(); + } + + @override + int get length { + _fireRead(); + return _data.length; + } + + @override + operator [](int index) { + _fireRead(); + return _data[index]; + } + + @override + set length(int newLength) { + _data.length = newLength; + _fireChanged(); + } + + @override + void operator []=(int index, value) { + _data[index] = value; + _fireChanged(); + } + + @override + void add(E value) { + _data.add(value); + _fireChanged(); + } + + @override + void addAll(Iterable iterable) { + _data.addAll(iterable); + _fireChanged(); + } + + // ############################################################# + // ## 下面是Queue的实现 + // ############################################################# + + @override + void addFirst(E element) { + _data.addFirst(element); + _fireChanged(); + } + + @override + void addLast(E element) { + _data.addLast(element); + _fireChanged(); + } + + @override + E removeFirst() { + E result = _data.removeFirst(); + _fireChanged(); + return result; + } +} + +/// ref: [DelegatingList] [ListMixin] +/// key没限定为String的理由:int key 比 String key 取值快10倍! +final class _Map extends Signal with MapMixin { + // 复制一份data,要不然外面可能是const集合 + _Map(Map data, {super.debugLabel}) : _data = Map.from(data); + final Map _data; + + @override + List get typeArgs => [TypeHook(), TypeHook()]; + + @override + String toString() { + return 'type:$runtimeType, name:$_name, debugLabel:$_debugLabel, length:$length'; + } + + // ############################################################# + // ## 下面为MapMixin的实现 + // ############################################################# + + @override + V? operator [](Object? key) { + _fireRead(); + return _data[key]; + } + + @override + void operator []=(K key, V value) { + if (_data[key] == value) return; + _data[key] = value; + _fireChanged(); + } + + @override + void clear() { + _data.clear(); + _fireChanged(); + } + + @override + Iterable get keys { + _fireRead(); + return _data.keys; + } + + @override + V? remove(Object? key) { + var result = _data.remove(key); + if (result != null) _fireChanged(); + return result; + } +} + +abstract base class _Iterable extends Signal implements Iterable { + _Iterable({super.debugLabel}); + + @override + String toString() { + return toSignalString(); + } + String toSignalString() { + return 'type:$runtimeType, name:$_name, debugLabel:$_debugLabel, length:$length'; + } + +} + +final class _Set extends _Iterable with SetMixin { + // 复制一份data,要不然外面可能是const集合 + _Set(Set data, {super.debugLabel}) : _data = Set.from(data); + final Set _data; + + @override + List get typeArgs => [TypeHook()]; + + // ############################################################# + // ## 下面为SetMixin的实现 + // ############################################################# + + @override + bool add(E value) { + var result = _data.add(value); + if (result) _fireChanged(); + return result; + } + + @override + bool contains(Object? element) { + _fireRead(); + return _data.contains(element); + } + + @override + Iterator get iterator { + _fireRead(); + return _data.iterator; + } + + @override + int get length { + _fireRead(); + return _data.length; + } + + @override + E? lookup(Object? element) { + _fireRead(); + return _data.lookup(element); + } + + @override + bool remove(Object? value) { + var result = _data.remove(value); + if (result) _fireChanged(); + return result; + } + + @override + Set toSet() { + _fireRead(); + return _data.toSet(); + } + + @override + void clear() { + _fireChanged(); + _data.clear(); + } +} diff --git a/packages/you_dart/lib/src/state_json.dart b/packages/you_dart/lib/src/state_json.dart new file mode 100644 index 00000000..91d60385 --- /dev/null +++ b/packages/you_dart/lib/src/state_json.dart @@ -0,0 +1,233 @@ +import 'dart:collection'; +import 'dart:convert'; + +import 'package:meta/meta.dart'; +import 'package:you_dart/src/core.dart'; +import 'package:you_dart/src/state.dart'; + +/// [Store] 的编解码器,为[Store]对象赋予json等的能力, 目前支持 +/// - [✅]json +/// - []yaml +/// +/// 这里可能会有各类支持,比如yaml等 + +/// [jsonDecode] 的第二个参数reviver真的有人在用吗?多层级解析完全搞不懂。 +@experimental +RESULT jsonDecodeBetter(String source, {required RESULT Function(JSON source)? fromJson}) { + Object jsonObject = jsonDecode(source); + if (fromJson == null) { + return jsonObject as RESULT; + } + return fromJson(jsonObject as JSON); +} + +/// json map 对 Map +/// json array 对 List +class JsonConverts { + late final List _converts; + + // Jsons({required List jsons}) : _jsons = [...jsons]; + + JsonConverts([List? converts]) { + _converts = converts ?? []; + + // default supported type + _converts.add( + JsonConvert(toJson: (o) => o.toIso8601String(), toObj: (j) => DateTime.parse(j)), + ); + } + + T loadJsonString(String source, T to) { + var from = jsonDecode(source); + loadJson(from, to); + return to; + } + + T loadJson(Map from, T to) { + for (var MapEntry(key: fieldName, value: nextTo) in to.fields.entries) { + // 字段未发现数据则跳过 + if (!from.containsKey(fieldName)) { + continue; + } + var nextFrom = from[fieldName]; + _loadField(to, fieldName, nextFrom, nextTo); + } + return to; + } + + void _loadField(Store parent, String fieldName, Object? from, Signal to) { + loadCollection(void Function(Object) addCallback) { + if (from is! List) { + return; + } + if (from.isEmpty) { + return; + } + var elementType = to.typeArgs[0]; + for (var element in from) { + //有转换器,先转再塞集合 + JsonConvert? json = _findToObj(element, elementType); + if (json != null) { + addCallback(json.toObj(element)); + return; + } + + // 托底方案,没有转换器就直接塞集合,但事先匹配类型,因为json谁知道哪里来的合不合法 + if (elementType.isTypeOf(element)) { + addCallback(element); + } + } + } + + switch (to) { + case Store(fields: var fields): + if (from is! Map) { + return; + } + + for (var MapEntry(key: fieldName, value: nextTo) in fields.entries) { + // 字段未发现数据则跳过 + if (!from.containsKey(fieldName)) { + continue; + } + var nextFrom = from[fieldName]; + _loadField(to, fieldName, nextFrom, nextTo); + } + case Value to: + var elementType = to.typeArgs[0]; + // 数据为空时: + // - 字段能放nullable值的,才能设null + if (from == null) { + if (elementType.isNullable()) { + to.value = null; + } + return; + } + // 有转换器先转再塞 + JsonConvert? json = _findToObj(from, elementType); + if (json != null) { + to.value = json.toObj(from); + return; + } + + // 托底方案,没有转换器就直接塞,但事先匹配类型,因为json谁知道哪里来的合不合法 + if (elementType.isTypeOf(from)) { + to.value = from; + return; + } + + assert(false, + "loadJson未处理, 请检查是没注册转换器还是json源有问题: fromJson【(jsonType:${from.runtimeType}) , jsonValue:$from】, load to field【storeType:${parent.runtimeType} fieldType:${to.runtimeType} fieldName: $fieldName fieldValue:$to】"); + case List to: + loadCollection((element) => to.add(element)); + case Set to: + loadCollection((element) => to.add(element)); + case Queue to: + loadCollection((element) => to.add(element)); + case Map to: + if (from is! Map || from.isEmpty) { + return; + } + var [keyType, valueType] = (to as Signal).typeArgs; + for (var MapEntry(key: key, value: value) in from.entries) { + JsonConvert? keyConvert = _findToObj(key, keyType); + JsonConvert? valueConvert = _findToObj(value, valueType); + var convertedKey = keyConvert == null ? key : keyConvert.toObj(key); + var convertedValue = valueConvert == null ? value : valueConvert.toObj(value); + + // 匹配类型,因为json谁知道哪里来的合不合法 + if (keyType.isTypeOf(convertedKey) && valueType.isTypeOf(convertedValue)) { + to[convertedKey] = convertedValue; + } + } + case _: + assert(false, + "内部bug,loadJson未处理: fromJson【(jsonType:${from.runtimeType}) , jsonValue:$from】, load to field【storeType:${parent.runtimeType} fieldType:${to.runtimeType} fieldName: $fieldName fieldValue:$to】"); + } + } + + JsonConvert? _findToObj(Object from, TypeHook toType) { + for (var json in _converts) { + if (json.isCanToObj(from, toType)) { + return json; + } + } + return null; + } + JsonConvert? _findToJson(Object from) { + for (var json in _converts) { + if (json.isCanToJson(from)) { + return json; + } + } + return null; + } + + String toJsonString(Store from, {JsonEncoder encoder = const JsonEncoder()}) { + return encoder.convert(_deepToJson(from)); + } + + Map toJson(Store from) { + return _deepToJson(from) as Map; + } + + Object? _deepToJson(Object? input) { + if (input == null) { + return null; + } + if (input is List) { + return input.map((e) => _deepToJson(e)).toList(); + } + if (input is Set) { + return input.map((e) => _deepToJson(e)).toList(); + } + if (input is Map) { + return input.map((k, v) => MapEntry(_deepToJson(k), _deepToJson(v))); + } + if (input is Queue) { + return input.map((e) => _deepToJson(e)).toList(); + } + if (input is Value) { + if (input.value==null) { + return null; + } + JsonConvert? json = _findToJson(input.value); + if (json != null) { + return json.toJson(input.value); + } + return _deepToJson(input.value); + } + if (input is Store) { + return input.fields.map((name, field) { + return MapEntry(name, _deepToJson(field)); + }); + } + return input; + } +} + +class JsonConvert { + JsonConvert({required Convert toJson, required Convert toObj}) + : _toJson = toJson, + _toObj = toObj; + + final Convert _toJson; + + final Convert _toObj; + + toJson(OBJ from) { + return _toJson(from); + } + + toObj(JSON from) { + return _toObj(from); + } + + bool isCanToObj(Object from, TypeHook toType) { + return from is JSON && toType.isType(); + } + + bool isCanToJson(Object from) { + return from is OBJ; + } +} diff --git a/packages/you_dart/lib/src/you_dart_base.dart b/packages/you_dart/lib/src/you_dart_base.dart deleted file mode 100644 index e8a6f159..00000000 --- a/packages/you_dart/lib/src/you_dart_base.dart +++ /dev/null @@ -1,6 +0,0 @@ -// TODO: Put public facing types in this file. - -/// Checks if you are awesome. Spoiler: you are. -class Awesome { - bool get isAwesome => true; -} diff --git a/packages/you_dart/lib/state.dart b/packages/you_dart/lib/state.dart new file mode 100644 index 00000000..7b5322b7 --- /dev/null +++ b/packages/you_dart/lib/state.dart @@ -0,0 +1,6 @@ +/// Support for doing something awesome. +/// +/// More dartdocs go here. +library; + +export 'src/state.dart' show Signal, SignalSubscription,StoreExtension,ListExtension,SetExtension,QueueExtension,MapExtension,ValueExtension,Value; diff --git a/packages/you_dart/lib/state_json.dart b/packages/you_dart/lib/state_json.dart new file mode 100644 index 00000000..6d537706 --- /dev/null +++ b/packages/you_dart/lib/state_json.dart @@ -0,0 +1,6 @@ +/// Support for doing something awesome. +/// +/// More dartdocs go here. +library; + +export 'src/state_json.dart' show JsonConverts, JsonConvert; diff --git a/packages/you_dart/lib/you_dart.dart b/packages/you_dart/lib/you_dart.dart deleted file mode 100644 index 314b07f6..00000000 --- a/packages/you_dart/lib/you_dart.dart +++ /dev/null @@ -1,8 +0,0 @@ -/// Support for doing something awesome. -/// -/// More dartdocs go here. -library; - -export 'src/you_dart_base.dart'; - -// TODO: Export any libraries intended for clients of this package. diff --git a/packages/you_dart/pubspec.yaml b/packages/you_dart/pubspec.yaml index d35e0815..566bd4bc 100644 --- a/packages/you_dart/pubspec.yaml +++ b/packages/you_dart/pubspec.yaml @@ -1,6 +1,6 @@ name: you_dart description: "dart lib, for data basic tools such as json、yaml serialization ." -version: 0.0.2 +version: 0.0.3 homepage: https://github.com/chen56/you repository: https://github.com/chen56/you @@ -9,8 +9,19 @@ environment: # Add regular dependencies here. dependencies: - # path: ^1.8.0 + flutter: + sdk: flutter + path: ^1.8.3 + file: ^7.0.0 + collection: ^1.18.0 + meta: ^1.11.0 + intl: + args: ^2.4.2 dev_dependencies: lints: ^3.0.0 test: ^1.24.0 + flutter_test: + sdk: flutter + + checks: ^0.3.0 \ No newline at end of file diff --git a/packages/you_dart/test/core_types_test.dart b/packages/you_dart/test/core_types_test.dart new file mode 100644 index 00000000..4f38a39b --- /dev/null +++ b/packages/you_dart/test/core_types_test.dart @@ -0,0 +1,65 @@ +import 'package:checks/checks.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:you_dart/src/core.dart'; + +void main() { + group("types.isType", () { + test('TypeHook can const 对比一下常量对类型参数的影响', () { + check(identical(const TypeHook(), const TypeHook())).equals(true); + check(identical(const TypeHook(), const TypeHook())).equals(true); + check(identical(const TypeHook(), const TypeHook())).equals(false); + check(identical(const TypeHook(), const TypeHook())).equals(false); + }); + + test('isType 普通对象 ', () { + check(const TypeHook().isType()).equals(true); + check(const TypeHook().isType()).equals(true); + check(const TypeHook().isType()).equals(false); + }); + + test('isType 数字原生对象', () { + // ok + check(const TypeHook().isType()).equals(true); + check(const TypeHook().isType()).equals(true); + check(const TypeHook().isType()).equals(true); + check(const TypeHook().isType()).equals(true); + + // 反过来不行 + check(const TypeHook().isType()).equals(false); + check(const TypeHook().isType()).equals(false); + check(const TypeHook().isType()).equals(false); + + // 其他类型当然更不行 + check(const TypeHook().isType()).equals(false); + check(const TypeHook().isType()).equals(false); + + // ⚠️特别注意 类型比较时,nullable 被忽视了 + check(const TypeHook().isType()).equals(true); + check(const TypeHook().isType()).equals(true); + + + }); + }); + test('isNullable ', () { + check(const TypeHook().isNullable()).equals(false); + check(const TypeHook().isNullable()).equals(true); + }); + + group("type name", () { + test('is string ', () { + check(types.simpleName(List)).equals("List"); + check(types.simpleName(String)).equals("String"); + }); + }); + + group("Uniquely", () { + test('equals ', () { + var a= Unique("a"); + var b= Unique("a"); + check(a== a).equals(true); + check(a!= b).equals(true); + }); + }); + + +} diff --git a/packages/you_dart/test/state_json_all_types_test.dart b/packages/you_dart/test/state_json_all_types_test.dart new file mode 100644 index 00000000..ebad2bcd --- /dev/null +++ b/packages/you_dart/test/state_json_all_types_test.dart @@ -0,0 +1,117 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. 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:checks/checks.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:you_dart/src/state_json.dart'; +import 'package:you_dart/src/state.dart'; + +JsonConverts jsons = JsonConverts([]); + +void main() { + group("String", () { + test('string', () { + T.string("value").checkCurrent('{"field":"value"}'); + T.string("").checkCurrent('{"field":""}'); + }); + test('string?', () { + T.stringNull(null).checkCurrent('{"field":null}'); + T.stringNull("value").checkCurrent('{"field":"value"}'); + }); + test("load", () { + // String + T.string("value").checkLoad('{"field":"value"}', '{"field":"value"}'); + + T.string("").checkLoad("{}", '{"field":""}'); + T.string("no override").checkLoad("{}", '{"field":"no override"}'); + T.string("").checkLoad('{"field":null}', '{"field":""}'); + + // String? + T.stringNull("value").checkLoad('{"field":"value"}', '{"field":"value"}'); + + T.stringNull().checkLoad("{}", '{"field":null}'); + // TODO 这块逻辑没处理,无值不覆盖 + // T.stringNull("no override,无值不覆盖").checkReJson("{}", '{"field":"no override,无值不覆盖"}'); + T.stringNull("override,有值,哪怕null也覆盖").checkLoad('{"field":null}', '{"field":null}'); + }); + }); + group("DateTime 自定义类型", () { + test('DateTime', () { + var store = T.dateTime(); + store.checkCurrent('{"field":"0000-01-01T00:00:00.000Z"}'); + + store.testField.value = DateTime.utc(2000); + + store.checkCurrent('{"field":"2000-01-01T00:00:00.000Z"}'); + }); + test('DateTime?', () { + T.dateTimeNull(null).checkCurrent('{"field":null}'); + T.dateTimeNull(DateTime.utc(2000)).checkCurrent('{"field":"2000-01-01T00:00:00.000Z"}'); + }); + test("load", () { + // not null + T.dateTime().checkLoad('{"field":"2000-01-01T00:00:00.000Z"}', '{"field":"2000-01-01T00:00:00.000Z"}'); + + T.dateTime(DateTime.utc(2000)).checkLoad("{}", '{"field":"2000-01-01T00:00:00.000Z"}'); + T.dateTime(DateTime.utc(2000)).checkLoad('{"field":null}', '{"field":"2000-01-01T00:00:00.000Z"}'); + + // nullable + T.dateTime(DateTime.utc(2000)).checkLoad('{"field":"2000-01-01T00:00:00.000Z"}', '{"field":"2000-01-01T00:00:00.000Z"}'); + + T.dateTimeNull().checkLoad("{}", '{"field":null}'); + // TODO 这块逻辑没处理,无值不覆盖 + // T.dateTimeNull(DateTime.utc(2000)).checkLoad("{}", '{"field":"2000-01-01T00:00:00.000Z"}'); + T.dateTimeNull().checkLoad('{"field":null}', '{"field":null}'); + }); + }); + group("list", () { + test('list', () { + var store = T.list(["0", "1"]); + store.checkCurrent('{"field":["0","1"]}'); + + store.testField.addAll(["3"]); + store.checkCurrent('{"field":["0","1","3"]}'); + }); + test("load", () { + T.list([]).checkLoad("{}", '{"field":[]}'); + T.list([]).checkLoad('{"field":null}', '{"field":[]}'); + }); + }); +} + +/// Test Store +final class T extends Store { + T(void Function(FieldBuilder) updates) { + FieldBuilder.build(this, updates); + } + + F get testField => fields["field"] as F; + + void checkCurrent(String expected) { + check(jsons.toJsonString(this)).equals(expected); + } + + void checkLoad(String jsonInput, String expected) { + jsons.loadJsonString(jsonInput, this); + check(jsons.toJsonString(this)).equals(expected); + } + + static T> string([String init = ""]) => T((b) => b.field("field", init.signal())); + + static T> stringNull([String? init]) => T((b) => b.field("field", init.signal())); + + static T> integer([int init = 0]) => T((b) => b.field("field", init.signal())); + + static T> integerNull([int? init]) => T((b) => b.field("field", init.signal())); + + static T> dateTime([DateTime? init]) => T((b) => b.field("field", (init ?? DateTime.utc(0)).signal())); + + static T> dateTimeNull([DateTime? init]) => T((b) => b.field("field", init.signal())); + + static T> list(List init) => T((b) => b.field("field", init.signal())); +} diff --git a/packages/you_dart/test/state_json_new_api_test.dart b/packages/you_dart/test/state_json_new_api_test.dart new file mode 100644 index 00000000..5dfa88c3 --- /dev/null +++ b/packages/you_dart/test/state_json_new_api_test.dart @@ -0,0 +1,338 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. 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 'dart:collection'; +import 'dart:convert'; + +import 'package:checks/checks.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:you_dart/src/state_json.dart'; +import 'package:you_dart/src/state.dart'; + +JsonConverts jsons = JsonConverts([ + /// 标注好Json范型,省事很多,减少dynamic/Object类型 + JsonConvert, RootStore>(toJson: (o) => o.toJson(), toObj: (j) => jsons.loadJson(j, RootStore())), + JsonConvert, SubStore>(toJson: (o) => o.toJson(), toObj: (j) => jsons.loadJson(j, SubStore())), + JsonConvert(toJson: (o) => o.name, toObj: (j) => NormalObject(j)), + JsonConvert(toJson: (o) => o.toJson(), toObj: (j) => TestEnum.fromJson(j)), +]); + +void main() { + group("basic", () { + test('toJson&loadJson', () { + { + RootStore testData = RootStore.testData(); + String source = testData.toJsonString(); + RootStore loaded = jsons.loadJsonString(source, RootStore()); + // debugPrint("sssssss1:${testData.toJsonString(encoder: const JsonEncoder.withIndent(" "))}"); + // debugPrint("sssssss2:${loaded.toJsonString(encoder: const JsonEncoder.withIndent(" "))}"); + + check(loaded.toJsonString()).equals(testData.toJsonString()); + + // ############################ + // # 简单场景:内置数据类型 + // ############################ + + /// value + check(loaded.str()).equals("str"); + check(loaded.int_()).equals(1); + check(loaded.dateTime.toString()).equals(testData.dateTime.toString()); + + /// collection + check(loaded.list.toList()).deepEquals(["list.0", "list.1"]); + check(loaded.set.toList()).deepEquals(["set.0", "set.1"]); + check(loaded.queue.toList()).deepEquals(["queue.0", "queue.1"]); + check(loaded.map).deepEquals({"map.0": "value0", "map.1": "value1"}); + + // sub store + check(loaded.subStore.str()).equals("subStore.str"); + check(loaded.subStore.int_()).equals(1); + check(loaded.subStore.list.toList()).deepEquals(["subStore.list.0", "subStore.list.1"]); + check(loaded.subStore.set.toList()).deepEquals(["subStore.set.0", "subStore.set.1"]); + check(loaded.subStore.queue.toList()).deepEquals(["subStore.queue.0", "subStore.queue.1"]); + check(loaded.subStore.map).deepEquals({"subStore.map.0": "value0", "subStore.map.1": "value1"}); + check(loaded.subStore.object.value.name).equals("subStore.object.name"); + + // ############################ + // # 复杂场景:需指定loader的套娃 + // ############################ + + // value signal: 自定义类型 + check(loaded.object.value.name).equals("object.name"); + + // collection signal: collection套娃 + check(loaded.storeList.first.str()).equals("storeList.0.str"); + check(loaded.storeSet.first.str()).equals("storeSet.0.str"); + check(loaded.storeQueue.first.str()).equals("storeQueue.0.str"); + check(loaded.storeMap.keys).deepEquals(["storeMap.0"]); + check(loaded.storeMap.values.first.str.value).equals("storeMap.0.str"); + } + }); + }); + + group("better api design", () { + test('jsonDecodeBetter:一对一,集合对集合,map对map,简单点', () { + var source = '["a","b"]'; + var x = jsonDecodeBetter('["a","b"]', fromJson: (List source) { + return source.map((e) => TestEnum.fromJson(e)).toList(); + }); + check(jsonEncode(x)).equals(source); + }); + test('jsonDecodeBetter:一对一,集合对集合,map对map,简单点', () { + var source = '{"deep":{"enums":["a","b"]},"other":2}'; + var x = jsonDecodeBetter(source, fromJson: (Map source) { + var deep = source["deep"]; + var list = deep["enums"]; + deep["enums"] = list.map((e) => TestEnum.fromJson(e)).toList(); + return source; + }); + check(jsonEncode(x)).equals(source); + }); + test('单独的store', () { + // 单独的store + var source = SubStore(str: "str").toJsonString(); + var x = jsonDecodeBetter(source, fromJson: (Map source) { + return SubStore.fromJson(source); + }); + check(x.toJsonString()).equals(source); + }); + // TODO 这些嵌套Store的case 如何处理? + // test('普通集合包Store', () { + // // 普通集合包Store + // var source = [SubStore(str: "str")].toJsonString(); + // var x = jsonDecodeBetter(source, fromJson: (List source) { + // return source.map((e) => SubStore.fromJson(e)); + // }); + // check(x.toList().toJsonString()).equals(source); + // }); + // test('Signal集合包Store', () { + // // Signal集合包Store + // var source = [SubStore(str: "str")].toJsonString(); + // var rebuildSignal = jsonDecodeBetter(source, fromJson: (List source) { + // return source.map((e) => SubStore.fromJson(e)).toList().signal(); + // }); + // check(rebuildSignal.toJsonString()).equals(source); + // }); + }); +} + +class FieldBuilder { + final _fields = {}; //{}; + void field(String name, Signal value, {FromJson? fromJson}) { + assert(!_fields.containsKey(name), "name:$name : value:$value"); + _fields[name] = value; + } +} + +base class TestStoreBase extends Store { + String toJsonString({JsonEncoder encoder = const JsonEncoder()}) { + return jsons.toJsonString(this, encoder: encoder); + } + + Map toJson() { + return jsons.toJson(this); + } + + T loadJson(dynamic from) { + jsons.loadJson(from, this); + return this as T; + } +} + +base class RootStore extends TestStoreBase { + RootStore({ + String str = "", + int int_ = 0, + DateTime? dateTime, + List list = const [], + Set set = const {}, + Queue? queue, + Map map = const {}, + SubStore? subStore, + NormalObject object = const NormalObject(""), + List storeList = const [], + Set storeSet = const {}, + Queue? storeQueue, + Map storeMap = const {}, + }) { + // ############################ + // # 简单场景:内置数据类型 + // ############################ + + // value signal: 内置支持的数据类型 + this.str = field("str", str.signal()); + this.int_ = field("int_", int_.signal()); + this.dateTime = field("dataTime", (dateTime ?? DateTime(0)).signal()); + + // collection signal: 元素为内置数据类型 + this.list = field("list", list.signal()); + this.set = field("set", set.signal()); + this.queue = field("queue", (queue ?? Queue()).signal()); + this.map = field("map", map.signal()); + + // store signal: store 套 store, 最简单的内置支持结构,不需要loader + this.subStore = field("subStore", (subStore ?? SubStore()).signal()); + + // ############################ + // # 复杂场景:需指定loader的套娃 + // ############################ + + // value signal: 自定义类型,要自己取toJson和Load,常见为枚举型,或简单数据结构Point,Size等 + this.object = field("object", object.signal()); + + // collection signal: collection套娃, collection套Signal、套自定义类型,要自己取toJson和Load + this.storeSet = field("storeSet", storeSet.signal()); + this.storeList = field("storeList", storeList.signal()); + this.storeQueue = field("storeQueue", (storeQueue ?? Queue()).signal()); + this.storeMap = field("storeMap", storeMap.signal()); + } + + RootStore.testData() + : this( + //#################### + // 简单类型 + //#################### + + //简单类型: value + str: "str", + int_: 1, + dateTime: DateTime(2000), + // 简单类型: collection + list: ["list.0", "list.1"], + set: {"set.0", "set.1"}, + queue: Queue.from(["queue.0", "queue.1"]), + map: {"map.0": "value0", "map.1": "value1"}, + // 简单类型: sub store + subStore: SubStore( + str: "subStore.str", + int_: 1, + set: {"subStore.set.0", "subStore.set.1"}, + list: ["subStore.list.0", "subStore.list.1"], + queue: Queue.from(["subStore.queue.0", "subStore.queue.1"]), + map: {"subStore.map.0": "value0", "subStore.map.1": "value1"}, + object: const NormalObject("subStore.object.name"), + baseList: ["subStore.baseList.0", "subStore.baseList.1"], + ), + //#################### + // 复杂类型 + //#################### + // 复杂类型: value 自定义 + object: const NormalObject("object.name"), + // 复杂类型: collection套娃 + storeSet: {SubStore(str: "storeSet.0.str")}, + storeList: [SubStore(str: "storeList.0.str")], + storeQueue: Queue.from([SubStore(str: "storeQueue.0.str")]), + storeMap: {"storeMap.0": SubStore(str: "storeMap.0.str")}, + ); + + factory RootStore.fromJson(Map input) { + return jsons.loadJson(input, RootStore()); + } + + // value + late final Value str; // = "".signal(field: "str", at: RootStore); + late final Value int_; // = 0.signal(field: "int_", at: RootStore); + late final Value dateTime; //= DateTime(0).signal(field: "dataTime",at: RootStore); + + /// 集合 + late final List list; //= [].signal(field: "list", at: RootStore); + late final Set set; //= {}.signal(field: "set", at: RootStore); + late final Queue queue; //= [].signal(field: "list", at: RootStore); + late final Map map; //= {}.signal(field: "map", at: RootStore); + + /// store + late final SubStore subStore; //= SubStore().signal(field: "subStore", at: RootStore); + + /// 嵌套对象或Signal,都自定义了loader,因为都是框架无法自动创建的集合内的对象 + late final Value object; + late final List storeList; + late final Set storeSet; + late final Queue storeQueue; + late final Map storeMap; +} + +abstract base class SubStoreBase extends TestStoreBase { + late final List baseList; // = {"base"}.signal(field: "baseSet", at: SubStoreBase); + + SubStoreBase({ + List baseList = const [], + }) { + this.baseList = field("baseList", baseList.signal()); + } +} + +// duplicate fields +base class SubStore extends SubStoreBase { + SubStore({ + String str = "", + int int_ = 0, + Set set = const {}, + List list = const [], + Queue? queue, + Map map = const {}, + NormalObject object = const NormalObject(""), + super.baseList, + }) { + this.str = field("str", str.signal()); + this.int_ = field("int_", int_.signal()); + this.set = field("set", set.signal()); + this.list = field("list", list.signal()); + this.queue = field("queue", (queue ?? Queue()).signal()); + this.map = field("map", map.signal()); + this.object = field("object", object.signal(), fromJson: NormalObject.jsonDecode); + } + + static SubStore load(Object input) => SubStore().loadJson(input); + + late final Value str; + late final Value int_; + late final Set set; + late final List list; + late final Queue queue; + late final Map map; + late final Value object; + + factory SubStore.fromJson(Map source) { + return SubStore().loadJson(source); + } + +//= NormalObject("sub.object").signal(field: "field:sub.object", at: SubStore); +// @override +// late final List overrideField; // = [].signal(field: "overrideField", at: SubStore); +} + +class NormalObject { + final String name; + + const NormalObject(this.name); + + toJson() { + return name; + } + + static NormalObject jsonDecode(input) { + return NormalObject(input as String); + } + + // FIXME 这里删掉toJson(),Field增加toJson和fromJson两个字段 + static jsonEncode(self) { + return (self as NormalObject).toJson(); + } +} + +enum TestEnum { + a, + b; + + static TestEnum fromJson(String value) { + return TestEnum.values.firstWhere((e) => e.name == value); + } + + String toJson() { + return name; + } +} diff --git a/packages/you_dart/test/state_store_test.dart b/packages/you_dart/test/state_store_test.dart new file mode 100644 index 00000000..68440cf1 --- /dev/null +++ b/packages/you_dart/test/state_store_test.dart @@ -0,0 +1,48 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. 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'; +import 'package:you_dart/src/state.dart'; + + +void main() { + group("basic", () { + group("json test", () { + test('test', () { + // try{ + // // assert(!this._fromJsons.containsKey(name),"Error duplicate field: ${this.runtimeType}.$name "); + // + // store.field("str"); + // fail("not here"); + // }catch(e){ + // check(e.toString()).equals("other"); + // } + }); + }); + }); +} + +// TODO ErrorDuplicateFieldStore 待测试 +base class ErrorDuplicateField extends Store { + final name = "define 1".signal(field: "name", at: ErrorDuplicateField); + final name2 = "define 2".signal(field: "name", at: ErrorDuplicateField); +} + +// inherit test use +base class InheritCaseBase extends Store { + late final Value baseA; //= "Base.a".signal(field: "Base.a", at: InheritCaseBase); + + InheritCaseBase(); +} + +base class InheritCaseChild extends InheritCaseBase { + late final Value a; + + InheritCaseChild() { + a = field("Child.a", "Child.a".signal()); + } +} diff --git a/packages/you_dart/test/state_test.dart b/packages/you_dart/test/state_test.dart new file mode 100644 index 00000000..14820a16 --- /dev/null +++ b/packages/you_dart/test/state_test.dart @@ -0,0 +1,138 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. 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:checks/checks.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:you_dart/src/state.dart'; + +void main() { + group("not null data is ok", () { + test('value', () { + { + var x = "x".signal(); + check(x()).equals("x"); + x.value = "y"; + check(x()).equals("y"); + } + }); + test('list', () { + List x = [1, 2].signal(); + check(x.toList()).deepEquals([1, 2]); + + x.addAll([3, 4]); + check(x.toList()).deepEquals([1, 2, 3, 4]); + + x.removeLast(); + check(x.toList()).deepEquals([1, 2, 3]); + }); + test('set', () { + var x = {1, 2}.signal(); + check(x.toList()).deepEquals([1, 2]); + + x.addAll([3, 4]); + check(x.toList()).deepEquals([1, 2, 3, 4]); + + x.removeAll({2, 3}); + check(x.toList()).deepEquals([1, 4]); + }); + test('map', () { + Map x = {"a": 1}.signal(); + check(x.keys.toList()).deepEquals(["a"]); + check(x.values.toList()).deepEquals([1]); + + x.addAll({"b": 2}); + check(x.keys.toList()).deepEquals(["a", "b"]); + check(x.values.toList()).deepEquals([1, 2]); + + x.remove("b"); + check(x.keys.toList()).deepEquals(["a"]); + check(x.values.toList()).deepEquals([1]); + }); + }); + + group(" null data is ok", () { + test('value', () { + { + var x = (null as String?).signal(); + check(x()).equals(null); + x.value = "y"; + check(x()).equals("y"); + } + { + int? i; + var x = i.signal(); + check(x()).equals(null); + x.value = 2; + check(x()).equals(2); + } + }); + test('list', () { + List x = [null, 2].signal(); + check(x.toList()).deepEquals([null, 2]); + + x.addAll([3, null]); + check(x.toList()).deepEquals([null, 2, 3, null]); + }); + test('set', () { + var x = {null, 2}.signal(); + check(x.toList()).deepEquals([null, 2]); + + x.addAll([3, null]); + check(x.toList()).deepEquals([null, 2, 3]); + }); + test('map', () { + Map x = {"a": null}.signal(); + check(x.keys.toList()).deepEquals(["a"]); + check(x.values.toList()).deepEquals([null]); + + x.addAll({"b": 2}); + check(x.keys.toList()).deepEquals(["a", "b"]); + check(x.values.toList()).deepEquals([null, 2]); + }); + }); + // TODO 回头看看需要管吗? + // 暂时不限制重复定义 + // group("multiple definitions 多次调用断言失败", () { + // test("value", () { + // try { + // var s = "1".signal(); + // s.signal(); + // fail("not here,should be fail"); + // } catch (e) { + // check(e.toString()).contains('''already signal()'''); + // } + // }); + // test("list", () { + // try { + // var s = ["1"].signal(); + // s.signal(); + // fail("not here,should be fail"); + // } catch (e) { + // check(e.toString()).contains('''already signal()'''); + // } + // }); + // test("set", () { + // try { + // var s = {"1"}.signal(); + // s.signal(); + // fail("not here,should be fail"); + // } catch (e) { + // check(e.toString()).contains('''already signal()'''); + // } + // }); + // test("map", () { + // try { + // var s = {"a": "b"}.signal(); + // s.signal(); + // fail("not here,should be fail"); + // } catch (e) { + // check(e.toString()).contains('''already signal()'''); + // } + // }); + // }); +} diff --git a/packages/you_dart/test/you_dart_test.dart b/packages/you_dart/test/you_dart_test.dart deleted file mode 100644 index 826d4574..00000000 --- a/packages/you_dart/test/you_dart_test.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:you_dart/you_dart.dart'; -import 'package:test/test.dart'; - -void main() { - group('A group of tests', () { - final awesome = Awesome(); - - setUp(() { - // Additional setup goes here. - }); - - test('First Test', () { - expect(awesome.isAwesome, isTrue); - }); - }); -} diff --git a/packages/you_flutter/example/state/state_example_1_counter.dart b/packages/you_flutter/example/state/state_example_1_counter.dart new file mode 100644 index 00000000..99dcaafa --- /dev/null +++ b/packages/you_flutter/example/state/state_example_1_counter.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:you_dart/src/state.dart'; +import 'package:you_flutter/src/state_widget.dart'; + +main() { + runApp(MaterialApp(home: Scaffold(body: HelloSingleValue()))); +} + +class HelloSingleValue extends StatelessWidget { + final counter = 0.signal(); + + HelloSingleValue({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Watch((context) => Text("counter.value:${counter.value}")), + Watch((context) => Text("or counter(): ${counter()}")), + ElevatedButton(onPressed: () => counter.value++, child: const Text("counter.value++")), + const Divider(), + const Text("特别注意,counter.value++, 会先调用getter,再调用setter") + ], + ); + } +} diff --git a/packages/you_flutter/example/state/state_example_2_store.dart b/packages/you_flutter/example/state/state_example_2_store.dart new file mode 100644 index 00000000..4e1b78de --- /dev/null +++ b/packages/you_flutter/example/state/state_example_2_store.dart @@ -0,0 +1,51 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:you_dart/src/state.dart'; +import 'package:you_flutter/src/state_widget.dart'; + +/// just a normal class +class Store { + Store(); + + final dice1 = 0.signal(); + final dice2 = 0.signal(); + + // compute value + int computeSum(){ + return dice1()+dice2(); + } +} + +// StreamBuilder?ø +main() { + runApp(MaterialApp(home: Scaffold(body: HelloStore()))); +} + +class HelloStore extends StatelessWidget { + final Store store = Store(); + + HelloStore({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Watch((context) => Text("store.dice1.value:${store.dice1()}")), + ElevatedButton( + onPressed: () => store.dice1.value=(Random().nextInt(6)+1), + child: const Text("store.dice1.value=Random()"), + ), + const Divider(), + Watch((context) => Text("store.dice2():${store.dice2()}")), + ElevatedButton( + onPressed: () => store.dice2.value=Random().nextInt(6)+1, + child: const Text("store.dice2.value=Random()"), + ), + const Divider(), + const Divider(), + Watch((context) => Text("computeSum():${store.computeSum()}")), + ], + ); + } +} diff --git a/packages/you_flutter/example/state/state_example_3_list.dart b/packages/you_flutter/example/state/state_example_3_list.dart new file mode 100644 index 00000000..261cdbf9 --- /dev/null +++ b/packages/you_flutter/example/state/state_example_3_list.dart @@ -0,0 +1,40 @@ + +import 'package:flutter/material.dart'; +import 'package:you_dart/src/state.dart'; +import 'package:you_flutter/src/state_widget.dart'; + +// StreamBuilder?ø +main() { + runApp(MaterialApp(home: Scaffold(body: HelloStore()))); +} + +int buildTimes = 0; +int watchBuildTimes = 0; + +class HelloStore extends StatelessWidget { + final List list = [].signal(); + + HelloStore({super.key}); + + @override + Widget build(BuildContext context) { + buildTimes++; + return Column( + children: [ + ElevatedButton( + onPressed: () => list.add(list.length + 1), + child: Text("list.add(list.length+1) ||| watch outer buildTimes:${buildTimes++}"), + ), + const Divider(), + Watch((context) { + return Column( + children: [ + Text("list.length:${list.length} ||| watch inner BuildTimes:${watchBuildTimes++}"), + for (var i in list) Text("list item: $i") + ], + ); + }), + ], + ); + } +} diff --git a/packages/you_flutter/example/state/state_example_4_map.dart b/packages/you_flutter/example/state/state_example_4_map.dart new file mode 100644 index 00000000..e17099ea --- /dev/null +++ b/packages/you_flutter/example/state/state_example_4_map.dart @@ -0,0 +1,40 @@ + +import 'package:flutter/material.dart'; +import 'package:you_dart/src/state.dart'; +import 'package:you_flutter/src/state_widget.dart'; + +// StreamBuilder?ø +main() { + runApp(MaterialApp(home: Scaffold(body: HelloStore()))); +} + +int buildTimes = 0; +int watchBuildTimes = 0; + +class HelloStore extends StatelessWidget { + final map = {}.signal(); + + HelloStore({super.key}); + + @override + Widget build(BuildContext context) { + buildTimes++; + return Column( + children: [ + ElevatedButton( + onPressed: () => map[map.length+1]="item: ${map.length+1}", + child: Text("map put ||| watch outer buildTimes:${buildTimes++}"), + ), + const Divider(), + Watch((context) { + return Column( + children: [ + Text("map.length:${map.length} ||| watch inner BuildTimes:${watchBuildTimes++}"), + for (var key in map.keys) Text("map item: key:$key, value:${map[key]} ") + ], + ); + }), + ], + ); + } +} diff --git a/packages/you_flutter/example/state/state_example_5_set.dart b/packages/you_flutter/example/state/state_example_5_set.dart new file mode 100644 index 00000000..24a2a64e --- /dev/null +++ b/packages/you_flutter/example/state/state_example_5_set.dart @@ -0,0 +1,40 @@ + +import 'package:flutter/material.dart'; +import 'package:you_dart/src/state.dart'; +import 'package:you_flutter/src/state_widget.dart'; + +// StreamBuilder?ø +main() { + runApp(MaterialApp(home: Scaffold(body: HelloStore()))); +} + +int buildTimes = 0; +int watchBuildTimes = 0; + +class HelloStore extends StatelessWidget { + final set = {}.signal(); + + HelloStore({super.key}); + + @override + Widget build(BuildContext context) { + buildTimes++; + return Column( + children: [ + ElevatedButton( + onPressed: () => set.add(set.length + 1), + child: Text("set.add(set.length+1) ||| watch outer buildTimes:${buildTimes++}"), + ), + const Divider(), + Watch((context) { + return Column( + children: [ + Text("set.length:${set.length} ||| watch inner BuildTimes:${watchBuildTimes++}"), + for (var i in set) Text("list item: $i") + ], + ); + }), + ], + ); + } +} diff --git a/packages/you_flutter/example/state/state_example_6_complex_data.dart b/packages/you_flutter/example/state/state_example_6_complex_data.dart new file mode 100644 index 00000000..74334073 --- /dev/null +++ b/packages/you_flutter/example/state/state_example_6_complex_data.dart @@ -0,0 +1,106 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:you_dart/src/state.dart'; +import 'package:you_flutter/src/state_widget.dart'; + +// StreamBuilder?ø +main() { + runApp(MaterialApp(home: Scaffold(body: HelloStore()))); +} + +class Player { + final name = "".signal(); + final dice1 =0.signal(); + final dice2 = 0.signal(); + + Player({required String name}) { + this.name.value = name; + } + + // compute value + int computeSum() { + return dice1() + dice2(); + } + + void play() { + dice1.value = Random().nextInt(6) + 1; + dice2.value = Random().nextInt(6) + 1; + } +} +class Store { + Store(); + + final List players = [].signal(); + final List lookers = ["zhao", "qian", "sun", "li", "zhou", "wu", "zheng", "wang"].signal(); + + // compute value + String win() { + if (players.isEmpty) { + return "No player!!!!, please click looker join game!!!!!"; + } + Player winPlayer = players.first; + for (var player in players) { + if (player.computeSum() > winPlayer.computeSum()) { + winPlayer = player; + } + } + return "Winner: Player(${winPlayer.name()}, computeSum: ${winPlayer.computeSum()} )"; + } + + void play() { + for (var p in players) { + p.play(); + } + } + + void join(String peopleName) { + lookers.remove(peopleName); + players.add(Player(name: peopleName)); + } + + void quit(Player player) { + lookers.add(player.name()); + players.remove(player); + } +} + +class HelloStore extends StatelessWidget { + final store = Store(); + + HelloStore({super.key}); + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + return Column( + children: [ + Text("GAME", style: theme.textTheme.headlineLarge), + Watch((context) => Text(store.win())), + ElevatedButton(onPressed: () => store.play(), child: const Text("【【【【【play!!!】】】】】")), + const Divider(), + Watch((context) { + return Column(children: [ + const Text("Lookers"), + for (var player in store.players) + ElevatedButton.icon( + label: Text("【${player.name()}】 : dice: ${player.computeSum()}, click quit "), + icon: const Icon(Icons.tag_faces_sharp), + onPressed: () => store.quit(player)), + const Divider(), + const Text("Lookers"), + for (var looker in store.lookers) + ElevatedButton.icon( + label: Text("【$looker】 click join"), + icon: const Icon(Icons.person), + onPressed: () { + store.lookers.remove(looker); + store.players.add(Player(name: looker)); + }, + ) + ]); + }), + ], + ); + } +} diff --git a/packages/you_flutter/example/state/state_example_limit_1.dart b/packages/you_flutter/example/state/state_example_limit_1.dart new file mode 100644 index 00000000..158048c7 --- /dev/null +++ b/packages/you_flutter/example/state/state_example_limit_1.dart @@ -0,0 +1,57 @@ + +import 'package:flutter/material.dart'; +import 'package:you_dart/src/state.dart'; +import 'package:you_flutter/src/state_widget.dart'; + +main() { + runApp(MaterialApp(home: Scaffold(body: HelloSingleValue()))); +} + +class HelloSingleValue extends StatelessWidget { + final counter = 0.signal(); + + HelloSingleValue({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Builder、StatefulBuilder的观测限制", style: Theme.of(context).textTheme.titleLarge), + const Divider(), + Watch((context) => Text("✅正常观测:${counter()},Watch下普通组件 ok")), + Watch((context) => Builder(builder: (context) { + return Text("✅正常观测:${counter()}, callback直接嵌套一层Builder、StatefulBuilder ok"); + })), + Watch((context) => Builder(builder: (context) { + return Builder(builder: (context) { + return Text("✅正常观测:${counter()}, callback直接嵌套多层Builder、StatefulBuilder ok"); + }); + })), + + /// 包裹在Column等容器后的Builder,Watch无法洞察 + Watch((context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Builder(builder: (context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("🟡callback 间接嵌套Builder、StatefulBuilder"), + Text(" 💔无法观测:${counter()}"), + Watch((context) => Text(" ✅恢复正常:${counter()}, 需重新Watch")), + ], + ); + }) + ], + )), + ElevatedButton(onPressed: () => counter.value++, child: const Text("counter.value++")), + const Divider(), + const Text("1. 为啥Builder类组件间接嵌套不起作用?因为Watch在你的callback执行时注册Signal监听器,而间接嵌套的Builder.build,不会在这个callback中直接执行,而是绕过了这个注册过程,这是flutter的机制."), + const Text("2. 为啥会有这个限制?因为Watch想给予你的程序可观测性状态的同时,又不希望改变你原有的flutter代码惯例,Watch即不需要你的Widget继承某类," + "也不需要你与dispose、生命周期做斗争,我相信与其和内存泄漏、生命周期打架," + "不如从api层级就采用无生命周期、无干扰的设计,减少dispose、addListener、removeListener这样的api,放心用即可!你可以决定继续用StatefulWidget,也可以完全不用"), + ], + ); + } +} diff --git a/packages/you_flutter/example/state/state_example_limit_2.dart b/packages/you_flutter/example/state/state_example_limit_2.dart new file mode 100644 index 00000000..98630474 --- /dev/null +++ b/packages/you_flutter/example/state/state_example_limit_2.dart @@ -0,0 +1,30 @@ + +import 'package:flutter/material.dart'; +import 'package:you_dart/src/state.dart'; +import 'package:you_flutter/src/state_widget.dart'; + +main() { + runApp(MaterialApp(home: Scaffold(body: HelloSingleValue()))); +} + +class HelloSingleValue extends StatelessWidget { + final counter = 0.signal(); + + HelloSingleValue({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Watch((context) { + // TODO Watch 回调内目前不能改值,会报错,这是在Widget.build()内,改值会引起Watch.setState + // 而setState规范是不能在Widget.build内使用,冲突了,应该在callback期间让信号停发通知即可 + + // 当前这里会报错: + counter.value+=1; + return Text("正常观测:${counter()}"); + }), + ], + ); + } +} diff --git a/packages/you_flutter/lib/src/state_widget.dart b/packages/you_flutter/lib/src/state_widget.dart new file mode 100644 index 00000000..8b3ffbe1 --- /dev/null +++ b/packages/you_flutter/lib/src/state_widget.dart @@ -0,0 +1,72 @@ +import 'dart:collection'; + +import 'package:flutter/widgets.dart'; +import 'package:you_dart/state.dart'; + +/// 类似[WidgetBuilder] [StatefulBuilder] +/// Watch用来build 观测state变化的。 +final class Watch extends StatefulWidget { + final WidgetBuilder builder; + + const Watch(this.builder, {super.key}); + + @override + State createState() { + return _WatchState(); + } +} + +final class _WatchState extends State { + final Map _signalConnections = HashMap(); + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + } + + @override + void dispose() { + for (var conn in _signalConnections.values) { + conn.cancel(); + } + _signalConnections.clear(); + + super.dispose(); + } + + // 如果客户程序返回的是Builder、StatefulBuilder 劫持它予以观测, + // 并递归的看下一层是不是也是Builder、StatefulBuilder + Widget _recursionHijackBuilder(Widget widget) { + if (widget is Builder) { + return Watch((context) { + return _recursionHijackBuilder(widget.builder(context)); + }); + } + if (widget is StatefulBuilder) { + return StatefulBuilder( + key: widget.key, + builder: (context, setState) { + return Watch((context) { + return _recursionHijackBuilder(widget.builder(context, setState)); + }); + }); + } + return widget; + } + + @override + Widget build(BuildContext context) { + return Signal.track(useSignal: () { + Widget result = widget.builder(context); + return _recursionHijackBuilder(result); + }, onChanged: (signal) { + setState(() {}); + }, onConnected: (conn) { + var x = _signalConnections[conn.signal]; + if (x != null) { + x.cancel(); + } + _signalConnections[conn.signal] = conn; + }); + } +} diff --git a/packages/you_flutter/lib/you_state.dart b/packages/you_flutter/lib/you_state.dart index 5e7ef7f9..989d6069 100644 --- a/packages/you_flutter/lib/you_state.dart +++ b/packages/you_flutter/lib/you_state.dart @@ -1,7 +1,4 @@ library you_state; -/// A Calculator. -class Calculator { - /// Returns [value] plus 1. - int addOne(int value) => value + 1; -} +export 'package:you_dart/state.dart'; +export 'src/state_widget.dart' show Watch; diff --git a/packages/you_flutter/pubspec.yaml b/packages/you_flutter/pubspec.yaml index d03a0ad6..65da9c62 100644 --- a/packages/you_flutter/pubspec.yaml +++ b/packages/you_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: you_flutter description: "Flutter framework project, for state , navigator and other flutter development infrastructure ." -version: 0.0.2 +version: 0.0.3 homepage: https://github.com/chen56/you repository: https://github.com/chen56/you @@ -11,7 +11,7 @@ environment: dependencies: flutter: sdk: flutter - + you_dart: ^0.0.3 dev_dependencies: flutter_test: sdk: flutter diff --git a/packages/you_flutter/test/you_test.dart b/packages/you_flutter/test/you_test.dart index 9a1c5759..29a12707 100644 --- a/packages/you_flutter/test/you_test.dart +++ b/packages/you_flutter/test/you_test.dart @@ -1,12 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:you_flutter/you_state.dart'; void main() { test('adds one to input values', () { - final calculator = Calculator(); - expect(calculator.addOne(2), 3); - expect(calculator.addOne(-7), -6); - expect(calculator.addOne(0), 1); }); }