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