getx_exten 2.0.3
getx_exten: ^2.0.3 copied to clipboard
A package with GetListener and GetConsumer for Getx Lovers
GetX Extensions ๐ #
A powerful state management extension for GetX that combines BLoC patterns with GetX's reactive superpowers. Build scalable Flutter apps with ease!
โจ Features #
- ๐ฏ Type Flexibility - Use ANY type as state (int, String, custom classes, etc.)
- ๐ฅ GetX Reactive - Direct
Rxsupport for lightweight reactive state - ๐๏ธ BLoC Patterns - Familiar Cubit and BLoC architecture
- โก Performance - Fine-grained reactivity with selectors
- ๐จ Rich Widgets - Specialized widgets for every use case
- ๐งช Well Tested - Comprehensive test suite
- ๐ฆ Zero Boilerplate - No base classes required
๐ฆ Installation #
Add to your pubspec.yaml:
dependencies:
getx_exten: ^2.0.3
get: ^4.6.5
๐ฏ Quick Start #
1. Create a Cubit (Simple State) #
class CounterCubit extends RxCubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
}
2. Use in Your Widget #
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cubit = Get.put(CounterCubit());
return Scaffold(
body: Center(
child: GetChanger<int>(
controller: cubit,
builder: (context, count) => Text('Count: $count'),
),
),
floatingActionButton: FloatingActionButton(
onPressed: cubit.increment,
child: Icon(Icons.add),
),
);
}
}
That's it! ๐
๐ Core Concepts #
RxCubit - Simple State Management #
Perfect for straightforward state that doesn't need events.
// Works with primitives
class CounterCubit extends RxCubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
}
// Works with custom classes
class User {
final String name;
final int age;
User(this.name, this.age);
}
class UserCubit extends RxCubit<User> {
UserCubit() : super(User('', 0));
void updateUser(String name, int age) {
emit(User(name, age));
}
}
// Works with nullable types
class SearchCubit extends RxCubit<String?> {
SearchCubit() : super(null);
void search(String query) => emit(query);
void clear() => emit(null);
}
// Works with collections
class TodosCubit extends RxCubit<List<String>> {
TodosCubit() : super([]);
void addTodo(String todo) => emit([...state, todo]);
}
RxBloc - Event-Driven State Management #
For complex state logic with events.
// Define events
abstract class CounterEvent {}
class Increment extends CounterEvent {}
class Decrement extends CounterEvent {}
class AddValue extends CounterEvent {
final int value;
AddValue(this.value);
}
// Create bloc
class CounterBloc extends RxBloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<Increment>((event, emit) async {
emit(state + 1);
});
on<Decrement>((event, emit) async {
emit(state - 1);
});
on<AddValue>((event, emit) async {
emit(state + event.value);
});
}
}
// Use in widget
final bloc = Get.put(CounterBloc());
bloc.add(Increment());
bloc.add(AddValue(5));
๐จ Widgets #
GetChanger - Simple Builder #
Rebuilds when state changes. Perfect for displaying state.
GetChanger<int>(
controller: counterCubit,
builder: (context, count) => Text('$count'),
)
// With buildWhen condition
GetChanger<int>(
controller: counterCubit,
buildWhen: (prev, curr) => curr % 2 == 0, // Only rebuild on even numbers
builder: (context, count) => Text('$count'),
)
// With direct Rx
final count = 0.obs;
GetChanger<int>(
rx: count,
builder: (context, value) => Text('$value'),
)
GetListenerWidget - Side Effects Only #
Listens to state without rebuilding. Perfect for navigation, snackbars, dialogs.
GetListenerWidget<int>(
controller: counterCubit,
listener: (context, count) {
if (count > 10) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Count exceeded 10!')),
);
}
},
child: MyWidget(),
)
// With listenWhen condition
GetListenerWidget<int>(
controller: counterCubit,
listenWhen: (prev, curr) => curr > prev, // Only listen on increase
listener: (context, count) {
print('Count increased: $count');
},
child: MyWidget(),
)
GetConsumer - Builder + Listener #
Combines building and listening. Perfect for complex UI with side effects.
GetConsumer<int>(
controller: counterCubit,
listener: (context, count) {
// Side effects
if (count == 10) {
showDialog(context: context, builder: (_) => AlertDialog(...));
}
},
builder: (context, count) {
// UI
return Text('$count');
},
)
// With independent conditions
GetConsumer<int>(
controller: counterCubit,
listenWhen: (prev, curr) => curr % 5 == 0, // Listen every 5
buildWhen: (prev, curr) => curr % 2 == 0, // Build every 2
listener: (context, count) => print('Multiple of 5: $count'),
builder: (context, count) => Text('$count'),
)
GetSelector - Fine-Grained Reactivity #
Only rebuilds when the selected value changes. HUGE performance boost!
class UserState {
final String name;
final int age;
final List<String> hobbies;
UserState(this.name, this.age, this.hobbies);
}
class UserCubit extends RxCubit<UserState> {
UserCubit() : super(UserState('', 0, []));
void updateName(String name) => emit(UserState(name, state.age, state.hobbies));
void updateAge(int age) => emit(UserState(state.name, age, state.hobbies));
}
// Only rebuilds when NAME changes (not age or hobbies!)
GetSelector<UserState, String>(
controller: userCubit,
selector: (state) => state.name,
builder: (context, name) => Text('Name: $name'),
)
// Using extension method (cleaner syntax)
userCubit.select(
(state) => state.age,
(context, age) => Text('Age: $age'),
)
// With primitive selectors
GetSelector<int, bool>(
controller: counterCubit,
selector: (count) => count > 10,
builder: (context, isHigh) => Text(isHigh ? 'High' : 'Low'),
)
GetMultiChanger - Multiple Sources #
Reacts to multiple state sources. Perfect for combining different cubits.
GetMultiChanger(
sources: [
Get.find<CartCubit>().rx,
Get.find<PriceCubit>().rx,
],
builder: (context) {
final cart = Get.find<CartCubit>();
final price = Get.find<PriceCubit>();
return Text('${cart.state.length} items - \$${price.state}');
},
)
// With mixed sources
final userCubit = Get.find<UserCubit>();
final count = 0.obs;
GetMultiChanger(
sources: [userCubit.rx, count],
builder: (context) => Text('${userCubit.state.name}: ${count.value}'),
)
๐ Advanced Usage #
Custom State Classes #
No inheritance required! Define your state however you want.
// Simple class
class TodoState {
final List<Todo> todos;
final bool isLoading;
TodoState(this.todos, this.isLoading);
}
// Sealed classes (recommended for complex states)
sealed class AuthState {}
class Authenticated extends AuthState {
final User user;
Authenticated(this.user);
}
class Unauthenticated extends AuthState {}
class AuthLoading extends AuthState {}
class AuthCubit extends RxCubit<AuthState> {
AuthCubit() : super(Unauthenticated());
Future<void> login(String email, String password) async {
emit(AuthLoading());
try {
final user = await authService.login(email, password);
emit(Authenticated(user));
} catch (e) {
emit(Unauthenticated());
}
}
}
Pattern Matching with Sealed Classes #
GetChanger<AuthState>(
controller: authCubit,
builder: (context, state) {
return switch (state) {
Authenticated(:final user) => HomePage(user: user),
Unauthenticated() => LoginPage(),
AuthLoading() => LoadingPage(),
};
},
)
Watching State Imperatively #
class MyController extends GetxController {
final counterCubit = CounterCubit();
late Worker _worker;
@override
void onInit() {
super.onInit();
// Watch state changes
_worker = counterCubit.watch((count) {
print('Count changed to: $count');
if (count == 10) {
// Do something
}
});
}
@override
void onClose() {
_worker.dispose();
super.onClose();
}
}
Combining Multiple Conditions #
GetConsumer<TodoState>(
controller: todoCubit,
// Rebuild only when todos list changes
buildWhen: (prev, curr) => prev.todos.length != curr.todos.length,
// Listen only when loading state changes
listenWhen: (prev, curr) => prev.isLoading != curr.isLoading,
listener: (context, state) {
if (!state.isLoading) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Todos loaded!')),
);
}
},
builder: (context, state) {
if (state.isLoading) return CircularProgressIndicator();
return ListView.builder(
itemCount: state.todos.length,
itemBuilder: (context, index) => TodoItem(state.todos[index]),
);
},
)
๐ฏ Best Practices #
1. Use the Right Widget for the Job #
// โ
Good - Use GetChanger for simple display
GetChanger<int>(
controller: cubit,
builder: (context, count) => Text('$count'),
)
// โ
Good - Use GetListenerWidget for side effects
GetListenerWidget<String>(
controller: messageCubit,
listener: (context, message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
},
child: MyWidget(),
)
// โ Bad - Don't use GetConsumer when you only need building
GetConsumer<int>(
controller: cubit,
listener: (context, count) {}, // Empty listener
builder: (context, count) => Text('$count'),
)
2. Leverage Selectors for Performance #
// โ Bad - Rebuilds when ANY field changes
GetChanger<UserState>(
controller: userCubit,
builder: (context, state) => Text(state.name),
)
// โ
Good - Only rebuilds when name changes
GetSelector<UserState, String>(
controller: userCubit,
selector: (state) => state.name,
builder: (context, name) => Text(name),
)
3. Use Direct Rx for Simple Cases #
// For simple reactive values, skip the cubit
class MyController extends GetxController {
final count = 0.obs;
final name = 'John'.obs;
final isLoading = false.obs;
void increment() => count.value++;
}
// Use directly
GetChanger<int>(
rx: controller.count,
builder: (context, count) => Text('$count'),
)
4. Combine with GetX Dependency Injection #
// In your binding or main.dart
Get.put(CounterCubit());
Get.lazyPut(() => UserCubit());
// Access anywhere
final counterCubit = Get.find<CounterCubit>();
5. Proper Disposal #
class MyPage extends StatefulWidget {
@override
State<MyPage> createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
late final CounterCubit cubit;
@override
void initState() {
super.initState();
cubit = CounterCubit();
}
@override
void dispose() {
cubit.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GetChanger<int>(
controller: cubit,
builder: (context, count) => Text('$count'),
);
}
}
๐งช Testing #
All widgets are fully testable:
testWidgets('GetChanger rebuilds on state change', (tester) async {
final cubit = CounterCubit();
await tester.pumpWidget(
MaterialApp(
home: GetChanger<int>(
controller: cubit,
builder: (context, count) => Text('$count'),
),
),
);
expect(find.text('0'), findsOneWidget);
cubit.increment();
await tester.pump();
expect(find.text('1'), findsOneWidget);
});
๐ Comparison with Other Solutions #
| Feature | GetX Extensions | flutter_bloc | GetX Alone |
|---|---|---|---|
| Type Flexibility | โ Any type | โ Any type | โ Any type |
| Fine-grained Selectors | โ Built-in | โ Via BlocSelector | โ Manual |
| Multi-source Reactivity | โ GetMultiChanger | โ Manual | โ Obx |
| Performance | โก Excellent | โก Excellent | โก Excellent |
| Boilerplate | ๐ข Low | ๐ก Medium | ๐ข Low |
| Learning Curve | ๐ข Easy | ๐ก Medium | ๐ข Easy |
| BLoC Pattern | โ Optional | โ Core | โ Not built-in |
| Direct Rx Support | โ Yes | โ No | โ Yes |
๐ค Contributing #
Contributions are welcome! Please read our contributing guidelines first.
๐ License #
This project is licensed under the MIT License - see the LICENSE file for details.
๐ Acknowledgments #
- Built on top of the amazing GetX package
- Inspired by flutter_bloc
- Thanks to all contributors!
๐ Support #
- ๐ง Email: edkluivert@gmail.com
- ๐ Issues: GitHub Issues
- ๐ฌ Discussions: GitHub Discussions
Made with โค๏ธ by the GetX Extensions team