Firestore Search
How to write a Firestore Search Flutter application with UtopiaHooks and Clean Architecture principles.
To visit Firestore Search repository press HERE.
Simplified Project Structureβ
|- search_page.dart - Coordinator between state & view layers
|- state
| |- search_page_state.dart - Layer that definies State and Hook responsible for business-logic
|- view
|- search_page_view.dart - View layer
For more info about our recommended directory structure visit our Guide.
SearchPageState πβ
search_page_state.dart
consists of two segments: SearchPageState
object and useSearchPageState
Hook responsible
for state management.
SearchPageState
contains everything necessary for View
, including variables, functions and getters.
useSearchPageState
serves as a wrapper for all the necessary hooks for SearchPage
's business-logic. In this use-case
it's consisted of several hooks:
Hooks | Description |
---|---|
useFieldState | Hook responsible for TextField state management |
useDebounced | Hook responsible for debouncing search queries |
useStream | Hook responsible for FireStore stream management |
useMemoized | Hook responsible for caching and updating FireStore stream |
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:utopia_hooks/utopia_hooks_flutter.dart';
class SearchPageState {
final FieldState search;
final List<String>? results;
const SearchPageState({required this.search, required this.results});
// getter determining whether the state is loading
bool get isLoading => results == null;
// getter determining whether there is search value in the input
bool get isEmpty => !isLoading && results!.isEmpty;
}
SearchPageState useSearchPageState() {
// FieldState declaration - it's responsible for state management of the search TextField
final search = useFieldState();
// Hook responsible for debouncing search queries
final debouncedSearch = useDebounced(search.value, duration: const Duration(milliseconds: 500));
// Function creating Firestore value stream
Stream<List<String>> createStream() {
Query<Map<String, dynamic>> query = FirebaseFirestore.instance.collection('users');
if (search.value.isNotEmpty) query = query.where('name', isEqualTo: debouncedSearch);
return query.snapshots().map((it) => it.docs.map((it) => it['name'] as String).toList());
}
// Hook caching Firestore stream and refreshing it once debouncedSearch changes value
final stream = useMemoized(createStream, [debouncedSearch]);
// Hook responsible for handling stream
final snapshot = useStream(stream, preserveState: false);
return SearchPageState(search: search, results: snapshot.data);
}
SearchPage - Coordinator πβ
We start with a simple wrapper that serves as a bridge between our state and view. In this simple case it's only
responsible
for initializing SearchPageState
.
In more complex structures, it also provides Flutter-related functions, such
as Navigator.of(context).push
to useSearchPageState
Hook.
class SearchPage extends StatelessWidget {
const SearchPage();
Widget build(BuildContext context) => const HookCoordinator(use: useSearchPageState, builder: SearchPageView.new);
}
SearchPageView πβ
SearchPageView is responsible for displaying search TextField and proper body based on search state. It handles 3
states of the page: loading, empty and not empty. It uses the SearchPageState
passed by SearchPage
.
import 'package:flutter/material.dart';
import 'package:utopia_hooks/utopia_hooks_flutter.dart';
import 'package:utopia_hooks_example/search/firebase/search/state/search_page_state.dart';
class SearchPageView extends StatelessWidget {
final SearchPageState state;
const SearchPageView(this.state);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: _buildSearchField()),
body: _buildBody(),
);
}
Widget _buildSearchField() {
// StatelessTextEditingControllerWrapper is a necessary Widget
// for FieldState's TextEditingController to work properly
return StatelessTextEditingControllerWrapper(
text: state.search,
builder: (controller) => TextField(
controller: controller,
decoration: const InputDecoration(hintText: "Search..."),
),
);
}
// Simple View management based on SearchPageState's values
Widget _buildBody() {
if (state.isLoading) return _buildLoading();
if (state.isEmpty) return _buildEmpty();
return _buildList();
}
Widget _buildLoading() => const Center(child: CircularProgressIndicator());
Widget _buildEmpty() => const Center(child: Text("No results"));
// Search-based content builder
Widget _buildList() {
return ListView.builder(
itemCount: state.results!.length,
itemBuilder: (context, index) => ListTile(title: Text(state.results![index])),
);
}
}
Crafted specially for you by UtopiaSoftware π½