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<Object> get props => [];}final classEditTodoTitleChangedextendsEditTodoEvent{ const EditTodoTitleChanged(this.title); final String title; @override List<Object> get props => [title];}final classEditTodoDescriptionChangedextendsEditTodoEvent{ const EditTodoDescriptionChanged(this.description); final String description; @override List<Object> get 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<EditTodoEvent, EditTodoState> { 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:
-
• EditTodoBlochandles the business logic to update theTodosRepository. -
• TodosRepositorynotifiesTodosOverviewBlocandStatsBloc. -
• TodosOverviewBlocandStatsBlocnotify 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)); }, ); }}
夜雨聆风