CH03 TODO App
TODO App ์ ์ ์ฌ๋ผ์ด๋์ ๋์จ ์ธ ๊ฐ์ง ๋ฐฉ์์ผ๋ก ๊ฐ๊ฐ ์ด ์ธ ๋ฒ ๊ตฌํํ๋ค.
ํ๋์ ํด๋ ๋ด์ ์ธ ์ฑ์ ๊ฐ๊ฐ ๋ง๋ค๊ณ , ์์ค๋ ํด๋ ์ต์๋จ์์ git init ํด์ ๊ด๋ฆฌํด์ผ๊ฒ ๋ค.
Independent State ์ ์์๋ก๋ TODO Item ์ โ์๋ฃ์ฌ๋ถโ ๋ฅผ ๋ค ์ ์๋ค.
๋ถ๋ณํ๋ ๊ฐ์ด ์๋๊ณ ์๋ฃ๊ฐ ๋ ๊ฒฝ์ฐ ๊ฐ์ด ๋ณํ๋ ์ฑ์ง์ด ์๊ณ , ์ด ๋ณ๊ฒฝ์ ์ฌ๋ถ์ ๋ฐ๋ผ Widget ์ rebuild ๊ฐ ํ์ํ๊ธฐ ๋๋ฌธ์ ChangeNotifierProvider ๋ฅผ ์ฌ์ฉํด์ผํ๋ค.
Computed State ๋ ๋ค๋ฅธ ๊ฒ(๊ฒ๋ค) ์ ์์กด๋ Computed ๋ ๊ฐ์ด๋ค. ์์๋ก๋ โ๋ฏธ์๋ฃ Item ์โ ๋ฅผ ๋ค ์ ์๋ค.


์ต์ข ์ ์ผ๋ก StateNotifierProvider ๋ก ์ฑ์ ๊ตฌํํ๋ค. ๊นํ๋ธ์ ํธ์ํด๋์๋ค. ๊ธฐ๋ก์ ์ํด Provider ์ฃผ์ ๋ถ๋ถ๊ณผ independent state, computed state ์ํ๋ง ์ฝ๋๋ฅผ ๋จ๊ธด๋ค.
import 'package:flutter/material.dart';
import 'package:flutter_state_notifier/flutter_state_notifier.dart';
import 'package:provider/provider.dart';
import 'providers/providers.dart';
import 'screens/screens.dart';
void main() {
runApp(const TodoApp());
}
class TodoApp extends StatelessWidget {
const TodoApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StateNotifierProvider<SearchTerm, SearchTermState>(
create: (_) => SearchTerm(),
),
StateNotifierProvider<Filter, FilterState>(
create: (_) => Filter(),
),
StateNotifierProvider<Todos, TodosState>(
create: (_) => Todos(),
),
StateNotifierProvider<ActiveTodoCount, ActiveTodoCountState>(
create: (_) => ActiveTodoCount(),
),
StateNotifierProvider<FilteredTodos, FilteredTodosState>(
create: (_) => FilteredTodos(),
),
// ChangeNotifierProvider(
// create: (_) => SearchTerm(),
// ),
// ChangeNotifierProvider(
// create: (_) => Filter(initialFilterType: FilterType.all),
// ),
// ChangeNotifierProvider(
// create: (_) => Todos(initialTodos: []),
// ),
// ProxyProvider<Todos, ActiveTodoCount>(
// update: (
// _,
// Todos todos,
// __,
// ) =>
// ActiveTodoCount(todos: todos),
// ),
// ProxyProvider3<Todos, Filter, SearchTerm, FilteredTodos>(
// update: (
// _,
// Todos todos,
// Filter filter,
// SearchTerm searchTerm,
// __,
// ) =>
// FilteredTodos(
// todos: todos, filter: filter, searchTerm: searchTerm),
// )
],
child: const MaterialApp(
debugShowCheckedModeBanner: false,
home: Home(),
),
);
}
}
import 'package:equatable/equatable.dart';
import 'package:flutter_state_notifier/flutter_state_notifier.dart';
class SearchTermState extends Equatable {
final String? searchTerm;
const SearchTermState({
required this.searchTerm,
});
@override
bool get stringify => true;
@override
List<Object> get props => [searchTerm ?? ''];
SearchTermState copyWith(String searchTerm) {
return SearchTermState(searchTerm: searchTerm);
}
}
class SearchTerm extends StateNotifier<SearchTermState> {
SearchTerm() : super(const SearchTermState(searchTerm: ''));
void searchTermChange(String searchTerm) {
state = SearchTermState(searchTerm: searchTerm);
}
}
// class SearchTerm with ChangeNotifier {
// late SearchTermState _state;
// final String? initialSearchTerm;
//
// SearchTermState get state => _state;
//
// SearchTerm({
// this.initialSearchTerm,
// }) {
// _state = SearchTermState(searchTerm: initialSearchTerm);
// }
//
// void update(String searchTerm) {
// _state = _state.copyWith(searchTerm);
// notifyListeners();
// }
// }
import 'package:equatable/equatable.dart';
import 'package:state_notifier/state_notifier.dart';
import '../models/model.dart';
import '../providers/providers.dart';
class ActiveTodoCountState extends Equatable {
final int activeTodoCount;
const ActiveTodoCountState({
required this.activeTodoCount,
});
@override
List<Object> get props {
return [activeTodoCount];
}
@override
bool get stringify => true;
ActiveTodoCountState copyWith(int activeTodoCount) {
return ActiveTodoCountState(activeTodoCount: activeTodoCount);
}
}
class ActiveTodoCount extends StateNotifier<ActiveTodoCountState>
with LocatorMixin {
ActiveTodoCount() : super(const ActiveTodoCountState(activeTodoCount: 0));
@override
void update(Locator watch) {
final List<Todo> todos = watch<TodosState>().todos;
final int newActiveTodoCount =
todos.where((todo) => !todo.isCompleted).toList().length;
state = ActiveTodoCountState(activeTodoCount: newActiveTodoCount);
super.update(watch);
}
}
// class ActiveTodoCount {
// final Todos todos;
//
// ActiveTodoCount({
// required this.todos,
// });
//
// ActiveTodoCountState get state {
// final int newActiveTodoCount =
// todos.state.todos.where((todo) => !todo.isCompleted).toList().length;
//
// return ActiveTodoCountState(activeTodoCount: newActiveTodoCount);
// }
// }
State ๋ฅผ ๋ค๋ฃฐ๋ immutable state ์ ์ฌ์ฉํด์ผ ํ๋ ์ด์ (+ Equatable ์๋ฆฌ)
Todo app ์ค์ต ์ค todo ๋ฅผ ์ญ์ ํ๋ ๊ณผ์ ์์ rebuild ๊ฐ ๋ฐ์ํ์ง ์๋ ํ์์ด ๋ฐ์ํ๋ค. ๊ทธ๋์ ๊ฐ์ฌ๋ ์ฝ๋๋ฅผ ๋ณด๊ณ ์์ ํ๋๋ rebuild ๊ฐ ์ ์๋ํ๋ค. ๋๋ถ์ ๊ธฐ๊ณ์ ์ผ๋ก ์ฌ์ฉํ๋ Equatable ์ ๋ค์ ์ดํด๋ดค๊ณ , Notifier ์ state ๋น๊ต ๋ก์ง๋ ๋ค์ ์ดํด๋ณด์๋ค.
Object.dart ์ == ๊ณผ hashCode
dart ์ ๋ชจ๋ ๊ฐ์ฒด๋ค์ Object ๋ฅผ ์์ํ๊ณ ์์ผ๋ฉฐ, Object ์ == ์ ์๋์ ๊ฐ๋ค.
/// The equality operator.
///
/// The default behavior for all [Object]s is to return true if and
/// only if this object and [other] are the same object.
///
/// Override this method to specify a different equality relation on
/// a class. The overriding method must still be an equivalence relation.
/// That is, it must be:
///
/// * Total: It must return a boolean for all arguments. It should never throw.
///
/// * Reflexive: For all objects `o`, `o == o` must be true.
///
/// * Symmetric: For all objects `o1` and `o2`, `o1 == o2` and `o2 == o1` must
/// either both be true, or both be false.
///
/// * Transitive: For all objects `o1`, `o2`, and `o3`, if `o1 == o2` and
/// `o2 == o3` are true, then `o1 == o3` must be true.
///
/// The method should also be consistent over time,
/// so whether two objects are equal should only change
/// if at least one of the objects was modified.
///
/// If a subclass overrides the equality operator, it should override
/// the [hashCode] method as well to maintain consistency.
external bool operator ==(Object other);
The default behavior for all [Object]s is to return true if and only if this object and [other] are the same object.
ํต์ฌ์ ์ด ๋ฌธ๊ตฌ๋ค. ์ฃผ์๊ฐ์ด ๊ฐ์์ผ Object ์ == ๋ true ๋ฅผ return ํ๋ค.
์ข ํฉํด์ ๋ณด๋ฉด dart ์์ ํน๋ณํ == ์ override ํ์ง ์๋ ์ด์, ์ฃผ์๊ฐ์ด ๊ฐ์์ผ๋ง == ์์ true ๋ฅผ ๋ฐ์ ์ ์๊ณ ๊ทธ ์ธ์๋ ๋ชจ๋ false ์ด๋ค.
๋ == ๊ณผ ๋ฐ์ ํ ์ฐ๊ด์ด ์๋(== ์ ์ํฅ์ ์ฃผ๋) hashCode ๋ฅผ ์ดํด๋ณด์.
/// The hash code for this object.
///
/// A hash code is a single integer which represents the state of the object
/// that affects [operator ==] comparisons.
///
/// All objects have hash codes.
/// The default hash code implemented by [Object]
/// represents only the identity of the object,
/// the same way as the default [operator ==] implementation only considers objects
/// equal if they are identical (see [identityHashCode]).
///
/// If [operator ==] is overridden to use the object state instead,
/// the hash code must also be changed to represent that state,
/// otherwise the object cannot be used in hash based data structures
/// like the default [Set] and [Map] implementations.
///
/// Hash codes must be the same for objects that are equal to each other
/// according to [operator ==].
/// The hash code of an object should only change if the object changes
/// in a way that affects equality.
/// There are no further requirements for the hash codes.
/// They need not be consistent between executions of the same program
/// and there are no distribution guarantees.
///
/// Objects that are not equal are allowed to have the same hash code.
/// It is even technically allowed that all instances have the same hash code,
/// but if clashes happen too often,
/// it may reduce the efficiency of hash-based data structures
/// like [HashSet] or [HashMap].
///
/// If a subclass overrides [hashCode], it should override the
/// [operator ==] operator as well to maintain consistency.
external int get hashCode;
If a subclass overrides [hashCode], it should override the [operator ==] operator as well to maintain consistency.
๋ผ๋ ๊ฒ์ ๋ณด๋ฉด == ๋ฅผ override ํ ๊ฒฝ์ฐ ๋ฐ๋์ hashCode ๋ override ํด์ค์ผ ํ๋ ๊ฒ์ ์ ์ ์๋ค.
Equatable ์์๋ == ๊ณผ hashCode ๋ฅผ override ํ๋ค
์๋๋ Equatable ๋ด์ ์๋ == ์ hashCode ์ด๋ค. ์ด๋ฅผ ๋ณด๋ฉด Equatable ๋ฅผ ์์ํ ๊ฒฝ์ฐ Object ์ ==, hashCode ๋ฅผ ์ฌ์ฉํ์ง ์๊ณ , ์๋์ Equatable ์ ==, hashCode ๋ฅผ ์ฌ์ฉํ๊ฒ ๋๋ค๋ ๊ฒ์ ์ ์ ์๋ค.
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Equatable &&
runtimeType == other.runtimeType &&
equals(props, other.props);
@override
int get hashCode => runtimeType.hashCode ^ mapPropsToHashCode(props);
์ด๋ฅผ ๋ฐํ์ผ๋ก Equatable ์ ์์ํ๊ณ ์๋ TodosState ์ ์ดํด๋ณด์.
Todo todo1 = Todo(id: '1', description: 'test');
Todo todo2 = Todo(id: '2', description: 'test');
List<Todo> todos1 = [todo1, todo2];
List<Todo> todos2 = [todo1, todo2];
print(todos1.hashCode); // 807548389
print(todos2.hashCode); // 639492342
TodosState todosState1 = TodosState(todos: todos1);
TodosState todosState2 = TodosState(todos: todos2);
print(todosState1.hashCode); // 556324449
print(todosState2.hashCode); // 556324449
print(todosState1 == todosState2); // true
์์ ๊ฐ์ด todos ์์ฒด๋ ๋ค๋ฅธ ์ฃผ์๊ฐ์ ๊ฐ์ง๊ณ ์์ด๋ ํด๋น todos ์ ๊ตฌ์ฑ์ด todo1, todo2๋ก ๊ฐ๊ธฐ์ todosState1, todosState2 ์ hashCode ๋ ๊ฐ์ ๊ฐ์ return ํ๊ณ ๋น๊ต ์ญ์ true ๊ฐ return ๋๋ค.
์ด๋ฒ์ ์์์ ๊ตฌ์ฑ์ ๋ณํ๋ฅผ ์ค๋ณด์.
Todo todo1 = Todo(id: '1', description: 'test');
Todo todo2 = Todo(id: '2', description: 'test');
List<Todo> todos = [todo1, todo2];
print(todos.length); // 2
print(todos.hashCode); // 960134229
TodosState todosState1 = TodosState(todos: todos);
print(todosState1.hashCode); // 592161286
todos.removeWhere((element) => element.id == '1');
print(todos.length); // 1
print(todos.hashCode); // 960134229
TodosState todosState2 = TodosState(todos: todos);
print(todosState1.hashCode); // 680645052
print(todosState2.hashCode); // 680645052
print(todosState1 == todosState2); // true
๊ฒฐ๊ตญ ์ต์ข ์ ์ผ๋ก todosState1, todosState2 ๊ฐ๊ฐ์ ์์๋ todos ๋ผ๋ ๋๊ฐ์ ์ธ์คํด์ค๋ค. ๊ทธ๋์ todos ๊ฐ ์ด๋ป๊ฒ ๋ณํ๋ ์ง๊ฐ์ ํด๋น ์ฃผ์๊ฐ์ ๋์ผํ๋ค. ๊ทธ๋์ todosState1, todosState2 ๋ ๋ชจ๋ ๋๊ฐ์ todos ๋ผ๋ ์ธ์คํด์ค๋ฅผ ์์๋ก ๊ฐ๊ณ ์์ผ๋ฏ๋ก ํญ์ ๊ฐ์ ์ ๋ฐ์ ์๋ค.
๋ด๊ฐ ๋ฒํ ์ค์๋ ์ด ํฌ์ธํธ์์ ๋์ค๋๋ฐ, old state ๊ณผ new state ์ ์ง๊ธ์ ์์์์ todosState1, todosState2 ๋ก ๋๋ค. ์ฆ, StateNotifier ์์ custom method ๋ฅผ ํตํด state ์ ๋ณํ๋ฅผ ์ค์ผ
ํ๋๋ฐ state ์์ฒด๋ ๊ทธ๋๋ก ๋๊ณ state ๋ด์ ์์๋ง ๋ฐ๊พผ ๊ฒ์ด๋ค.
๋ค์ ๋งํด, state ์ ๋ณํ
๋ฅผ ์ค๋ค๋ ๊ฒ์ identical ํ์ ์ ๋ํด ์์ ํ ๋ณํ๋ฅผ ์ฃผ๊ธฐ ์ํด์ ์ฃผ์๊ฐ ๋ณ๊ฒฝ๊น์ง ๊ณ ๋ คํ์ด์ผ ํ๋๋ฐ ๋์ผํ ๊ฐ์ฒด๋ฅผ ๋๊ณ ๋ด๋ถ ์์๋ง ๋ฐ๊ฟ๋ฒ๋ฆฌ๋ ์ ์์์ฒ๋ผ ๋ณํ๋ฅผ ์คฌ๋คํ๋ค ๊ฒฐ๊ตญ ๊ฐ์ object ๊ฐ ๋ ๊ฒ์ด๋ค. todosState1 ์์ todosState2 ์ ๊ฐ์ด ๋ฐ๊ฟจ์ง๋ง ๊ฒฐ๊ตญ ๊ฐ์ object ์ธ ๊ฒ์ด๋ค.
๊ทธ๋์ state ๋ฅผ ๋ค๋ฃฐ๋๋ ์์ ํ immutable object ๋ก ์๋ก ๋ง๋ค์ด๋ด์ผ ํ๋ค. ๊ทธ๊ฒ์ด ์๊ฐํ๊ธฐ๋ ํธํ๊ณ ๋ฒ๊ทธ๋ฅผ ์ค์ด๋ ๋ฐฉ๋ฒ์ด๋ค. ๊ทธ๋ผ ๋ด๊ฐ ์ค์ํ ์ฝ๋๋ฅผ ๋ณด์.
// state ๊ฐ ๋ณ๊ฒฝ๋์๋ค๊ณ ์ธ์๋์ง ์์
void removeTodo(String removeTargetTodoId) {
print('before : ${state.todos.length}');
state.todos.removeWhere((todo) => todo.id == removeTargetTodoId);
print('after : ${state.todos.length}');
final List<Todo> todos = [...state.todos];
state = state.copyWith(todos);
}
// ์ ์๋ํ๋ ์ฝ๋
void removeTodo(String removeTargetTodoId){
final List<Todo> todos = [...state.todos.where((todo) => todo.id != removeTargetTodoId).toList()];
state = state.copyWith(todos);
}
์๋ชป๋ ์ฝ๋๋ฅผ ๋ณด๋ฉด ๊ฒฐ๊ตญ removeTodo() ํจ์๊ฐ ๋์ํ๊ธฐ ์ ๊ณผ ํ๋ state ๊ฐ ๊ฐ์ง todos ์ ๋ด์ฉ์ ๋ฐ๋์์์ง์ธ์ , todos ์์ฒด๋ ๊ทธ๋๋ก
์ด๊ธฐ ๋๋ฌธ์ ๊ฒฐ๊ตญ state ์ ๋ณํ๋ ๋ฐ์ํ์ง ์์๋ค๊ณ ํ์ ๋๋ค. ๊ณ ์ณ์ง ์ฝ๋์์๋ removeWhere ์ด ์๋๋ผ where ์ ํตํด์ ํ์ํ ์์๋ค์ ์ฐพ์ ํ toList() ๋ก ์์ ํ ๋ค๋ฅธ todos ๋ฅผ ๋ง๋ค์ด์ ๊ฒฐ๊ตญ state ๋ฅผ ๋ฐ๊พธ๊ณ ์๋ค.
์ถ๊ฐ๋ก Equatable ๊ณผ ๊ด๋ จํ์ฌ ์ฐธ๊ณ ํ๊ธฐ ์ข์๋ ํฌ์คํ ๋งํฌ๋ฅผ ๋จ๊ธด๋ค.
Last updated