Green Onyeji
3 min readFeb 20, 2024

Data Excellence Unleashed: Streamlining Flutter Apps with Unified Cubits Part 2

In the part 1, we looked at the traditional approach of creating separate cubits for each data entity. In this part, we are going to optimize our approach to a unified cubit for different data entities. One of the core requirement for this article is an understanding of Generics. You can find tons of tutorial around generics on the internet. If you don’t know generics, you can start from here

Proposed Approach: Using Generic Repository Pattern and Dependency Injection

To address the limitations of the traditional approach, we propose leveraging the generic repository pattern combined with dependency injection. This approach offers a more flexible and scalable solution for managing data operations in Flutter applications.

Introducing the Generic Repository Pattern

The generic repository pattern abstracts data access logic into reusable components, promoting code reuse and maintainability. By encapsulating common data operations such as fetching from remote servers and local databases within a generic repository, we can eliminate code duplication and simplify the management of data entities.

Leveraging Dependency Injection with GetIt

Dependency injection is a software design pattern that facilitates the management of object dependencies and promotes loose coupling between components. In our solution, we’ll utilize the GetIt package to implement dependency injection, allowing us to register and retrieve instances of our generic repositories throughout the application.

Lets code, first creating the generic repository pattern

abstract class DataRepository<T> {
Future<T> getDataFromServer(String nextPageUrl);
}

class DataRepositoryImpl<T> implements DataRepository<T> {
final Future<T> Function(String) _serverFetchFunction;

DataRepositoryImpl({
required Future<T> Function(String) serverFetchFunction,
}) : _serverFetchFunction = serverFetchFunction;

@override
Future<T> getDataFromServer(String nextPageUrl) async {
return _serverFetchFunction(nextPageUrl);
}
}

Now, lets create a generic cubit which would act as our unified one cubit for all our data entities

abstract class DataStateBase<T> {
T getData();
DataStateBase<T> copyWith(T newData);
}

class DataLoading<T> extends DataStateBase<T> {
@override
T getData() => null;

@override
DataStateBase<T> copyWith(T newData) => DataLoading<T>();
}

class DataLoaded<T> extends DataStateBase<T> {
final T data;

DataLoaded(this.data);

@override
T getData() => data;

@override
DataStateBase<T> copyWith(T newData) => DataLoaded<T>(newData);
}




abstract class DataStateCubit<T> {
Future<void> pullDataFromServer(String nextPageUrl);
}

class DataCubit<T> extends Cubit<DataStateBase<T>> implements DataStateCubit<T>{
final DataRepository<T> dataRepository;

DataCubit(this.dataRepository) : super(DataLoading<T>());

@override
Future<void> pullDataFromServer(String nextPageUrl) async{
if(nextPageUrl.isNotEmpty()){
try {
final response = await dataRepository.getDataFromServer(nextPageUrl ?? "");
response.fold(
(l) => emit(DataLoaded<T>([] as T)),
(r) {
emit(DataLoaded<T>(r.data as T));
if (r.next != null && r.next!.isNotEmpty) {
pullDataFromServer(r.next ?? "");
}
}
);
} on Error {

}
}
}

}

Now we have our generic repository and cubit. Next, is how do we use these for pulling multiple data entities from the server. I am using a clean code architecture, so I have a dependency injection file where I basically plug all my implemented classes to the abstract ones used to create the logic.



class AppInitializer {
static late GetIt instanceLocator;

AppInitializer._();

void close() {
instanceLocator.reset();
}

Future<void> create() async {
instanceLocator = GetIt.instance;
initialize();
}

void initialize() {
initializeRemoteDataSources();
initRepos();
initBlocs();
}

void initBlocs() {
instanceLocator.registerLazySingleton<DataCubit<List<Product>>>(() => DataCubit(dataRepository: instanceLocator()));
instanceLocator.registerLazySingleton<DataCubit<List<Orders>>>(() => DataCubit(dataRepository: instanceLocator()));
}

void initRepos() {
instanceLocator.registerLazySingleton<DataRepository<List<Product>>>(() => DataRepositoryImpl<List<Product>>(
serverFetchFunction: () => GetIt.I.get<ApiServices>().getProducts(""),
));

instanceLocator.registerLazySingleton<DataRepository<List<Orders>>>(() => DataRepositoryImpl<List<Product>>(
serverFetchFunction: () => GetIt.I.get<ApiServices>().getOrders(""),
));
}

void initializeRemoteDataSources() {
instanceLocator.registerLazySingleton<NetworkInfo>(
() => NetworkInfoImplementation(),
);

instanceLocator.registerLazySingleton<IApiClient>(
() => DioClient(
instanceLocator(),
),
);

instanceLocator.registerLazySingleton<ApiServices>(
() => ApiServicesImpl(
apiClient: instanceLocator(),
),
);
}

}

To create bloc providers for your app access, you can do this

BlocProvider(
create: (_) => GetIt.I.get<DataCubit<List<Product>>>(),
),

With this approach, we have just one cubit file and different instances of the cubit based on data entities. Thus, saving us from creating different folders and files (state and cubit).

Advantages of Unified Cubits Approach

  • Code Reusability: With the generic repository pattern, we can reuse data access logic across multiple data entities, reducing code duplication and promoting consistency.
  • Flexibility: The use of generics allows for a high degree of flexibility in managing different types of data entities within the same code structure.
  • Scalability: Dependency injection with GetIt enables easy management and retrieval of repository instances, facilitating scalability as the application grows.

By adopting this approach, software engineers can build applications that are more maintainable, extensible, and easier to manage in the long run.

I hope this helps you next time you are creating your cubits or BLoC, feel free to ask any questions.

Green Onyeji

Software Engineer | Technical Architect | Design Manager