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:
| Hooks | Description |
|---|---|
| useFieldState | Hook responsible for TextField state management |
| useAutoComputedState | Hook responsible for handling async queries |
| useProvided | Hook responsible for retrieving Objects defined in the ValueProvider |
| useMemoized | Hook 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 π½