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 π½