TODO App μ μ μ¬λΌμ΄λμ λμ¨ μΈ κ°μ§ λ°©μμΌλ‘ κ°κ° μ΄ μΈ λ² κ΅¬ννλ€.
νλμ ν΄λ λ΄μ μΈ μ±μ κ°κ° λ§λ€κ³ , μμ€λ ν΄λ μ΅μλ¨μμ git init ν΄μ κ΄λ¦¬ν΄μΌκ² λ€.
Independent State μ μμλ‘λ TODO Item μ βμλ£μ¬λΆβ λ₯Ό λ€ μ μλ€.
λΆλ³νλ κ°μ΄ μλκ³ μλ£κ° λ κ²½μ° κ°μ΄ λ³νλ μ±μ§μ΄ μκ³ , μ΄ λ³κ²½μ μ¬λΆμ λ°λΌ Widget μ rebuild κ° νμνκΈ° λλ¬Έμ ChangeNotifierProvider λ₯Ό μ¬μ©ν΄μΌνλ€.
Computed State λ λ€λ₯Έ κ²(κ²λ€) μ μμ‘΄λ Computed λ κ°μ΄λ€. μμλ‘λ βλ―Έμλ£ Item μβ λ₯Ό λ€ μ μλ€.
Copy 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(),
),
);
}
}
Copy 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();
// }
// }
Copy 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 μ == μ μλμ κ°λ€.
Copy /// 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 λ₯Ό μ΄ν΄λ³΄μ.
Copy /// 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 λ₯Ό μ¬μ©νκ² λλ€λ κ²μ μ μ μλ€.
Copy @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 μ μ΄ν΄λ³΄μ.
Copy 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 λλ€.
μ΄λ²μ μμμ ꡬμ±μ λ³νλ₯Ό μ€λ³΄μ.
Copy 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 λ‘ μλ‘ λ§λ€μ΄λ΄μΌ νλ€. κ·Έκ²μ΄ μκ°νκΈ°λ νΈνκ³ λ²κ·Έλ₯Ό μ€μ΄λ λ°©λ²μ΄λ€. κ·ΈλΌ λ΄κ° μ€μν μ½λλ₯Ό 보μ.
Copy // 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 λ₯Ό λ°κΎΈκ³ μλ€.