Read Time: ⏱️ 10 minutes
Step-by-step guide for migrating between BLoC and GetX.
- ✅ Reduce boilerplate code (40-60% reduction)
- ✅ Faster development speed
- ✅ Simpler architecture
- ✅ Built-in routing and DI
# pubspec.yaml
dependencies:
# Remove or keep BLoC temporarily
# flutter_bloc: ^8.1.3 # Can coexist during migration
# hydrated_bloc: ^9.1.2
# Add GetX
get: ^4.6.6
get_storage: ^2.1.1Run: flutter pub get
Before (BLoC):
// State classes
abstract class CounterState extends Equatable {}
class CounterInitial extends CounterState {
@override
List<Object> get props => [];
}
class CounterLoaded extends CounterState {
final int count;
CounterLoaded(this.count);
@override
List<Object> get props => [count];
}
class CounterLoading extends CounterState {
@override
List<Object> get props => [];
}
class CounterError extends CounterState {
final String message;
CounterError(this.message);
@override
List<Object> get props => [message];
}After (GetX):
// No separate state classes needed
class CounterController extends GetxController {
final count = 0.obs;
final isLoading = false.obs;
final errorMessage = ''.obs;
// State is managed through reactive variables
bool get hasError => errorMessage.value.isNotEmpty;
bool get isInitial => count.value == 0 && !isLoading.value;
}Before (BLoC):
class CounterCubit extends Cubit<CounterState> {
final CounterRepository repository;
CounterCubit({required this.repository}) : super(CounterInitial());
Future<void> loadCounter() async {
emit(CounterLoading());
try {
final count = await repository.getCount();
emit(CounterLoaded(count));
} catch (e) {
emit(CounterError(e.toString()));
}
}
void increment() {
if (state is CounterLoaded) {
final currentState = state as CounterLoaded;
emit(CounterLoaded(currentState.count + 1));
}
}
}After (GetX):
class CounterController extends GetxController {
final CounterRepository repository;
CounterController({required this.repository});
final count = 0.obs;
final isLoading = false.obs;
final errorMessage = ''.obs;
@override
void onInit() {
super.onInit();
loadCounter();
}
Future<void> loadCounter() async {
isLoading.value = true;
errorMessage.value = '';
try {
count.value = await repository.getCount();
} catch (e) {
errorMessage.value = e.toString();
} finally {
isLoading.value = false;
}
}
void increment() => count.value++;
}Before (BLoC):
BlocBuilder<CounterCubit, CounterState>(
builder: (context, state) {
if (state is CounterLoading) {
return CircularProgressIndicator();
}
if (state is CounterLoaded) {
return Text('${state.count}');
}
if (state is CounterError) {
return Text('Error: ${state.message}');
}
return Container();
},
)After (GetX):
Obx(() {
if (controller.isLoading.value) {
return CircularProgressIndicator();
}
if (controller.hasError) {
return Text('Error: ${controller.errorMessage}');
}
return Text('${controller.count}');
})Before (BLoC):
BlocListener<CounterCubit, CounterState>(
listener: (context, state) {
if (state is CounterLoaded && state.count == 10) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Reached 10!')),
);
}
},
child: Container(),
)After (GetX):
class CounterController extends GetxController {
final count = 0.obs;
@override
void onInit() {
super.onInit();
// Listen for specific values
ever(count, (value) {
if (value == 10) {
Get.snackbar('Milestone', 'Reached 10!');
}
});
}
}Before (BLoC):
// Push
Navigator.of(context).pushNamed('/details');
// Push with arguments
Navigator.of(context).pushNamed(
'/details',
arguments: {'id': 123},
);
// Pop
Navigator.of(context).pop();After (GetX):
// Push
Get.toNamed('/details');
// Push with arguments
Get.toNamed('/details', arguments: {'id': 123});
// Pop
Get.back();
// Named routes with bindings
GetPage(
name: '/details',
page: () => DetailsView(),
binding: DetailsBinding(),
)Before (BLoC with get_it):
// Setup
final getIt = GetIt.instance;
void setupDependencies() {
getIt.registerSingleton<CounterRepository>(
CounterRepositoryImpl(),
);
getIt.registerFactory<CounterCubit>(
() => CounterCubit(repository: getIt()),
);
}
// Usage
final cubit = getIt<CounterCubit>();After (GetX):
// Setup with Bindings
class HomeBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<CounterRepository>(() => CounterRepositoryImpl());
Get.lazyPut(() => CounterController(repository: Get.find()));
}
}
// Usage
final controller = Get.find<CounterController>();
// Or in widget
final controller = Get.put(CounterController(repository: Get.find()));Before (BLoC):
class CounterCubit extends HydratedCubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
@override
int? fromJson(Map<String, dynamic> json) {
return json['value'] as int;
}
@override
Map<String, dynamic>? toJson(int state) {
return {'value': state};
}
}After (GetX):
class CounterController extends GetxController {
final _storage = GetStorage();
final count = 0.obs;
@override
void onInit() {
super.onInit();
// Load from storage
count.value = _storage.read('count') ?? 0;
// Save on change
ever(count, (value) {
_storage.write('count', value);
});
}
}- Add GetX dependencies
- Convert state classes to reactive variables
- Convert Cubits/Blocs to Controllers
- Replace BlocBuilder with Obx
- Replace BlocListener with workers
- Update navigation calls
- Set up Bindings for DI
- Convert state persistence
- Update tests
- Remove BLoC dependencies (when done)
- ✅ Better type safety
- ✅ Explicit state management
- ✅ Superior testing infrastructure
- ✅ Better for large teams
# pubspec.yaml
dependencies:
flutter_bloc: ^8.1.3
bloc: ^8.1.2
hydrated_bloc: ^9.1.2
equatable: ^2.0.5
# Keep GetX temporarily for gradual migration
# get: ^4.6.6Before (GetX):
class CounterController extends GetxController {
final count = 0.obs;
final isLoading = false.obs;
final errorMessage = ''.obs;
}After (BLoC):
// Define all possible states
abstract class CounterState extends Equatable {
const CounterState();
}
class CounterInitial extends CounterState {
const CounterInitial();
@override
List<Object> get props => [];
}
class CounterLoading extends CounterState {
const CounterLoading();
@override
List<Object> get props => [];
}
class CounterLoaded extends CounterState {
final int count;
const CounterLoaded(this.count);
@override
List<Object> get props => [count];
}
class CounterError extends CounterState {
final String message;
const CounterError(this.message);
@override
List<Object> get props => [message];
}After (BLoC):
class CounterCubit extends Cubit<CounterState> {
final CounterRepository repository;
CounterCubit({required this.repository})
: super(const CounterInitial());
Future<void> loadCounter() async {
emit(const CounterLoading());
try {
final count = await repository.getCount();
emit(CounterLoaded(count));
} catch (e) {
emit(CounterError(e.toString()));
}
}
void increment() {
if (state is CounterLoaded) {
final currentCount = (state as CounterLoaded).count;
emit(CounterLoaded(currentCount + 1));
}
}
}Before (GetX):
Obx(() {
if (controller.isLoading.value) {
return CircularProgressIndicator();
}
return Text('${controller.count}');
})After (BLoC):
BlocBuilder<CounterCubit, CounterState>(
builder: (context, state) {
if (state is CounterLoading) {
return CircularProgressIndicator();
}
if (state is CounterLoaded) {
return Text('${state.count}');
}
return Container();
},
)Before (GetX):
ever(count, (value) {
if (value == 10) {
Get.snackbar('Milestone', 'Reached 10!');
}
});After (BLoC):
BlocListener<CounterCubit, CounterState>(
listener: (context, state) {
if (state is CounterLoaded && state.count == 10) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Reached 10!')),
);
}
},
child: Container(),
)After (BLoC with get_it):
final getIt = GetIt.instance;
void setupDependencies() {
// Repositories
getIt.registerLazySingleton<CounterRepository>(
() => CounterRepositoryImpl(),
);
// Cubits
getIt.registerFactory<CounterCubit>(
() => CounterCubit(repository: getIt()),
);
}
// In main.dart
void main() {
setupDependencies();
runApp(MyApp());
}- Add BLoC dependencies
- Define state classes for each feature
- Create Cubits/Blocs
- Replace Obx with BlocBuilder
- Replace workers with BlocListener
- Set up dependency injection (get_it)
- Update navigation (if using GetX routing)
- Convert storage (GetStorage → HydratedBloc)
- Update all tests
- Remove GetX dependencies (when done)
Week 1-2:
- Set up new state manager
- Migrate 1-2 simple features
- Test thoroughly
Week 3-4:
- Migrate complex features
- Update tests
- Fix any issues
Week 5-6:
- Complete remaining features
- Remove old dependencies
- Final testing
Migrating everything at once is risky:
- ❌ High chance of bugs
- ❌ Difficult to test
- ❌ Team disruption
- ❌ Longer downtime
Only do this for very small apps (< 10 screens)
// Write tests before migrating
test('counter increments', () {
// This test should pass with both implementations
});const useBLoC = true;
Widget build(BuildContext context) {
return useBLoC
? BlocBuilder<CounterCubit, CounterState>(...)
: Obx(() => ...);
}Don't migrate by layer. Migrate complete features:
- ✅ Counter feature (all layers)
- ✅ Notes feature (all layers)
- ❌ All controllers first (incomplete features)
You can run BLoC and GetX side-by-side during migration
Pitfall 1: Forgetting to dispose
// GetX controllers need disposal
@override
void onClose() {
// Clean up
super.onClose();
}Pitfall 2: Not using .value
// Wrong
Text('${controller.count}')
// Correct
Text('${controller.count.value}')Pitfall 1: Missing state checks
// Wrong - will crash if state is not CounterLoaded
final count = (state as CounterLoaded).count;
// Correct
if (state is CounterLoaded) {
final count = state.count;
}Pitfall 2: Forgetting Equatable
// Wrong - will rebuild unnecessarily
class CounterLoaded extends CounterState {
final int count;
CounterLoaded(this.count);
}
// Correct
class CounterLoaded extends CounterState {
final int count;
CounterLoaded(this.count);
@override
List<Object> get props => [count];
}| App Size | To GetX | To Riverpod | To BLoC |
|---|---|---|---|
| Small (5-10 screens) | 1 week | 1-2 weeks | 2 weeks |
| Medium (10-50 screens) | 2-4 weeks | 3-5 weeks | 4-8 weeks |
| Large (50+ screens) | 1-3 months | 2-4 months | 2-4 months |
- All features working correctly
- All tests passing
- Performance verified
- Memory usage checked
- Build size acceptable
- Team trained on new approach
- Documentation updated
- Old dependencies removed
- CI/CD updated
- Code review completed
- ✅ Both directions are feasible
- ✅ Can be done gradually
- ✅ Doesn't require full rewrite
- ✅ Estimate time correctly
- ✅ Test thoroughly
- ✅ Migrate feature-by-feature
- ✅ Keep team informed
- ✅ Hybrid approach
- ✅ Keep current solution if working
- ✅ Migrate only new features
← Previous: Decision Guide | Back to Navigation
Last Updated: November 12, 2025