Reputation: 752
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.
Upvotes: 2
Views: 6396
Reputation: 3917
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.
Upvotes: 1
Reputation: 9706
For somebody checking this thread, having states like TodosEmpty
and TodosLoaded
with the list of todos is very much OK. Although, I would go with freezed package and simplify it, like I did in with my app:
part of 'books_list_cubit.dart';
@freezed
class BooksListPageState with _$BooksListPageState {
const factory BooksListPageState.loading() = BooksListPageLoading;
const factory BooksListPageState.empty() = BooksListPageEmpty;
const factory BooksListPageState.success(List<Book> books) = BooksListPageSuccess;
}
More on this is in my Flutter cubits + hooks + Hive DB tutorial.
Upvotes: 0
Reputation: 3076
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 🤓
Upvotes: 3