Yet another state management solution for Flutter applications.
Base Core is an opinionated state management solution designed for large-scale Flutter applications. It provides a collection of utility classes that work together seamlessly to manage application state, handle use cases, and implement the BLoC (Business Logic Component) pattern.
Note: This library heavily depends on dartz and rxdart
- BLoC pattern implementation
- Reactive state management
- Use case execution framework
- Error handling and retry mechanisms
- Activity tracking
- Structured logging
A foundational implementation of the BLoC pattern that provides:
- Automatic subscription management
- Built-in logging
- Disposable resources cleanup
A powerful state management solution that offers:
- Centralized state management
- Use case execution
- Automatic retry mechanism for failed operations
- Activity indication
- Error handling
- Stream-based state updates
Abstract classes for implementing business logic:
UseCase<P, R>
: For single-execution use casesStreamingUseCase<P, R>
: For continuous data streamsDataManagerUseCase<P, R>
: Specialized use cases for DataManager
Add this to your package's pubspec.yaml
file:
dependencies:
base_core: ^1.0.0
class GetUserProfile extends UseCase<String, UserProfile> {
final UserRepository userRepository;
GetUserProfile(this.userRepository);
@override
Future<Either<Failure, UserProfile>> execute(String userId) async {
try {
final profile = await userRepository.getProfile(userId);
return right(profile);
} catch (e) {
return left(UnexpectedFailure(e.toString()));
}
}
}
class UserState {
final UserProfile? profile;
final bool isLoggedIn;
final List<User> friends;
// Add other state properties as needed
}
class UserUseCaseGenerator extends UseCaseGenerator<UserState> {
UserUseCaseGenerator() {
final userRepository = UserRepository();
addUseCase(GetUserProfile(userRepository));
addUseCase(UpdateUserUseCase(userRepository));
addUseCaseWithMapFn(GetUserAges(userRepository), GetUserAges.mapToUser);
addStreamingUseCase(StreamUserAgeUseCase(userRepository));
addUseCaseWithMapFn(RetryableUseCase(userRepository), (u, _) => u);
}
}
class UserDataManager extends DataManager<UserState> {
UserDataManager() : super(UserUseCaseGenerator());
void getUserProfile(String userId) {
runUseCase<GetUserProfile, String>(userId);
}
void updateUser(UserProfile profile) {
runUseCase<UpdateUserUseCase, UserProfile>(profile);
}
void getUsersAges() {
runUseCase<GetUserAges, void>(null);
}
void registerAgeStream(TestingStreamUserAgeUseCaseParams params) {
registerStreamingUseCase<StreamUserAgeUseCase, TestingStreamUserAgeUseCaseParams>(params);
}
void retryableUseCase() {
runUseCase<RetryableUseCase, void>(null);
}
}
class UserBloc extends BaseBloc {
final UserDataManager _dataManager;
UserBloc(this._dataManager);
void loadUserProfile(String userId) {
_dataManager.getUserProfile(userId);
}
void updateUser(UserProfile profile) {
_dataManager.updateUser(profile);
}
Stream<bool> get isLoading => _dataManager.isLoading;
Stream<Failure> get onFailure => _dataManager.onFailure;
Stream<UserState> get state => _dataManager.rx;
}
class UserProfileScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final bloc = BlocProvider.of<UserBloc>(context);
return ValueStreamBuilder<UserState>(
stream: bloc.state,
builder: (context, snapshot) {
final state = snapshot.data;
return // Your UI implementation
},
);
}
}
Monitor loading states across your application:
dataManager.isLoading.listen((isLoading) {
// Handle loading state
});
Handle failures and errors gracefully:
dataManager.onFailure.listen((failure) {
// Handle failure
});
The DataManager automatically handles retryable failures:
class NetworkFailure extends RetryableFailure {
dynamic params;
NetworkFailure(this.params, Type runtimeType) : super(
params: params,
useCase: runtimeType,
delay: Duration(milliseconds: 500),
);
}
class GetUserProfile extends UseCase<String, UserProfile> {
final UserRepository userRepository;
GetUserProfile(this.userRepository);
@override
Future<Either<Failure, UserProfile>> execute(String userId) async {
try {
final profile = await userRepository.getProfile(userId);
return right(profile);
} catch (e) {
// if the error code is 500, we want to retry the use case
if (e.code == 500) {
return left(NetworkFailure(userId, this.runtimeType));
}
return left(UnexpectedFailure(e.toString()));
}
}
}
- Keep use cases focused on single responsibilities
- Implement proper error handling in use cases
- Use appropriate failure types
- Dispose of resources properly
- Utilize the built-in logging system for debugging
Contributions are welcome! Please feel free to submit a Pull Request.