Refactoring workflow for Flutter applications with safety guarantees and test-driven improvements
Performs systematic refactoring of Flutter apps with test-driven safety guarantees and incremental changes.
/plugin marketplace add Kaakati/rails-enterprise-dev/plugin install reactree-flutter-dev@manifest-marketplaceSystematic refactoring workflow for improving Flutter application code structure while maintaining functionality and ensuring safety through comprehensive testing.
/flutter-refactor [refactoring description]
/flutter-refactor extract common form validation logic into reusable service
/flutter-refactor split large UserController into separate profile and settings controllers
/flutter-refactor move API endpoints to centralized configuration
/flutter-refactor convert stateful widgets to GetX controllers
/flutter-refactor extract repeated UI patterns into reusable components
/flutter-refactor improve error handling across all repositories
This command performs safe, test-driven refactoring:
Before:
class UserController extends GetxController {
final _isLoading = false.obs;
final _error = Rx<String?>(null);
Future<void> validateAndSaveProfile(String name, String email) async {
// Complex validation logic
if (name.isEmpty || name.length < 2) {
_error.value = 'Name must be at least 2 characters';
return;
}
if (!email.contains('@') || !email.contains('.')) {
_error.value = 'Invalid email format';
return;
}
// Additional validation...
_isLoading.value = true;
// Save logic...
}
}
After:
// New service
class ValidationService {
Either<String, String> validateName(String name) {
if (name.isEmpty || name.length < 2) {
return Left('Name must be at least 2 characters');
}
return Right(name);
}
Either<String, String> validateEmail(String email) {
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(email)) {
return Left('Invalid email format');
}
return Right(email);
}
}
// Simplified controller
class UserController extends GetxController {
final ValidationService _validationService;
final _isLoading = false.obs;
final _error = Rx<String?>(null);
UserController(this._validationService);
Future<void> validateAndSaveProfile(String name, String email) async {
final nameValidation = _validationService.validateName(name);
if (nameValidation.isLeft()) {
_error.value = nameValidation.fold((l) => l, (r) => null);
return;
}
final emailValidation = _validationService.validateEmail(email);
if (emailValidation.isLeft()) {
_error.value = emailValidation.fold((l) => l, (r) => null);
return;
}
_isLoading.value = true;
// Save logic...
}
}
Tests Added:
// test/services/validation_service_test.dart
void main() {
late ValidationService validationService;
setUp(() {
validationService = ValidationService();
});
group('validateName', () {
test('returns error for empty name', () {
final result = validationService.validateName('');
expect(result.isLeft(), true);
});
test('returns error for name less than 2 characters', () {
final result = validationService.validateName('A');
expect(result.isLeft(), true);
});
test('returns success for valid name', () {
final result = validationService.validateName('John');
expect(result.isRight(), true);
});
});
group('validateEmail', () {
test('returns error for invalid email', () {
final result = validationService.validateEmail('invalid');
expect(result.isLeft(), true);
});
test('returns success for valid email', () {
final result = validationService.validateEmail('test@example.com');
expect(result.isRight(), true);
});
});
}
Before:
class UserController extends GetxController {
// Profile management
final _user = Rx<User?>(null);
final _isLoadingProfile = false.obs;
Future<void> loadProfile() async { /* ... */ }
Future<void> updateProfile(User user) async { /* ... */ }
Future<void> uploadAvatar(File file) async { /* ... */ }
// Settings management
final _settings = Rx<Settings?>(null);
final _isLoadingSettings = false.obs;
Future<void> loadSettings() async { /* ... */ }
Future<void> updateSettings(Settings settings) async { /* ... */ }
// Notification preferences
final _notificationEnabled = true.obs;
final _emailEnabled = true.obs;
void toggleNotifications() { /* ... */ }
void toggleEmail() { /* ... */ }
// Account management
Future<void> deleteAccount() async { /* ... */ }
Future<void> exportData() async { /* ... */ }
}
After:
// Separate controllers by responsibility
class ProfileController extends GetxController {
final GetUserProfileUseCase _getUserProfileUseCase;
final UpdateProfileUseCase _updateProfileUseCase;
final UploadAvatarUseCase _uploadAvatarUseCase;
final _user = Rx<User?>(null);
final _isLoading = false.obs;
User? get user => _user.value;
bool get isLoading => _isLoading.value;
ProfileController(
this._getUserProfileUseCase,
this._updateProfileUseCase,
this._uploadAvatarUseCase,
);
Future<void> loadProfile() async {
_isLoading.value = true;
final result = await _getUserProfileUseCase();
result.fold(
(failure) => _handleError(failure),
(user) => _user.value = user,
);
_isLoading.value = false;
}
Future<void> updateProfile(User user) async { /* ... */ }
Future<void> uploadAvatar(File file) async { /* ... */ }
}
class SettingsController extends GetxController {
final GetSettingsUseCase _getSettingsUseCase;
final UpdateSettingsUseCase _updateSettingsUseCase;
final _settings = Rx<Settings?>(null);
final _isLoading = false.obs;
Settings? get settings => _settings.value;
bool get isLoading => _isLoading.value;
SettingsController(
this._getSettingsUseCase,
this._updateSettingsUseCase,
);
Future<void> loadSettings() async { /* ... */ }
Future<void> updateSettings(Settings settings) async { /* ... */ }
}
class NotificationController extends GetxController {
final _notificationEnabled = true.obs;
final _emailEnabled = true.obs;
bool get notificationEnabled => _notificationEnabled.value;
bool get emailEnabled => _emailEnabled.value;
void toggleNotifications() {
_notificationEnabled.value = !_notificationEnabled.value;
_savePreferences();
}
void toggleEmail() {
_emailEnabled.value = !_emailEnabled.value;
_savePreferences();
}
Future<void> _savePreferences() async { /* ... */ }
}
Bindings Updated:
// Before
class UserBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut(() => UserController(/* many dependencies */));
}
}
// After
class ProfileBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut(() => ProfileController(
Get.find(),
Get.find(),
Get.find(),
));
}
}
class SettingsBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut(() => SettingsController(
Get.find(),
Get.find(),
));
}
}
Before:
class ProductListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context, index) {
return Card(
elevation: 2,
margin: EdgeInsets.all(8),
child: ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(products[index].imageUrl),
),
title: Text(
products[index].name,
style: TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text('\$${products[index].price.toStringAsFixed(2)}'),
trailing: IconButton(
icon: Icon(Icons.add_shopping_cart),
onPressed: () => addToCart(products[index]),
),
),
);
},
);
}
}
After:
// Extracted reusable widget
class ProductCard extends StatelessWidget {
final Product product;
final VoidCallback onAddToCart;
const ProductCard({
Key? key,
required this.product,
required this.onAddToCart,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
margin: const EdgeInsets.all(8),
child: ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(product.imageUrl),
),
title: Text(
product.name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text('\$${product.price.toStringAsFixed(2)}'),
trailing: IconButton(
icon: const Icon(Icons.add_shopping_cart),
onPressed: onAddToCart,
),
),
);
}
}
// Simplified usage
class ProductListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context, index) {
return ProductCard(
product: products[index],
onAddToCart: () => addToCart(products[index]),
);
},
);
}
}
Widget Test Added:
// test/presentation/widgets/product_card_test.dart
void main() {
testWidgets('ProductCard displays product information', (tester) async {
final product = Product(
id: '1',
name: 'Test Product',
price: 29.99,
imageUrl: 'https://example.com/image.jpg',
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ProductCard(
product: product,
onAddToCart: () {},
),
),
),
);
expect(find.text('Test Product'), findsOneWidget);
expect(find.text('\$29.99'), findsOneWidget);
expect(find.byIcon(Icons.add_shopping_cart), findsOneWidget);
});
testWidgets('ProductCard calls onAddToCart when button tapped', (tester) async {
var called = false;
final product = Product(id: '1', name: 'Test', price: 10.0, imageUrl: '');
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ProductCard(
product: product,
onAddToCart: () => called = true,
),
),
),
);
await tester.tap(find.byIcon(Icons.add_shopping_cart));
expect(called, true);
});
}
Before:
class UserRepositoryImpl implements UserRepository {
final UserRemoteDataSource remoteDataSource;
@override
Future<Either<Failure, User>> getUser(String id) async {
try {
final userModel = await remoteDataSource.getUser(id);
return Right(userModel.toEntity());
} catch (e) {
return Left(ServerFailure()); // Generic error
}
}
}
After:
class UserRepositoryImpl implements UserRepository {
final UserRemoteDataSource remoteDataSource;
final NetworkInfo networkInfo;
final Logger logger;
@override
Future<Either<Failure, User>> getUser(String id) async {
// Check network connectivity
if (!await networkInfo.isConnected) {
logger.warning('No network connection');
return Left(NetworkFailure(message: 'No internet connection'));
}
try {
final userModel = await remoteDataSource.getUser(id);
logger.info('Successfully fetched user: $id');
return Right(userModel.toEntity());
} on ServerException catch (e) {
logger.error('Server error fetching user', e);
return Left(ServerFailure(
message: e.message ?? 'Server error occurred',
statusCode: e.statusCode,
));
} on TimeoutException catch (e) {
logger.error('Request timeout for user', e);
return Left(NetworkFailure(message: 'Request timed out'));
} on FormatException catch (e) {
logger.error('Invalid response format', e);
return Left(ServerFailure(message: 'Invalid server response'));
} catch (e, stackTrace) {
logger.error('Unexpected error fetching user', e, stackTrace);
return Left(UnexpectedFailure(
message: 'An unexpected error occurred',
exception: e,
));
}
}
}
Before:
// Scattered throughout codebase
class UserProvider {
final baseUrl = 'https://api.example.com';
// ...
}
class ProductProvider {
final baseUrl = 'https://api.example.com';
// ...
}
After:
// Centralized configuration
class AppConfig {
static const String apiBaseUrl = String.fromEnvironment(
'API_BASE_URL',
defaultValue: 'https://api.example.com',
);
static const Duration apiTimeout = Duration(seconds: 10);
static const int maxRetries = 3;
static const String storageKey = 'app_storage';
static const Duration cacheExpiry = Duration(hours: 1);
}
// Usage
class UserProvider {
final http.Client client;
final String baseUrl;
UserProvider({
required this.client,
this.baseUrl = AppConfig.apiBaseUrl,
});
Future<UserModel> getUser(String id) async {
final response = await client.get(
Uri.parse('$baseUrl/users/$id'),
).timeout(AppConfig.apiTimeout);
// ...
}
}
Before refactoring:
During refactoring:
After refactoring:
When the user invokes /flutter-refactor [description]:
/flutter-dev to add new features after refactoring/flutter-debug if refactoring introduces issues/flutter-feature to rebuild features with better architecture// DON'T: Rewrite entire module at once
// This makes debugging impossible if something breaks
// DO: Refactor piece by piece with tests
// 1. Extract method A, test
// 2. Extract method B, test
// 3. Combine into new structure, test
// DON'T: Change code without safety net
// Risk: Breaking functionality without knowing
// DO: Write/verify tests first
// 1. Ensure existing tests pass
// 2. Add missing tests
// 3. Refactor with confidence
// 4. Verify tests still pass
// DON'T: Add features while refactoring
// Hard to track what caused issues
// DO: Refactor in dedicated commit
// 1. Commit: "Refactor UserController into smaller controllers"
// 2. Commit: "Add new profile feature"