簡體   English   中英

使用 Bloc/Cubit 進行 Flutter 狀態管理

[英]Flutter State Management with Bloc/Cubit

對於你們中的許多人來說,這是一個明顯/愚蠢的問題,但我已經到了我不再有任何線索的地步。 我真的很難理解 Bloc / Cubit 的狀態管理。

期望:我有一個包含所有食譜的 ListView (recipe_list) 和一個“添加”按鈕的頁面。 每當我單擊 ListItem 或“添加”按鈕時,我都會轉到下一頁 (recipe_detail)。 在此頁面上,我可以創建一個新配方(如果之前單擊了“添加”按鈕)、更新或刪除現有配方(如果之前單擊了 ListItem)。 當我單擊“保存”或“刪除”按鈕時,導航器會彈出,然后我返回上一頁(recipe_list)。 我使用 Cubit 來管理配方列表的狀態。 我的期望是在單擊“保存”或“刪除”后,ListView 會自動更新。 但我必須刷新應用程序才能顯示更改。

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));
  }
}

當您導航到RecipeDetail頁面時,您似乎正在創建另一個BlocProvider 當您推送新的MaterialPageRoute ,這個新頁面會額外包裝在新的RecipeCubit 然后,當您調用context.read<RecipeCubit>() ,您將引用該提供程序(因為這是小部件樹中最接近的BlocProvider )。 您的RecipeList無法對這些更改做出反應,因為它的BlocBuilder正在尋找在小部件樹( MyAppBlocProvider聲明在其上方的BlocProvider 除此之外,當您關閉RecipeDetail頁面時,新創建的提供程序無論如何都會從小部件樹中刪除,因為它是在剛剛被推離屏幕的MaterialPageRoute中聲明的。

盡量去除附加BlocProvider (一個在RecipeList ,在OnTap的功能RecipeCardWidget ):

onTap: () {
  Navigator.push(context, MaterialPageRoute(
      builder: (context) {
        return BlocProvider<RecipeCubit>(  // remove this BlocProvider
          create: (context) => RecipeCubit(RecipeRepository()),
          child: RecipeDetail(recipe: recipes[index]),
        );
      }
  ));
},

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM