Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2ca0257
Fix view model lifecycle and example test coverage
lwj1994 Mar 16, 2026
0057bb6
Add AGENTS guidance across monorepo
lwj1994 Mar 16, 2026
2eeb4ad
feat: support string interpolation and improved escaping in `GenSpec`…
lwj1994 Mar 17, 2026
94b33b9
refactor: improve lifecycle management and error handling in `AutoDis…
lwj1994 Mar 17, 2026
6369cbd
Fix lifecycle and generator edge cases
lwj1994 Mar 17, 2026
b28d898
refactor(auto_dispose): 改进实例管理和生命周期处理
Mar 17, 2026
2994f13
refactor(view_model): dispose 阶段错误统一走 onDisposeError 回调
Mar 17, 2026
3bb1f37
refactor(view_model): 合并 onListenerError/onDisposeError 为统一的 onError 回调
Mar 17, 2026
6f76d54
refactor(view_model): 扩展 ErrorType 枚举并统一错误处理管道
Mar 17, 2026
1d06b45
ci: migrate `codecov` workflow to GitHub-hosted runners and remove `p…
Mar 17, 2026
636403e
fix(view_model): 对齐批量 tag 读取语义与文档
Mar 17, 2026
3cc5e7d
fix(view_model): 完善错误处理测试覆盖与代码清理
Mar 17, 2026
de48e73
chore(view_model): publish version 1.0.3-dev.0
Mar 17, 2026
513ea6b
chore: publish version 1.0.3-dev.0 (annotation/generator) + 1.0.3-dev…
Mar 18, 2026
2d2a7cf
chore: publish version 1.0.3-dev.1 (annotation/generator)
Mar 18, 2026
bea127f
feat: enhance error handling and refine string generation
lwj1994 Mar 24, 2026
3b67326
publish version:1.0.3-dev.2
lwj1994 Mar 24, 2026
df2f436
publish version:1.0.3-dev.3
lwj1994 Mar 28, 2026
1e45784
publish version:1.0.3
Apr 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions .agents/skills/view_model/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,13 @@ Use this skill for requests like:

4. Choose access API correctly
- `watch(spec)`: create/get + bind + listen (reactive rebuild/`onUpdate`).
- `read(spec)`: create/get + bind, no listener.
- `watchCached/readCached`: lookup existing instance only (no creation).
- `read(spec)`: create/get + bind, no ViewModel listener.
- `watchCached/readCached`: lookup existing instance only (no creation); `watchCached` binds + listens, `readCached` binds only.
- `maybeWatchCached/maybeReadCached`: null-safe cached lookup.
- `watchCachesByTag/readCachesByTag`: batch tag lookup.
- `watchCachesByTag/readCachesByTag`: batch tag lookup; `watchCachesByTag`
is batch `watch`, `readCachesByTag` is batch `read`: it still binds,
participates in lifecycle cleanup, and reacts to recreate/dispose, but it
does not react to `notifyListeners()`.
- `listen/listenState/listenStateSelect`: side-effect listeners, auto-cleaned on binding dispose.
- `recycle(vm)`: force unbind all and dispose; next `watch/read` gets fresh instance.

Expand All @@ -94,16 +97,16 @@ Use this skill for requests like:
- For route-based pause/resume, register:
- `MaterialApp(navigatorObservers: [ViewModel.routeObserver])`
- Built-in pause providers: route cover, ticker mode, app lifecycle.
- Use `StateViewModelValueWatcher` for selector-level rebuilds (usually pair with `read`, not `watch`).
- Use `StateViewModelValueWatcher` for selector-level rebuilds; pair it with
`read`, not `watch`, to avoid duplicate subscriptions and broad rebuilds.
- `ObservableValue` + `ObserverBuilder(1/2/3)` for lightweight reactive values; same `shareKey` means shared underlying state.

8. App-level setup
- Call `ViewModel.initialize(...)` once at app startup (subsequent calls are ignored).
- Configure `ViewModelConfig` when needed:
- `isLoggingEnabled`
- `equals` (state equality strategy)
- `onListenerError`
- `onDisposeError`
- `onError` (with `ErrorType.listener` / `ErrorType.lifecycle` / `ErrorType.dispose` / `ErrorType.pauseResume`)
- If using `equals: (a, b) => a == b`, ensure state classes implement `==` and `hashCode`.

9. Testing and mocking
Expand All @@ -119,12 +122,17 @@ Use this skill for requests like:

Do:
- Keep `watch` for reactive UI, `read` for imperative actions.
- For field-level updates (`listenStateSelect`, `StateViewModelValueWatcher`,
selector-based rebuilds), read the ViewModel with `read` and let the
selector mechanism drive updates; avoid `watch` on the same ViewModel.
- Set explicit `key` whenever instance sharing is a requirement.
- Dispose non-widget bindings explicitly.
- Use `listenStateSelect` for side effects on selected state fields.

Don't:
- Claim `read` is "non-binding" (it still binds and affects lifecycle).
- Pair selector-level rebuild tools with `watch`; this usually causes broader
rebuilds than intended.
- Use cached APIs expecting auto-create behavior.
- Overuse `aliveForever` for page-scoped state.
- Forget `ViewModel.routeObserver` when relying on route pause behavior.
Expand Down
29 changes: 19 additions & 10 deletions .agents/skills/view_model/references/README_FULL_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,12 @@ List<MyVM> vms = viewModelBinding.watchCachesByTag<MyVM>('group-a');
List<MyVM> vms = viewModelBinding.readCachesByTag<MyVM>('group-a');
```

`watchCachesByTag` behaves like batch `watch`: it binds each matched instance
and listens for ViewModel changes. `readCachesByTag` behaves like batch `read`:
it still binds each matched instance, participates in binding cleanup on
dispose, and reacts to recreate/dispose events, but it does not react to
`notifyListeners()`.

### listen / listenState / listenStateSelect

Fire-and-forget listeners that are automatically cleaned up when the binding disposes. These use `read` internally (bind without triggering widget rebuild) and then attach custom callbacks:
Expand All @@ -413,6 +419,10 @@ viewModelBinding.listenStateSelect(
);
```

For field-level updates, prefer `read` plus selector-based listeners. Avoid
pairing `listenStateSelect` with `watch` on the same ViewModel, or you'll keep
the broad ViewModel listener and lose the point of selective updates.

### recycle

Force-disposes a ViewModel by calling `unbindAll()` on its handle (removes all bindingIds, triggering disposal). The next `watch`/`read` call with the same spec will create a fresh instance.
Expand Down Expand Up @@ -588,7 +598,8 @@ Only rebuilds when the selected properties of a `StateViewModel` change:

```dart
class _MyPageState extends State<MyPage> with ViewModelStateMixin {
// Use read — the ValueWatcher handles its own subscriptions internally
// Use read — the ValueWatcher handles its own subscriptions internally.
// Avoid watch here, or the whole ViewModel will still trigger rebuilds.
late final vm = viewModelBinding.read(userSpec);

@override
Expand All @@ -602,7 +613,11 @@ class _MyPageState extends State<MyPage> with ViewModelStateMixin {
}
```

Internally, each selector is wrapped into a `listenStateSelect` call on the ViewModel. The widget only rebuilds when at least one selector's output differs from its previous value (compared using `ViewModelConfig.equals` or `==` by default).
Internally, each selector is wrapped into a `listenStateSelect` call on the
ViewModel. The widget only rebuilds when at least one selector's output differs
from its previous value (compared using `ViewModelConfig.equals` or `==` by
default). To keep updates truly fine-grained, read the ViewModel with `read`
and let the selector mechanism drive rebuilds instead of also using `watch`.

### ObservableValue & ObserverBuilder

Expand Down Expand Up @@ -800,16 +815,10 @@ void main() {
// Used by StateViewModel.setState and listenStateSelect
equals: (a, b) => a == b,

// Global error handler for listener errors
onListenerError: (error, stackTrace, context) {
// context is 'notifyListeners' or 'stateListener'
// Global error handler for listener and disposal errors
onError: (error, stackTrace, type) {
crashReporter.report(error, stackTrace);
},

// Global error handler for disposal errors
onDisposeError: (error, stackTrace) {
debugPrint('Disposal error: $error');
},
),
lifecycles: [DebugLifecycle()],
);
Expand Down
9 changes: 9 additions & 0 deletions .agents/skills/view_model/references/README_FULL_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,18 @@ StreamViewModel() {
| **`watch(spec)`** | 在 Widget 的 `build` 或逻辑中 | **响应式**:VM 变化会触发 UI 刷新。若 VM 不存在则创建。 |
| **`read(spec)`** | 事件回调、只需调用方法时 | **非响应式**:仅读取,不监听。若 VM 不存在则创建。 |
| **`watchCached(key/tag)`** | 寻找现有的单例或共享 VM | 如果缓存里没找到,它会抛出异常。 |
| **`readCached(key/tag)`** | 读取现有缓存但不触发刷新 | 只查找已有实例,不创建;会参与 binding 生命周期,但不响应 `notifyListeners()`。 |
| **`watchCachesByTag(tag)`** | 按 tag 批量响应式获取 VM | 批量版 `watch`:会 `bind`,也会监听变化。 |
| **`readCachesByTag(tag)`** | 按 tag 批量读取已有 VM | 批量版 `read`:会 `bind`、参与 dispose 清理,并感知 recreate/dispose,但不响应 `notifyListeners()`。 |
| **`listenStateSelect(...)`**| 针对性监听某个字段 | 例如:只有 `user.age` 变了才弹窗,别的字段变了不理。 |
| **`recycle(vm)`** | 强制销毁重来 | 比如:退出登录时,一键回收所有用户相关的 VM。 |

补充说明:

- `watch*` 和 `read*` 都会建立 binding,都会影响实例生命周期;差别主要在于是否监听 ViewModel 自身的变化。
- `readCachesByTag(tag)` 不会响应 `notifyListeners()`,但会注册 recreate listener;它不是纯静态查询,binding dispose 时仍会自动 `unbind/removeRef`。
- 做字段级更新时,优先用 `read` 拿到 ViewModel,再交给 `listenStateSelect` 或 `StateViewModelValueWatcher` 驱动更新;不要再对同一个 ViewModel 额外 `watch`。

---

## 💤 智能 暂停 / 恢复 机制
Expand Down
8 changes: 7 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@
"Bash(git commit:*)",
"Bash(fd pub:*)",
"Bash(git check-ignore:*)",
"Bash(git rm:*)"
"Bash(git rm:*)",
"Bash(git pull:*)",
"Bash(git stash:*)",
"Bash(dart test:*)",
"Bash(git status:*)",
"Bash(git:*)",
"Bash(flutter analyze:*)"
]
}
}
89 changes: 68 additions & 21 deletions .github/workflows/codecov.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,87 @@ on:
branches:
- main
- develop
pull_request:
branches:
- main
- develop

permissions:
contents: read

jobs:
codecov-upload:
runs-on: self-hosted # Specify job runs on self-hosted runner
defaults:
run:
working-directory: packages/view_model
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: 👷 Install Dependencies
timeout-minutes: 1
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true

- name: Verify toolchain
timeout-minutes: 2
run: |
puro dart --version
puro dart pub get
dart --version
flutter --version

- name: 🔎 Check format
timeout-minutes: 1
run: puro dart format --set-exit-if-changed -l 80 -o none lib/
- name: Check view_model
timeout-minutes: 5
working-directory: packages/view_model
run: |
flutter pub get
dart format --set-exit-if-changed -l 80 -o none lib test
dart analyze --fatal-infos --fatal-warnings lib
flutter test --coverage test

- name: 📈 Check analyzer
timeout-minutes: 1
run: puro dart analyze --fatal-infos --fatal-warnings lib/
- name: Check view_model_annotation
timeout-minutes: 3
working-directory: packages/view_model_annotation
run: |
dart pub get
dart format --set-exit-if-changed -l 80 -o none lib
dart analyze --fatal-infos --fatal-warnings lib

- name: Check view_model_generator
timeout-minutes: 5
working-directory: packages/view_model_generator
run: |
dart pub get
dart format --set-exit-if-changed -l 80 -o none lib test
dart analyze --fatal-infos --fatal-warnings lib test
dart test

#- name: 🌐 Setup Chrome
# uses: browser-actions/setup-chrome@latest
- name: Check view_model_devtools_extension
timeout-minutes: 5
working-directory: packages/view_model_devtools_extension
run: |
flutter pub get
dart format --set-exit-if-changed -l 80 -o none lib test
dart analyze --fatal-infos --fatal-warnings lib test
flutter test

- name: 🧪 Run tests
timeout-minutes: 2
- name: Check example counter
timeout-minutes: 4
working-directory: example/counter
run: |
flutter pub get
dart format --set-exit-if-changed -l 80 -o none lib test
dart analyze --fatal-infos --fatal-warnings lib test
flutter test

- name: Check example todo_list
timeout-minutes: 4
working-directory: example/todo_list
run: |
puro flutter test --coverage test
flutter pub get
dart format --set-exit-if-changed -l 80 -o none lib test
dart analyze --fatal-infos --fatal-warnings lib test
flutter test

- name: 📥 Upload coverage to Codecov
- name: Upload coverage to Codecov
timeout-minutes: 1
uses: codecov/codecov-action@v5
with:
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ build/
.fvm/

.trae
.agent
.agent
.claude/settings.local.json
Loading
Loading