Skip to main content

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:

HooksDescription
useFieldStateHook responsible for TextField state management
useDebouncedHook responsible for debouncing search queries
useStreamHook responsible for FireStore stream management
useMemoizedHook 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 πŸ‘½