Skip to main content

Search - Clean Architecture

How to write a Search Flutter application with UtopiaHooks and Clean Architecture principles.

To visit Search - Clean Architecture repository press HERE.

Project Structure

|- model
| |- user.dart
|- repository
| |- user_repository.dart
|- search
| |- 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

For more info about our recommended directory structure visit our Guides.

Setup

User model

User is a primitive (not serializable) object created for demonstration purposes.

class User {
final String name;

const User({required this.name});
}

User Repository

User Repository serves as a simple mock service layer of the application.

import 'package:utopia_hooks_example/search/clean/model/user.dart';

abstract interface class UserRepository {
Future<List<User>> getUsers({String? name});
}

final class MockUserRepository implements UserRepository {
static const users = [User(name: "A"), User(name: "B"), User(name: "C")];

const MockUserRepository();


Future<List<User>> getUsers({String? name}) async {
await Future<void>.delayed(const Duration(seconds: 1));
if (name == null) {
return users;
} else {
return users.where((it) => it.name.contains(name)).toList();
}
}
}

SearchCleanApp 🔗

SearchCleanApp is an outer layer of the application. It's consisted of MaterialApp and ValueProvider.

ValueProvider usually provides global states of the application to individual local states of the application.

void main() async => runApp(const SearchCleanApp());

class SearchCleanApp extends StatelessWidget {
static const UserRepository _userRepository = MockUserRepository();

const SearchCleanApp();


Widget build(BuildContext context) {
return const ValueProvider(
_userRepository,
child: MaterialApp(home: SearchPage()),
);
}
}

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
useAutoComputedStateHook responsible for handling async queries
useProvidedHook responsible for retrieving Objects defined in the ValueProvider
useMemoizedHook responsible for caching values

import 'package:utopia_hooks/utopia_hooks.dart';
import 'package:utopia_hooks_example/search/clean/repository/user_repository.dart';

class SearchPageState {
final FieldState search;
final List<String>? results;

const SearchPageState({required this.search, required this.results});

bool get isLoading => results == null;

bool get isEmpty => !isLoading && results!.isEmpty;
}

SearchPageState useSearchPageState() {
// retrieve UserRepository from global state
final userRepository = useProvided<UserRepository>();

// FieldState declaration - it's responsible for state management of the search TextField
final search = useFieldState();

// state responsible for computing async queris from our mock database
final state = useAutoComputedState(
debounceDuration: const Duration(milliseconds: 500),
keys: [search.value],
() async => userRepository.getUsers(name: search.value.isEmpty ? null : search.value),
);

final results = useMemoized(() => state.valueOrNull?.map((it) => it.name).toList(), [state.valueOrNull]);

return SearchPageState(search: search, results: results);
}

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.

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() {
// TextEditingControllerWrapper is a necessary Widget
// for FieldState's TextEditingController to work properly
return TextEditingControllerWrapper(
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])),
);
}
}

See also



Crafted for you by UtopiaSoftware 👾