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 Guide.

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 GlobalStates of the application to individual LocalStates 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_flutter.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() {
// 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 πŸ‘½