Note Widgets
As a final step, we're going to create a couple of widgets for the note feature:
A list tile that displays a note entry and navigates to the NoteEntryFormPage
when tapped.
A widget for streaming the 10 most recent notes.
A widget for displaying a list of notes in a paginated manner.
A page that allows the user to create, update and delete a note entry.
The main page for the note feature showcasing the StreamNotesListView
, PaginatedNotesListView
and a floating action button for creating a new note entry.
1. Widgets
In the note_app/modules/presentation/lib/note/widgets/
directory, create the following files:
1.1. Create the NoteEntryListTile
Make a file named note_entry_list_tile.dart
and paste the code below:
import 'package:domain/note/entities/note_entry.dart';
import 'package:flutter/material.dart';
import 'package:presentation/note/pages/note_entry_form_page.dart';
/// {@template NoteEntryListTile}
/// A [ListTile] that displays a [NoteEntry] and navigates to the
/// [NoteEntryFormPage] when tapped.
/// {@endtemplate}
class NoteEntryListTile extends StatelessWidget {
/// {@macro NoteEntryListTile}
const NoteEntryListTile({super.key, required this.noteEntry});
/// The [NoteEntry] to display.
final NoteEntry noteEntry;
@override
Widget build(BuildContext context) {
return ListTile(
key: ValueKey(noteEntry.id),
title: Text(noteEntry.title.isEmpty ? 'No title' : noteEntry.title),
subtitle:
Text(noteEntry.content.isEmpty ? 'No content' : noteEntry.content),
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => NoteEntryFormPage(noteEntry: noteEntry),
),
),
);
}
}
1.2. Create the StreamNotesListView
Make a file named stream_notes_list_view.dart
and paste the code below:
import 'package:domain/note/entities/note_entry.dart';
import 'package:domain/note/use_cases/watch_note_entries.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:presentation/note/widgets/note_entry_list_tile.dart';
/// {@template StreamNotesListView}
/// A widget for displaying a list of [NoteEntry]s emitted by the
/// [WatchNoteEntries] use case.
/// {@endtemplate}
class StreamNotesListView extends StatefulWidget {
/// {@macro StreamNotesListView}
const StreamNotesListView({super.key});
@override
State<StreamNotesListView> createState() => _StreamNotesListViewState();
}
class _StreamNotesListViewState extends State<StreamNotesListView> {
@override
Widget build(BuildContext context) {
final noteEntries = context.select<WatchNoteEntries, List<NoteEntry>>(
(useCase) => useCase.rightEvent ?? [],
);
if (noteEntries.isEmpty) {
return Container();
}
return ListView.builder(
itemCount: noteEntries.length,
itemBuilder: (context, index) =>
NoteEntryListTile(noteEntry: noteEntries[index]),
);
}
}
1.3. Create the PaginatedNotesListView
Make a file named paginated_notes_list_view.dart
and paste the code below:
The infinite_scroll_pagination package is utilized to implement pagination.
import 'package:domain/domain.dart';
import 'package:domain/note/entities/note_entry.dart';
import 'package:domain/note/use_cases/fetch_note_entries.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:presentation/note/widgets/note_entry_list_tile.dart';
/// {@template PaginatedNotesListView}
/// A widget for displaying a list of [NoteEntry]s paginated by the
/// [FetchNoteEntries] use case.
/// {@endtemplate}
class PaginatedNotesListView extends StatefulWidget {
const PaginatedNotesListView({super.key});
@override
State<PaginatedNotesListView> createState() => _PaginatedNotesListViewState();
}
class _PaginatedNotesListViewState extends State<PaginatedNotesListView> {
/// A controller containing all the [NoteEntry]s displayed on the list.
final _pagingController =
PagingController<dynamic, NoteEntry>(firstPageKey: 0);
@override
void initState() {
super.initState();
// Fetch the first page of note entries
_fetchNoteEntries(0);
// Add a listener to the paging controller so that we can fetch the next
// page of note entries when the user scrolls to the end of the list
_pagingController
.addPageRequestListener((pageKey) => _fetchNoteEntries(pageKey));
}
@override
void dispose() {
_pagingController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocConsumer<FetchNoteEntries, RunnerState>(
listener: (context, fetchNoteEntriesState) {
if (fetchNoteEntriesState is RunSuccess<List<NoteEntry>>) {
// Update the note entries in the paging controller when the
// [FetchNoteEntries] use case succeeds
_updatePagingController();
}
},
builder: (context, fetchNoteEntriesState) {
return PagedListView<dynamic, NoteEntry>(
pagingController: _pagingController,
shrinkWrap: true,
builderDelegate: PagedChildBuilderDelegate<NoteEntry>(
itemBuilder: (context, noteEntry, index) =>
NoteEntryListTile(noteEntry: noteEntry),
),
);
},
);
}
/// Fetches a collection of note entries respective to the given [pageKey].
void _fetchNoteEntries(dynamic pageKey) {
context.read<FetchNoteEntries>().run(
params: FetchNoteEntriesParams(pageToken: pageKey),
);
}
/// Updates the note entries in the paging controller.
void _updatePagingController() {
final fetchNoteEntries = context.read<FetchNoteEntries>();
final params = fetchNoteEntries.rightParams;
final noteEntries = fetchNoteEntries.rightValue;
if (params == null || noteEntries == null) {
return;
}
if (params.pageToken == 0) {
// If the token used to fetch the note entries is `0`, then we are
// fetching the first page
_pagingController.refresh();
}
if (noteEntries.length < params.limit) {
// If the number of note entries is less than the limit, then we have
// reached the end of the list
_pagingController.appendLastPage(noteEntries);
} else {
_pagingController.appendPage(
noteEntries,
params.pageToken + noteEntries.length,
);
}
}
}
2. Pages
In the note_app/modules/presentation/lib/note/pages/
directory, create the following files:
2.1. Create the NoteEntryFormPage
Make a file named note_entry_form_page.dart
and paste the code below:
import 'package:domain/note/entities/note_entry.dart';
import 'package:domain/note/use_cases/create_note_entry.dart';
import 'package:domain/note/use_cases/delete_note_entry.dart';
import 'package:domain/note/use_cases/update_note_entry.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
/// {@template NoteEntryFormPage}
/// A page that allows the user to create, edit or delete a [NoteEntry].
/// {@endtemplate}
class NoteEntryFormPage extends StatefulWidget {
const NoteEntryFormPage({super.key, this.noteEntry});
/// The note entry to edit. If `null`, a new note entry will be created.
final NoteEntry? noteEntry;
@override
State<NoteEntryFormPage> createState() => _NoteEntryFormPageState();
}
class _NoteEntryFormPageState extends State<NoteEntryFormPage> {
/// The text controller for the note title.
late final _titleController = TextEditingController(
text: widget.noteEntry != null ? widget.noteEntry!.title : null);
/// The text controller for the note content.
late final _contentController = TextEditingController(
text: widget.noteEntry != null ? widget.noteEntry!.content : null);
@override
void dispose() {
_titleController.dispose();
_contentController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
_saveButton(),
if (widget.noteEntry != null) _deleteButton(),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 24),
child: Column(
children: [_titleField(), _contentField()],
),
),
),
);
}
/// Returns a text field widget for the note title.
Widget _titleField() => TextFormField(
controller: _titleController,
decoration: const InputDecoration(hintText: 'Title'),
);
/// Returns a text field widget for the note content.
Widget _contentField() => TextFormField(
controller: _contentController,
maxLines: null,
decoration: const InputDecoration(
border: InputBorder.none,
hintText: 'Write your note here...',
),
);
/// Creates the note entry if it is new. Otherwise, updates the existing note.
Widget _saveButton() {
return IconButton(
onPressed: () async {
final result = widget.noteEntry == null
? await context.read<CreateNoteEntry>().run(
params: CreateNoteEntryParams(
title: _titleController.text,
content: _contentController.text,
),
)
: await context.read<UpdateNoteEntry>().run(
params: UpdateNoteEntryParams(
id: widget.noteEntry!.id,
title: _titleController.text,
content: _contentController.text,
),
);
if (!mounted) {
return;
}
if (result?.isRight() ?? false) {
Navigator.of(context).pop();
}
},
icon: const Icon(Icons.save),
);
}
/// Deletes the note entry.
Widget _deleteButton() {
return IconButton(
onPressed: () async {
final result = await context.read<DeleteNoteEntry>().run(
params: DeleteNoteEntryParams(id: widget.noteEntry!.id),
);
if (!mounted) {
return;
}
if (result?.isRight() ?? false) {
Navigator.of(context).pop();
}
},
icon: const Icon(Icons.delete),
);
}
}
2.2. Create the HomePage
Make a file named home_page.dart
and paste the code below:
import 'package:flutter/material.dart';
import 'package:presentation/note/pages/note_entry_form_page.dart';
import 'package:presentation/note/widgets/paginated_notes_list_view.dart';
import 'package:presentation/note/widgets/stream_notes_list_view.dart';
/// {@template HomePage}
/// The home page for the note feature.
/// {@endtemplate}
class HomePage extends StatefulWidget {
/// {@macro HomePage}
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
/// The views that displayed on the home page.
final _homeViews = <Widget>[
const StreamNotesListView(),
const PaginatedNotesListView(),
];
/// An index for the currently displayed view from the [_homeViews].
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(child: _homeViews[_selectedIndex]),
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const NoteEntryFormPage(),
),
),
child: const Icon(Icons.add),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (value) {
setState(() => _selectedIndex = value);
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.short_text),
label: 'Recent (Stream)',
),
BottomNavigationBarItem(
icon: Icon(Icons.notes),
label: 'All (Paginated)',
),
],
),
);
}
}
Once all widgets are created, you're done! 🎉
Last updated