for many of you this is an obvious / stupid question, but I've come to a point where I don't have a clue anymore. I have real difficulties understanding State Management with Bloc / Cubit.
Expectation: I have a page with a ListView (recipe_list) of all recipes and an 'add' button. Whenever I click on a ListItem or the 'add' button I go to the next page (recipe_detail). On this page I can create a new recipe (if clicked the 'add' button before), update or delete the existing recipe (if clicked on ListItem before). When I click the 'save' or 'delete' button the Navigator pops and I go back to the previous page (recipe_list). I used Cubit to manage the state of the recipe list. My expectation is that the ListView updates automatically after I clicked 'save' or 'delete'. But I have to refresh the App to display the changes.
main.dart
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Recipe Demo',
home: BlocProvider<RecipeCubit>(
create: (context) => RecipeCubit(RecipeRepository())..getAllRecipes(),
child: const RecipeList(),
)
);
}
}
recipe_list.dart
class RecipeList extends StatefulWidget {
const RecipeList({Key? key}) : super(key: key);
@override
_RecipeListState createState() => _RecipeListState();
}
class _RecipeListState extends State<RecipeList> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 24.0
),
color: const Color(0xFFF6F6F6),
child: Stack(
children: [
Column(
children: [
Container(
margin: const EdgeInsets.only(
top: 32.0,
bottom: 32.0
),
child: const Center(
child: Text('Recipes'),
),
),
Expanded(
child: BlocBuilder<RecipeCubit, RecipeState>(
builder: (context, state) {
if (state is RecipeLoading) {
return const Center(
child: CircularProgressIndicator(),
);
} else if (state is RecipeError) {
return const Center(
child: Icon(Icons.close),
);
} else if (state is RecipeLoaded) {
final recipes = state.recipes;
return ListView.builder(
itemCount: recipes.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return BlocProvider<RecipeCubit>(
create: (context) => RecipeCubit(RecipeRepository()),
child: RecipeDetail(recipe: recipes[index]),
);
}
));
},
child: RecipeCardWidget(
title: recipes[index].title,
description: recipes[index].description,
),
);
},
);
} else {
return const Text('Loading recipes error');
}
}
),
),
],
),
Positioned(
bottom: 24.0,
right: 0.0,
child: FloatingActionButton(
heroTag: 'addBtn',
onPressed: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return BlocProvider<RecipeCubit>(
create: (context) => RecipeCubit(RecipeRepository()),
child: const RecipeDetail(recipe: null),
);
}
));
},
child: const Icon(Icons.add_rounded),
backgroundColor: Colors.teal,
),
)
],
),
),
),
);
}
}
recipe_detail.dart
class RecipeDetail extends StatefulWidget {
final Recipe? recipe;
const RecipeDetail({Key? key, required this.recipe}) : super(key: key);
@override
_RecipeDetailState createState() => _RecipeDetailState();
}
class _RecipeDetailState extends State<RecipeDetail> {
final RecipeRepository recipeRepository = RecipeRepository();
final int _recipeId = 0;
late String _recipeTitle = '';
late String _recipeDescription = '';
final recipeTitleController = TextEditingController();
final recipeDescriptionController = TextEditingController();
late FocusNode _titleFocus;
late FocusNode _descriptionFocus;
bool _buttonVisible = false;
@override
void initState() {
if (widget.recipe != null) {
_recipeTitle = widget.recipe!.title;
_recipeDescription = widget.recipe!.description;
_buttonVisible = true;
}
_titleFocus = FocusNode();
_descriptionFocus = FocusNode();
super.initState();
}
@override
void dispose() {
recipeTitleController.dispose();
recipeDescriptionController.dispose();
_titleFocus.dispose();
_descriptionFocus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 24.0
),
color: const Color(0xFFF6F6F6),
child: Stack(
children: [
Column(
children: [
Align(
alignment: Alignment.topLeft,
child: InkWell(
child: IconButton(
highlightColor: Colors.transparent,
color: Colors.black54,
onPressed: () {
Navigator.pop(context);
},
icon: const Icon(Icons.arrow_back_ios_new_rounded),
),
),
),
TextField(
focusNode: _titleFocus,
controller: recipeTitleController..text = _recipeTitle,
decoration: const InputDecoration(
hintText: 'Enter recipe title',
border: InputBorder.none
),
style: const TextStyle(
fontSize: 26.0,
fontWeight: FontWeight.bold
),
onSubmitted: (value) => _descriptionFocus.requestFocus(),
),
TextField(
focusNode: _descriptionFocus,
controller: recipeDescriptionController..text = _recipeDescription,
decoration: const InputDecoration(
hintText: 'Enter recipe description',
border: InputBorder.none
),
),
],
),
Positioned(
bottom: 24.0,
left: 0.0,
child: FloatingActionButton(
heroTag: 'saveBtn',
onPressed: () {
if (widget.recipe == null) {
Recipe _newRecipe = Recipe(
_recipeId,
recipeTitleController.text,
recipeDescriptionController.text
);
context.read<RecipeCubit>().createRecipe(_newRecipe);
//recipeRepository.createRecipe(_newRecipe);
Navigator.pop(context);
} else {
Recipe _newRecipe = Recipe(
widget.recipe!.id,
recipeTitleController.text,
recipeDescriptionController.text
);
context.read<RecipeCubit>().updateRecipe(_newRecipe);
//recipeRepository.updateRecipe(_newRecipe);
Navigator.pop(context);
}
},
child: const Icon(Icons.save_outlined),
backgroundColor: Colors.amberAccent,
),
),
Positioned(
bottom: 24.0,
right: 0.0,
child: Visibility(
visible: _buttonVisible,
child: FloatingActionButton(
heroTag: 'deleteBtn',
onPressed: () {
context.read<RecipeCubit>().deleteRecipe(widget.recipe!.id!);
//recipeRepository.deleteRecipe(widget.recipe!.id!);
Navigator.pop(context);
},
child: const Icon(Icons.delete_outline_rounded),
backgroundColor: Colors.redAccent,
),
),
),
],
),
),
),
);
}
}
recipe_state.dart
part of 'recipe_cubit.dart';
abstract class RecipeState extends Equatable {
const RecipeState();
}
class RecipeInitial extends RecipeState {
@override
List<Object> get props => [];
}
class RecipeLoading extends RecipeState {
@override
List<Object> get props => [];
}
class RecipeLoaded extends RecipeState {
final List<Recipe> recipes;
const RecipeLoaded(this.recipes);
@override
List<Object> get props => [recipes];
}
class RecipeError extends RecipeState {
final String message;
const RecipeError(this.message);
@override
List<Object> get props => [message];
}
recipe_cubit.dart
part 'recipe_state.dart';
class RecipeCubit extends Cubit<RecipeState> {
final RecipeRepository recipeRepository;
RecipeCubit(this.recipeRepository) : super(RecipeInitial()) {
getAllRecipes();
}
void getAllRecipes() async {
try {
emit(RecipeLoading());
final recipes = await recipeRepository.getAllRecipes();
emit(RecipeLoaded(recipes));
} catch (e) {
emit(const RecipeError('Error'));
}
}
void createRecipe(Recipe recipe) async {
await recipeRepository.createRecipe(recipe);
final newRecipes = await recipeRepository.getAllRecipes();
emit(RecipeLoaded(newRecipes));
}
void updateRecipe(Recipe recipe) async {
await recipeRepository.updateRecipe(recipe);
final newRecipes = await recipeRepository.getAllRecipes();
emit(RecipeLoaded(newRecipes));
}
void deleteRecipe(int id) async {
await recipeRepository.deleteRecipe(id);
final newRecipes = await recipeRepository.getAllRecipes();
emit(RecipeLoaded(newRecipes));
}
}