乐于分享
好东西不私藏

Build a Todo App with Flutter(21)

Build a Todo App with Flutter(21)

The EditTodo feature allows users to edit an existing todo item and save the changes.

EditTodoState

EditTodoState keeps track of the information needed when editing a todo.

part of 'edit_todo_bloc.dart';enum EditTodoStatus { initial, loading, success, failure }extension EditTodoStatusX on EditTodoStatus { bool get isLoadingOrSuccess => [    EditTodoStatus.loading,    EditTodoStatus.success,  ].contains(this);}final classEditTodoStateextendsEquatable{  const EditTodoState({this.status = EditTodoStatus.initial,this.initialTodo,this.title = '',this.description = '',  });  final EditTodoStatus status;  final Todo? initialTodo;  final String title;  final String description;  bool get isNewTodo => initialTodo == null;  EditTodoState copyWith({    EditTodoStatus? status,    Todo? initialTodo,String? title,String? description,  }) {return EditTodoState(      status: status ?? this.status,      initialTodo: initialTodo ?? this.initialTodo,      title: title ?? this.title,      description: description ?? this.description,    );  }  @override  List<Object?get props => [status, initialTodo, title, description];}

EditTodoEvent

The different events the bloc will react to are:

  • • EditTodoTitleChanged
  • • EditTodoDescriptionChanged
  • • EditTodoSubmitted
part of 'edit_todo_bloc.dart';sealed classEditTodoEventextendsEquatable{  const EditTodoEvent();  @override  List<Objectget props => [];}final classEditTodoTitleChangedextendsEditTodoEvent{  const EditTodoTitleChanged(this.title);  final String title;  @override  List<Objectget props => [title];}final classEditTodoDescriptionChangedextendsEditTodoEvent{  const EditTodoDescriptionChanged(this.description);  final String description;  @override  List<Objectget props => [description];}final classEditTodoSubmittedextendsEditTodoEvent{  const EditTodoSubmitted();}

EditTodoBloc

EditTodoBloc depends on the TodosRepository, just like TodosOverviewBloc and StatsBloc.

EditTodoBloc does not subscribe to _todosRepository.getTodos. It is a “write-only” bloc meaning it doesn’t need to read any information from the repository.

part'edit_todo_event.dart';part 'edit_todo_state.dart';classEditTodoBlocextendsBloc<EditTodoEventEditTodoState{  EditTodoBloc({required TodosRepository todosRepository,required Todo? initialTodo,  }) : _todosRepository = todosRepository,super(         EditTodoState(           initialTodo: initialTodo,           title: initialTodo?.title ?? '',           description: initialTodo?.description ?? '',         ),       ) {on<EditTodoTitleChanged>(_onTitleChanged);on<EditTodoDescriptionChanged>(_onDescriptionChanged);on<EditTodoSubmitted>(_onSubmitted);  }  final TodosRepository _todosRepository;  void _onTitleChanged(    EditTodoTitleChanged event,    Emitter<EditTodoState> emit,  ) {    emit(state.copyWith(title: event.title));  }  void _onDescriptionChanged(    EditTodoDescriptionChanged event,    Emitter<EditTodoState> emit,  ) {    emit(state.copyWith(description: event.description));  }  Future<void> _onSubmitted(    EditTodoSubmitted event,    Emitter<EditTodoState> emit,  ) async {    emit(state.copyWith(status: EditTodoStatus.loading));final todo = (state.initialTodo ?? Todo(title: '')).copyWith(      title: state.title,      description: state.description,    );try {await _todosRepository.saveTodo(todo);      emit(state.copyWith(status: EditTodoStatus.success));    } catch (e) {      emit(state.copyWith(status: EditTodoStatus.failure));    }  }}

There are many features that depend on the same list of todos, but there is no bloc-to-bloc communication.

All features are independent of each other and rely on the TodosRepository to listen for changes in the list of todos, as well as perform updates to the list.

For example, the EditTodos doesn’t know anything about the TodosOverview or Stats features.

When the UI submits a EditTodoSubmitted event:

  • • EditTodoBloc handles the business logic to update the TodosRepository.
  • • TodosRepository notifies TodosOverviewBloc and StatsBloc.
  • • TodosOverviewBloc and StatsBloc notify the UI which update with the new state.

EditTodoPage

The EditTodosPage provides an instance of the EditTodosBloc via BlocProvider. Unlike the other features, the EditTodosPage is a separate route which is why it exposes a static route method. This makes it easy to push the EditTodosPage onto the navigation stack via Navigator.of(context).push(...).

A simplified representation of the widget tree for the EditTodosPage is:

├── BlocProvider<EditTodosBloc>│   └── EditTodosPage│       └── BlocListener<EditTodosBloc>│           └── EditTodosView│               ├── TitleField│               ├── DescriptionField│               └── Floating Action Button
classEditTodoPageextendsStatelessWidget{  const EditTodoPage({super.key});  static Route<void> route({Todo? initialTodo}) {return MaterialPageRoute(      fullscreenDialog: true,      builder: (context) => BlocProvider(        create: (context) => EditTodoBloc(          todosRepository: context.read<TodosRepository>(),          initialTodo: initialTodo,        ),        child: const EditTodoPage(),      ),    );  }  @override  Widget build(BuildContext context) {return BlocListener<EditTodoBloc, EditTodoState>(      listenWhen: (previous, current) =>          previous.status != current.status &&          current.status == EditTodoStatus.success,      listener: (context, state) => Navigator.of(context).pop(),      child: const EditTodoView(),    );  }}classEditTodoViewextendsStatelessWidget{  const EditTodoView({super.key});  @override  Widget build(BuildContext context) {final l10n = context.l10n;final status = context.select((EditTodoBloc bloc) => bloc.state.status);final isNewTodo = context.select(      (EditTodoBloc bloc) => bloc.state.isNewTodo,    );return Scaffold(      appBar: AppBar(        title: Text(          isNewTodo              ? l10n.editTodoAddAppBarTitle              : l10n.editTodoEditAppBarTitle,        ),      ),      floatingActionButton: FloatingActionButton(        tooltip: l10n.editTodoSaveButtonTooltip,        shape: const ContinuousRectangleBorder(          borderRadius: BorderRadius.all(Radius.circular(32)),        ),        onPressed: status.isLoadingOrSuccess            ? null            : () => context.read<EditTodoBloc>().add(const EditTodoSubmitted()),        child: status.isLoadingOrSuccess            ? const CupertinoActivityIndicator()            : const Icon(Icons.check_rounded),      ),      body: const CupertinoScrollbar(        child: SingleChildScrollView(          child: Padding(            padding: EdgeInsets.all(16),            child: Column(              children: [_TitleField(), _DescriptionField()],            ),          ),        ),      ),    );  }}class_TitleFieldextendsStatelessWidget{  const _TitleField();  @override  Widget build(BuildContext context) {final l10n = context.l10n;final state = context.watch<EditTodoBloc>().state;final hintText = state.initialTodo?.title ?? '';return TextFormField(      key: const Key('editTodoView_title_textFormField'),      initialValue: state.title,      decoration: InputDecoration(        enabled: !state.status.isLoadingOrSuccess,        labelText: l10n.editTodoTitleLabel,        hintText: hintText,      ),      maxLength: 50,      inputFormatters: [        LengthLimitingTextInputFormatter(50),        FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9\s]')),      ],      onChanged: (value) {        context.read<EditTodoBloc>().add(EditTodoTitleChanged(value));      },    );  }}class_DescriptionFieldextendsStatelessWidget{  const _DescriptionField();  @override  Widget build(BuildContext context) {final l10n = context.l10n;final state = context.watch<EditTodoBloc>().state;final hintText = state.initialTodo?.description ?? '';return TextFormField(      key: const Key('editTodoView_description_textFormField'),      initialValue: state.description,      decoration: InputDecoration(        enabled: !state.status.isLoadingOrSuccess,        labelText: l10n.editTodoDescriptionLabel,        hintText: hintText,      ),      maxLength: 300,      maxLines: 7,      inputFormatters: [        LengthLimitingTextInputFormatter(300),      ],      onChanged: (value) {        context.read<EditTodoBloc>().add(EditTodoDescriptionChanged(value));      },    );  }}