Cubit Base
cubit_base is a Flutter package designed to simplify state management for applications using the Cubit architecture (part of the BLoC pattern). It provides utilities to handle API data fetching and pagination with minimal boilerplate code, making it easier to manage asynchronous operations and state updates in your Flutter apps.
Features
- Simplified API Fetching: Fetch data from APIs with built-in state management for loading, success, and error states.
- Pagination Support: Handle paginated data fetching with automatic page tracking and state updates.
- Type-Safe State Management: Works with generic types to support various data models (e.g.,
UserModel,NotificationModel). - Status Change Callbacks: Optional callbacks to monitor state changes during fetching or pagination.
- Reusable and Extensible: Integrates seamlessly with Cubit and can be used with any use case or repository pattern.
Installation
Add cubit_base to your pubspec.yaml:
dependencies:
cubit_base: ^[latest_version]
Run the following command to install the package:
flutter pub get
Then, import the package in your Dart code:
import 'package:cubit_base/cubit_base.dart';
Usage
The cubit_base package provides two main methods in the Fetcher class:
fetchWithBase: For fetching single data items from an API.fetchWithPaginate: For fetching paginated lists of data with automatic page handling.
Key Components
- BaseState: A base state class for managing single data fetching with statuses (
initial,loading,success,error). - BasePaginationState: A state class for managing paginated data with additional pagination-specific statuses (
paging) and properties likereachedMax. - DataState: A sealed class representing the result of an API call (
DataSuccessorDataFailed). - Fetcher: A utility class with static methods to handle fetching and pagination logic.
Example
Below is an example demonstrating how to use cubit_base for both single data fetching and paginated data fetching.
1. Setting Up a Cubit
First, define your data model and Cubit. For this example, let's assume a UserCubit and a UserState.
UserCubit
// user_cubit.dart
import 'package:bloc/bloc.dart';
import 'package:cubit_base/cubit_base.dart';
class UserCubit extends Cubit<UserState> {
// or you can use with usecase
final UserRepository repository;
UserCubit(this.repository) : super(UserState.initial());
Future<void> fetchUser(num userId) async {
await Fetcher.fetchWithBase(
fetcher: repository.getUser(userId), // getUserUseCase.call(params:userId);
state: state.getUserState,
emitter: (newState)=> emit(state.copyWith(getUserState: newState.data)),
onStatusChange: (status) => print('Status: $status'),
);
}
Future<void> fetchUsersList() async {
await Fetcher.fetchWithBase(
fetcher: repository.getUsersList(), // getUsersListUseCase.call(params:null);
state: state.getUsersListState,
emitter: (newState)=> emit(state.copyWith(getUsersListState: newState.data)),
onStatusChange: (status) => print('Status: $status'),
);
}
Future<void> fetchUsersListWithPaginate() async {
await Fetcher.fetchWithPaginate(
fetcher: repository.getUsersListWithPaginate(params:state.usersListState.query), // getUsersListUseCase.call(params:null);
state: state.usersListState,
emitter: (newState)=> emit(state.copyWith(usersListState: newState.data)),
onStatusChange: (status) => print('Status: $status'),
);
}
}
UserState
// user_state.dart
class UserState{
final BaseState<UserModel> getUserState;
final BaseState<List<UserModel>> getUsersListState;
final BasePaginationState<UserModel> usersListState;
factory UserState.initial() => UserState(getUserState: BaseState.initial(), getUsersListState: BaseState.initial(), usersListState: BasePaginationState(query: BaseQuery(page: 1, size: 10)));
@override
UserState copyWith({
BaseState<UserModel>? getUserState,
BaseState<List<UserModel>>? getUsersListState,
BasePaginationState<UserModel>? usersListState,
}) {
return UserState(
getUserState: getUserState ?? this.getUserState,
getUsersListState: getUsersListState ?? this.getUsersListState,
usersListState: usersListState ?? this.usersListState,
);
}
}
2. Using fetchWithBase
Use fetchWithBase to fetch a single item, such as a user profile.
// Fetch a single user
userCubit.fetchUser(123);
// Fetch a list of users
userCubit.fetchUsersList();
This will:
- Set the state to
loading. - Call the repository's
getUsermethod. - Update the state to
successwith the fetched data orerrorwith an error message. - Reset the state to
initialafter completion. - Trigger
onStatusChangecallbacks for each state change.
3. Using fetchWithPaginate
Use fetchWithPaginate to fetch paginated data, such as a list of users.
// Fetch a list of users with pagination
userCubit.fetchUsersListWithPaginate();
This will:
- For the first page (
page == 1):- Set the state to
loading. - Fetch the data and reset the list.
- Update the state to
successwith the new list orerrorwith an error message. - Set
reachedMaxbased on the response size. - Increment the page number.
- Set the state to
- For subsequent pages:
- Check if pagination is allowed (
!reachedMaxand not alreadypaging). - Set the state to
paging. - Append new data to the existing list.
- Update
reachedMaxand increment the page number.
- Check if pagination is allowed (
- Reset the state to
initialafter completion. - Trigger
onStatusChangecallbacks for each state change.
4. UI Integration
Integrate with your UI using a BlocBuilder to react to state changes.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class UserScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => ...,
child: BlocBuilder<UserCubit, UserState>(
builder: (context, state) {
if (state.status.isLoading) {
return Center(child: CircularProgressIndicator());
} else if (state.status.isSuccess) {
return ListView.builder(
itemCount: state.list.length + (state.reachedMax ? 0 : 1),
itemBuilder: (context, index) {
if (index < state.list.length) {
return ListTile(title: Text(state.list[index].name));
} else {
context.read<UserCubit>().fetchUsers();
return Center(child: CircularProgressIndicator());
}
},
);
} else if (state.status.isError) {
return Center(child: Text('Error: ${state.errorMessage}'));
}
return Container();
},
),
);
}
}
API Reference
Fetcher.fetchWithBase<T>
Fetches a single item from an API and updates the state.
Parameters:
fetcher: AFuture<DataState<T?>>representing the API call.state: The currentBaseState<T>to update.emitter: A function to emit the new state.onStatusChange: An optional callback for status changes (loading,success,error,initial).
Example:
Fetcher.fetchWithBase<T>(
fetcher: useCases.call(...),
state: state.<targetState>,
emitter: (newData) => emit(state.copyWith(<targetState>: newData)),
onStatusChange: (status) => print(status),
);
Fetcher.fetchWithPaginate<T>
Fetches a paginated list from an API and updates the state.
Parameters:
fetcher: AFuture<DataState<List<T>?>>representing the API call.state: The currentBasePaginationState<T>to update.emitter: A function to emit the new state.onStatusChange: An optional callback for status changes (loading,paging,success,error,initial).
Example:
Fetcher.fetchWithBase<T>(
fetcher: useCases.call(...),
state: state.<targetState>,
emitter: (newData) => emit(state.copyWith(<targetState>: newData)),
onStatusChange: (status) => print(status),
);
Contributing
Contributions are welcome! Please follow these steps:
- Fork the repository.
- Create a feature branch (
git checkout -b feature/YourFeature). - Commit your changes (
git commit -m 'Add YourFeature'). - Push to the branch (
git push origin feature/YourFeature). - Open a Pull Request.
Please ensure your code follows the package's coding style and includes appropriate tests.
License
This package is licensed under the MIT License. See the LICENSE file for details.