I'm new to using Bloc and Cubit so I'm trying to figure out some best practices specifically with the State component. I have a simple Todos app where the Todos can be in multiple different states:
part of 'todos_cubit.dart';
abstract class TodosState extends Equatable {
const TodosState();
@override
List<Object> get props => [];
}
class TodosLoading extends TodosState {}
class TodosLoaded extends TodosState {
final List<Todo> todos;
TodosLoaded(this.todos);
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is TodosLoaded && listEquals(other.todos, todos);
}
@override
int get hashCode => todos.hashCode;
}
class TodosEmpty extends TodosState {}
class TodosError extends TodosState {
final String error;
TodosError(this.error);
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is TodosError && other.error == error;
}
@override
int get hashCode => error.hashCode;
}
My question is, should I keep the List<Todo> todos in the TodosLoaded subclass or should it be moved to the base class? My thoughts are that by moving it to the base class, it would make my TodosEmpty state redundant because I could simple check to see if the list of todos is empty in the UI. If this is the case, should I also move the String error to the base class?
Im sure there are pros and cons to each approach, just hoping to bounce ideas off anyone with more experience with Bloc.
Try using Cubit, it simplifies the code a lot.
I would probably create a function addToDo(todoItem) and removeToDo(todoItem) in the cubit class that updates the list and emits the change. The list variable would be in the Cubit, and you will refer to this list from the Widget by using context.read<TodoCubit>().todoList.
And you will use the functions like so: context.read<TodoCubit>().addToDo(todoItem)
I've written a tutorial for best approach for Cubit implementation Flutter | Firebase Authentication with Cubit (Bloc) — Tutorial 1 of 2
I think this article will be a lot of help for you. Take a look 🤓
Prepare your project like:
lib/
├── cubit/
│ ├── api_cubit.dart
│ ├── api_state.dart
├── manager/
│ └── api_manager.dart
├── models/
│ └── user_model.dart
├── screens/
│ └── home_screen.dart
└── main.dart
user_model.dart
class User {
final int id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'],
name: json['name'],
email: json['email'],
);
}
}
api_manager.dart //create a class to handle API calls
import 'package:dio/dio.dart';
import '../models/user_model.dart';
class ApiManager {
final Dio _dio = Dio();
Future<List<User>> fetchUsers() async {
try {
final response = await _dio.get('https://jsonplaceholder.typicode.com/users');
return (response.data as List).map((user) => User.fromJson(user)).toList();
} catch (e) {
throw Exception('Failed to fetch users');
}
}
}
api_cubit.dart, define the Cubit to manage the state
import 'package:flutter_bloc/flutter_bloc.dart';
import '../manager/api_manager.dart';
import '../models/user_model.dart';
import 'api_state.dart';
class ApiCubit extends Cubit<ApiState> {
final ApiManager apiManager;
ApiCubit({required this.apiManager}) : super(ApiInitial());
Future<void> fetchUsers() async {
emit(ApiLoading());
try {
final users = await apiManager.fetchUsers();
emit(ApiLoaded(users: users));
} catch (e) {
emit(ApiError(message: e.toString()));
}
}
}
api_state.dart, define the states for the Cubit
import '../models/user_model.dart';
abstract class ApiState {}
class ApiInitial extends ApiState {}
class ApiLoading extends ApiState {}
class ApiLoaded extends ApiState {
final List<User> users;
ApiLoaded({required this.users});
}
class ApiError extends ApiState {
final String message;
ApiError({required this.message});
}
Build the UI to display the fetched data
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../cubit/api_cubit.dart';
import '../cubit/api_state.dart';
import '../manager/api_manager.dart';
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => ApiCubit(apiManager: ApiManager())..fetchUsers(),
child: Scaffold(
appBar: AppBar(title: Text('Users')),
body: BlocBuilder<ApiCubit, ApiState>(
builder: (context, state) {
if (state is ApiLoading) {
return Center(child: CircularProgressIndicator());
} else if (state is ApiLoaded) {
return ListView.builder(
itemCount: state.users.length,
itemBuilder: (context, index) {
final user = state.users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
);
},
);
} else if (state is ApiError) {
return Center(child: Text(state.message));
}
return Center(child: Text('Press the button to fetch users.'));
},
),
),
);
}
}
You can store the fetched data locally (e.g., using a package like shared_preferences) and load it from the cache before making a new API call.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With