easy_state_core 0.1.0
easy_state_core: ^0.1.0 copied to clipboard
A universal network-request-driven state management framework. Works with GetX, Riverpod, Bloc, or any state management solution.
easy_state_core #
A universal network-request-driven state management framework for Flutter.
一个通用的网络请求驱动状态管理框架,适用于 Flutter。
Introduction (介绍) #
Core Philosophy: Define an abstract request interface, implement it, and the controller automatically handles the request lifecycle.
核心思想:定义一个抽象的请求接口,用户实现该接口后,控制器在生命周期中自动发起请求。
Key Features (核心特性) #
| Feature | Description |
|---|---|
EasyRequest<T> |
Single value async request engine / 单值异步请求引擎 |
EasyPager<T> |
Pagination engine with loadMore / 分页引擎,支持加载更多 |
EasyStore<S> |
Pluggable state storage interface / 可插拔状态存储接口 |
| Zero-overhead adapters | Direct state injection, no Stream subscription needed / 直接状态注入,无需 Stream 订阅 |
Install (安装) #
Add to your pubspec.yaml:
在 pubspec.yaml 中添加:
dependencies:
easy_state_core: ^0.1.0
For adapters (适配器):
dependencies:
easy_state_getx: ^0.1.0 # GetX adapter
easy_state_riverpod: ^0.1.0 # Riverpod adapter
Quick Start (快速上手) #
Single Value Request (单值请求) #
import 'package:easy_state_core/easy_state_core.dart';
// 1. Create engine with fetcher
final engine = EasyRequest<User>(
fetcher: () => api.getUser(),
);
// 2. Listen to state changes
engine.stream.listen((state) {
print('Status: ${state.status}');
print('Data: ${state.data}');
print('Is refreshing: ${state.isRefreshing}');
});
// 3. Trigger fetch
await engine.fetch();
// 4. Refresh (keeps old data visible)
await engine.refresh();
// 5. Retry on error
await engine.retry();
// 6. Dispose when done
engine.dispose();
Paginated Request (分页请求) #
final pager = EasyPager<Article>(
fetcher: (page) => api.getArticles(page: page),
pageSize: 20,
);
// First page
await pager.fetch();
// Load next page (appends items)
await pager.loadMore();
// Refresh (resets to page 1)
await pager.refresh();
// Access state
pager.stream.listen((state) {
print('Items: ${state.items.length}');
print('Page: ${state.page}');
print('Has more: ${state.hasMore}');
print('Is loading more: ${state.isLoadingMore}');
});
pager.dispose();
State Models (状态模型) #
EasyValueState<T> #
State model for single value requests.
单值请求的状态模型。
| Field | Type | Description |
|---|---|---|
status |
EasyStatus |
Current status / 当前状态 |
data |
T? |
The fetched data / 获取的数据 |
error |
Object? |
Error if failed / 错误信息 |
stackTrace |
StackTrace? |
Stack trace for debugging / 调试用堆栈 |
isRefreshing |
bool |
True during refresh (old data visible) / 刷新中(旧数据可见) |
EasyPagedState<T> #
State model for paginated requests.
分页请求的状态模型。
| Field | Type | Description |
|---|---|---|
status |
EasyStatus |
Current status / 当前状态 |
items |
List<T> |
Accumulated items / 累积的数据列表 |
page |
int |
Current page (1-based) / 当前页码(从1开始) |
pageSize |
int |
Items per page / 每页数量 |
hasMore |
bool |
More pages available / 是否有更多数据 |
isRefreshing |
bool |
Refreshing first page / 正在刷新首页 |
isLoadingMore |
bool |
Loading next page / 正在加载下一页 |
error |
Object? |
Fetch/refresh error / 首次加载或刷新错误 |
loadMoreError |
Object? |
Load more error / 加载更多错误 |
State Machine (状态机) #
EasyStatus Enum #
enum EasyStatus {
idle, // Initial state / 初始状态
loading, // Fetching data / 加载中
success, // Data loaded / 加载成功
empty, // No data / 数据为空
error, // Failed / 加载失败
}
EasyRequest State Transitions (单值请求状态流转) #
Mermaid Diagram:
stateDiagram-v2
[*] --> idle
idle --> loading: fetch()
loading --> success: data received
loading --> empty: empty data
loading --> error: exception
success --> loading: fetch()
success --> success: refresh() [isRefreshing=true]
error --> loading: retry()
error --> error: refresh() fails [keeps error]
note right of success: refresh() keeps old data visible
ASCII Diagram:
┌─────────────────────────────────────────────────────────────┐
│ EasyRequest State Machine │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────┐ fetch() ┌─────────┐ data ┌─────────┐ │
│ │ idle │ ─────────► │ loading │ ───────► │ success │ │
│ └──────┘ └─────────┘ └────┬────┘ │
│ │ │ │
│ │ empty refresh() │
│ ▼ (isRefreshing) │
│ ┌─────────┐ │ │
│ │ empty │ │ │
│ └─────────┘ ▼ │
│ │ ┌───────────┐ │
│ │ error │ success + │ │
│ ▼ │ refreshing│ │
│ ┌─────────┐ └───────────┘ │
│ │ error │ ◄─── refresh fails │
│ └────┬────┘ (keeps old data) │
│ │ │
│ retry() │
│ │ │
│ ▼ │
│ ┌─────────┐ │
│ │ loading │ │
│ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
EasyPager State Transitions (分页请求状态流转) #
Mermaid Diagram:
stateDiagram-v2
[*] --> idle
idle --> loading: fetch()/refresh()
loading --> success: items received
loading --> empty: no items
loading --> error: exception
success --> success: loadMore() [isLoadingMore=true]
success --> success: refresh() [isRefreshing=true]
success --> success: loadMore fails [loadMoreError set]
error --> loading: fetch()/refresh()
ASCII Diagram:
┌──────────────────────────────────────────────────────────────────┐
│ EasyPager State Machine │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────┐ fetch() ┌─────────┐ items ┌─────────┐ │
│ │ idle │ ─────────► │ loading │ ───────► │ success │ │
│ └──────┘ └─────────┘ └────┬────┘ │
│ │ │ │
│ empty/error loadMore() │
│ │ (isLoadingMore) │
│ ▼ │ │
│ ┌──────────────┐ ▼ │
│ │ empty/error │ ┌────────────┐ │
│ └──────────────┘ │ success + │ │
│ │ loadingMore│ │
│ └─────┬──────┘ │
│ │ │
│ success/fail │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ On loadMore fail: items preserved, loadMoreError set │ │
│ │ On refresh fail with items: items preserved, error set │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
Overlay Flags (覆盖层标志) #
| Flag | When Set | Behavior |
|---|---|---|
isRefreshing |
During refresh() |
Old data remains visible / 旧数据保持可见 |
isLoadingMore |
During loadMore() |
Existing items preserved / 现有数据保留 |
Adapters (适配器) #
Architecture (架构) #
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Fetcher │────►│ Engine │────►│ EasyStore │
│ (your API) │ │ EasyRequest │ │ (interface) │
└─────────────┘ │ EasyPager │ └──────┬──────┘
└─────────────┘ │
▼
┌──────────────────────────────────────┐
│ Adapter Implementations │
├──────────────┬───────────────────────┤
│ RxStore │ RiverpodStore (TBD) │
│ (GetX) │ BlocStore (TBD) │
└──────────────┴───────────────────────┘
GetX Adapter (GetX 适配器) #
// 1. Single value controller
class UserController extends EasyGetxController<User> {
@override
Future<User> onFetch() => api.getUser();
// Optional: customize behavior
@override
bool get autoFetch => true; // Auto-fetch on ready
@override
Duration get fetchDelay => Duration.zero; // Delay before auto-fetch
@override
void onError(Object error, StackTrace stack) {
// Handle errors
}
}
// 2. In your widget
class UserPage extends StatelessWidget {
final controller = Get.put(UserController());
@override
Widget build(BuildContext context) {
return controller.obx(
(user) => Text(user!.name),
onLoading: CircularProgressIndicator(),
onError: (error) => Text('Error: $error'),
onEmpty: Text('No user'),
);
}
}
// 3. Paginated controller
class ArticleListController extends EasyGetxPagedController<Article> {
@override
Future<List<Article>> onFetch({required int page}) {
return api.getArticles(page: page, pageSize: pageSize);
}
@override
int get pageSize => 20;
}
// 4. In your widget with infinite scroll
class ArticleListPage extends StatelessWidget {
final controller = Get.put(ArticleListController());
@override
Widget build(BuildContext context) {
return controller.obx(
(articles) => ListView.builder(
itemCount: articles!.length + 1,
itemBuilder: (context, index) {
if (index == articles.length) {
return controller.hasMore
? LoadMoreButton(onTap: controller.loadMore)
: SizedBox.shrink();
}
return ArticleCard(articles[index]);
},
),
);
}
}
Riverpod Adapter (Riverpod 适配器) #
// 1. Create a Notifier that wraps EasyRequest
class UserNotifier extends Notifier<EasyValueState<User>> {
late final EasyRequest<User> _engine;
@override
EasyValueState<User> build() {
_engine = EasyRequest<User>(
fetcher: () => ref.read(apiProvider).getUser(),
);
_engine.stream.listen((s) => state = s);
_engine.fetch();
ref.onDispose(_engine.dispose);
return EasyValueState<User>.initial();
}
Future<void> refresh() => _engine.refresh();
Future<void> retry() => _engine.retry();
}
final userProvider = NotifierProvider<UserNotifier, EasyValueState<User>>(
UserNotifier.new,
);
Custom Adapter Guide (自定义适配器指南) #
To create your own adapter, implement the EasyStore<S> interface:
要创建自定义适配器,实现 EasyStore<S> 接口:
/// The store interface - just 3 members!
abstract interface class EasyStore<S> {
S get value;
set value(S value);
void dispose();
}
Example: ValueNotifier adapter (示例:ValueNotifier 适配器)
class ValueNotifierStore<S> implements EasyStore<S> {
ValueNotifierStore(this._notifier);
final ValueNotifier<S> _notifier;
@override
S get value => _notifier.value;
@override
set value(S v) => _notifier.value = v;
@override
void dispose() {} // ValueNotifier managed externally
}
Usage (使用方式):
final notifier = ValueNotifier(EasyValueState<User>.initial());
final engine = EasyRequest<User>(
fetcher: () => api.getUser(),
store: ValueNotifierStore(notifier),
);
// Engine writes directly to notifier - no stream needed!
await engine.fetch();
print(notifier.value.data); // User data
Advanced (进阶) #
Concurrency Protection (并发保护) #
Engines use in-flight Future guards to prevent duplicate requests:
引擎使用 in-flight Future 保护机制防止重复请求:
// These calls return the SAME Future (no duplicate requests)
final f1 = engine.fetch();
final f2 = engine.fetch();
assert(identical(f1, f2)); // true
Empty Detection (空值检测) #
Built-in isValueEmpty() handles common types:
内置 isValueEmpty() 处理常见类型:
isValueEmpty(null); // true
isValueEmpty([]); // true
isValueEmpty({}); // true
isValueEmpty(''); // true
isValueEmpty('hello'); // false
isValueEmpty([1, 2, 3]); // false
Error Handling (错误处理) #
Stale data on error: When refresh() fails but old data exists, the data is preserved.
错误时保留旧数据:当 refresh() 失败但存在旧数据时,数据会被保留。
// state.data still contains old user
// state.status == EasyStatus.error
// state.error contains the exception
Separate loadMore errors: Pagination separates refresh errors from loadMore errors.
分离加载更多错误:分页引擎将刷新错误与加载更多错误分开处理。
// state.error - refresh/fetch error
// state.loadMoreError - loadMore error (items preserved)
FAQ (常见问题) #
Mermaid diagrams not rendering? (Mermaid 图表不显示?) #
Mermaid diagrams render on GitHub but not on pub.dev. ASCII diagrams are provided as fallback.
Mermaid 图表在 GitHub 上可渲染,但在 pub.dev 上不行。已提供 ASCII 图表作为备选。
When to use EasyRequest vs EasyPager? (何时使用 EasyRequest vs EasyPager?) #
| Use Case | Engine |
|---|---|
| Single object (user profile, settings) / 单个对象(用户资料、设置) | EasyRequest<T> |
| List with pagination / 带分页的列表 | EasyPager<T> |
| List without pagination / 不带分页的列表 | EasyRequest<List<T>> |
How is hasMore calculated? (hasMore 如何计算?) #
hasMore = fetchedItems.length >= pageSize
If your API returns fewer items than pageSize, hasMore becomes false.
如果 API 返回的数据少于 pageSize,hasMore 变为 false。
License #
MIT