easy_state_getx
GetX adapter for easy_state_core. Provides seamless integration between easy_state's request-driven state management and GetX's reactive system.
Features
- EasyGetxController - Single value async request controller with
StateMixinintegration - EasyGetxPagedController - Pagination controller with infinite scroll support
- RxStore - GetX
Rx<T>backed state store - Auto-sync with
obx()- State automatically maps toRxStatusfor easy UI binding - Stale-while-revalidate - Old data stays visible during refresh
- Concurrency protection - Duplicate requests automatically deduplicated
Installation
Add to your pubspec.yaml:
dependencies:
easy_state_getx: ^0.1.0
Then run:
flutter pub get
Quick Start
Single Value Request
import 'package:easy_state_getx/easy_state_getx.dart';
// 1. Define your controller
class UserController extends EasyGetxController<User> {
final UserApi api;
UserController(this.api);
@override
Future<User> onFetch() => api.getUser();
}
// 2. Register and use in widget
class UserPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final controller = Get.put(UserController(Get.find()));
return Scaffold(
body: controller.obx(
(user) => UserCard(user: user!),
onLoading: const Center(child: CircularProgressIndicator()),
onEmpty: const Center(child: Text('No user found')),
onError: (error) => Center(child: Text('Error: $error')),
),
floatingActionButton: FloatingActionButton(
onPressed: controller.refreshData,
child: const Icon(Icons.refresh),
),
);
}
}
Paginated Request
// 1. Define paginated controller
class ArticleListController extends EasyGetxPagedController<Article> {
final ArticleApi api;
ArticleListController(this.api);
@override
int get pageSize => 20;
@override
Future<List<Article>> onFetch({required int page}) {
return api.getArticles(page: page, pageSize: pageSize);
}
}
// 2. Use with infinite scroll
class ArticleListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final controller = Get.put(ArticleListController(Get.find()));
return Scaffold(
body: controller.obx(
(articles) => ListView.builder(
itemCount: articles!.length + 1,
itemBuilder: (context, index) {
// Load more trigger
if (index == articles.length) {
if (controller.isLoadingMore) {
return const Center(child: CircularProgressIndicator());
}
if (controller.hasMore) {
return TextButton(
onPressed: controller.loadMore,
child: const Text('Load More'),
);
}
return const SizedBox.shrink();
}
return ArticleTile(articles[index]);
},
),
onLoading: const Center(child: CircularProgressIndicator()),
onEmpty: const Center(child: Text('No articles')),
onError: (error) => Center(child: Text('Error: $error')),
),
);
}
}
API Reference
EasyGetxController<T>
Base controller for single value async requests.
| Property/Method | Type | Description |
|---|---|---|
autoFetch |
bool |
Auto-fetch when controller is ready. Default: true |
fetchDelay |
Duration |
Delay before auto-fetch. Default: Duration.zero |
easyState |
Rx<EasyValueState<T>> |
Reactive state object |
onFetch() |
Future<T> |
Required. Implement your fetch logic |
onError(error, stack) |
void |
Optional error callback |
formatError(error) |
String |
Format error for RxStatus. Default: error.toString() |
fetch() |
Future<void> |
Trigger initial fetch |
refreshData() |
Future<void> |
Refresh with stale-while-revalidate |
retry() |
Future<void> |
Retry when in error state |
State Access:
controller.easyState.value.status // EasyStatus (idle/loading/success/empty/error)
controller.easyState.value.data // T? - your data
controller.easyState.value.error // Object? - error if any
controller.easyState.value.isRefreshing // bool - refreshing with old data visible
EasyGetxPagedController<T>
Base controller for paginated async requests.
| Property/Method | Type | Description |
|---|---|---|
autoFetch |
bool |
Auto-fetch when ready. Default: true |
fetchDelay |
Duration |
Delay before auto-fetch. Default: Duration.zero |
pageSize |
int |
Items per page. Default: 20 |
easyState |
Rx<EasyPagedState<T>> |
Reactive pagination state |
onFetch({required int page}) |
Future<List<T>> |
Required. Fetch page (1-based) |
onError(error, stack) |
void |
Refresh/fetch error callback |
onLoadMoreError(error, stack) |
void |
Load more error callback |
refreshData() |
Future<void> |
Refresh from page 1 |
loadMore() |
Future<void> |
Load next page |
retryLoadMore() |
Future<void> |
Retry failed loadMore |
resetPagination() |
void |
Reset to initial state |
Convenience Accessors:
controller.items // List<T> - all loaded items
controller.page // int - current page number
controller.hasMore // bool - more pages available
controller.isRefreshing // bool - refreshing first page
controller.isLoadingMore// bool - loading next page
controller.error // Object? - refresh/fetch error
controller.loadMoreError// Object? - loadMore error
RxStore<S>
GetX Rx<S> backed implementation of EasyStore<S>.
final rx = EasyValueState<User>.initial().obs;
final store = RxStore<EasyValueState<User>>(rx, onSet: (state) {
// Called when state changes
});
GetX Extensions
EasyGetXExtension on GetInterface:
// Get or create controller
final controller = Get.getOrPut<UserController>(() => UserController());
// Execute action only if registered
Get.doIfRegistered<UserController, void>((c) => c.refreshData());
// Safe delete (no error if not registered)
await Get.safeDelete<UserController>();
EasyStatusExtension on EasyStatus:
EasyStatus.loading.toRxStatus(hasData: false); // RxStatus.loading()
EasyStatus.success.toRxStatus(hasData: true); // RxStatus.success()
EasyStatus.error.toRxStatus(hasData: false, 'Error msg'); // RxStatus.error('Error msg')
Complete Example
User Profile with Pull-to-Refresh
class UserProfileController extends EasyGetxController<UserProfile> {
final UserRepository repo;
UserProfileController(this.repo);
@override
Future<UserProfile> onFetch() => repo.fetchProfile();
@override
void onError(Object error, StackTrace stack) {
Get.snackbar('Error', formatError(error));
}
@override
String formatError(Object error) {
if (error is NetworkException) return 'Network error. Please retry.';
if (error is AuthException) return 'Session expired. Please login.';
return 'Something went wrong';
}
}
class UserProfilePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final c = Get.put(UserProfileController(Get.find()));
return Scaffold(
appBar: AppBar(title: const Text('Profile')),
body: RefreshIndicator(
onRefresh: c.refreshData,
child: c.obx(
(profile) => ListView(
children: [
CircleAvatar(backgroundImage: NetworkImage(profile!.avatarUrl)),
Text(profile.name, style: Theme.of(context).textTheme.headlineMedium),
Text(profile.email),
Text('Joined: ${profile.joinedDate}'),
],
),
onLoading: const Center(child: CircularProgressIndicator()),
onError: (error) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(error ?? 'Unknown error'),
ElevatedButton(onPressed: c.retry, child: const Text('Retry')),
],
),
),
),
),
);
}
}
Infinite Scroll List with Error Handling
class ProductListController extends EasyGetxPagedController<Product> {
final ProductApi api;
ProductListController(this.api);
@override
int get pageSize => 15;
@override
Future<List<Product>> onFetch({required int page}) {
return api.getProducts(page: page, limit: pageSize);
}
@override
void onLoadMoreError(Object error, StackTrace stack) {
Get.snackbar('Load More Failed', 'Tap to retry');
}
}
class ProductListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final c = Get.put(ProductListController(Get.find()));
return Scaffold(
appBar: AppBar(title: const Text('Products')),
body: RefreshIndicator(
onRefresh: c.refreshData,
child: c.obx(
(products) => NotificationListener<ScrollNotification>(
onNotification: (notification) {
// Auto load more when near bottom
if (notification is ScrollEndNotification &&
notification.metrics.extentAfter < 200 &&
c.hasMore &&
!c.isLoadingMore) {
c.loadMore();
}
return false;
},
child: ListView.builder(
itemCount: products!.length + 1,
itemBuilder: (context, index) {
if (index == products.length) {
return _buildFooter(c);
}
return ProductCard(products[index]);
},
),
),
onLoading: const Center(child: CircularProgressIndicator()),
onEmpty: const Center(child: Text('No products available')),
onError: (error) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 48),
Text(error ?? 'Failed to load'),
ElevatedButton(
onPressed: c.refreshData,
child: const Text('Retry'),
),
],
),
),
),
),
);
}
Widget _buildFooter(ProductListController c) {
return Obx(() {
if (c.isLoadingMore) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
);
}
if (c.loadMoreError != null) {
return Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: TextButton.icon(
onPressed: c.retryLoadMore,
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
),
),
);
}
if (!c.hasMore) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(child: Text('No more items')),
);
}
return const SizedBox.shrink();
});
}
}
RxStatus Mapping
The controller automatically maps EasyStatus to GetX's RxStatus:
| EasyStatus | Has Data | RxStatus |
|---|---|---|
idle |
No | empty() |
idle |
Yes | success() |
loading |
No | loading() |
loading |
Yes | success() |
success |
Yes | success() |
empty |
- | empty() |
error |
No | error(message) |
error |
Yes | success() (stale data preserved) |
| Refreshing | Yes | loadingMore() |
License
BSD 3-Clause License. See LICENSE for details.
Libraries
- easy_state_getx
- GetX adapter for easy_state_core.