본문 바로가기

Flutter/10 app Todo with provider

[Flutter] App Todo(refactoring StateNotifierProvider)

오늘은 기존 코딩을 StateNotifierProvider 로 변경해보겠습니다.

개발환경 : 윈도우11, 안드로이드 스튜디오, flutter 2.10.3

소스코드 위치 - 09_StateNotifierProvider · mike-bskim/todo_test · GitHub

 

Release 09_StateNotifierProvider · mike-bskim/todo_test

 

github.com

 

수정 대상은

todo_filter.dart

todo_search.dart

todo_list.dart

todo_active_count.dart

filtered_todos.dart

main.dart

todos_screen.dart

 

 

todo_filter.dart - 주요 수정부분만 표시함

 

class TodoFilter extends StateNotifier<TodoFilterState> {
  // 상태를 가지고 있으므로 기존에 사용하던 state 변수는 모두 제거
  // TodoFilterState _state = ;
  // TodoFilterState get state => _state;
  TodoFilter() : super(TodoFilterState.init());

  void changeFilter(Filter newFilter) {
    state = state.copyWith(filter: newFilter);
    // notifyListeners(); // 이것도 이제는 필요없음
  }
}

 

todo_search.dart - 주요 수정부분만 표시함

 

class TodoSearch extends StateNotifier<TodoSearchState> {
  // TodoSearchState _state = TodoSearchState.init();
  // TodoSearchState get state => _state;
  TodoSearch() : super(TodoSearchState.init());

  void setSearchTerm(String newSearchTerm) {
    state = state.copyWith(searchTerm: newSearchTerm);
    // notifyListeners();
  }
}

 

todo_list.dart - 주요 수정부분만 표시함

 

class TodoList extends StateNotifier<TodoListState> {
  // TodoListState _state = TodoListState.init();
  // TodoListState get state => _state;
  TodoList() : super(TodoListState.init());


  // 리스트 추가
  void addTodo(String todoDesc) {
    int? newNum;
    if (state.todos.isEmpty) {
      newNum = 1;
    } else {
      // 마지막 id 에서 1 증가
      newNum = int.parse(state.todos.last.id) + 1;
    }
    final newTodo = Todo(id: newNum.toString(), desc: todoDesc);
    final newTodos = [...state.todos, newTodo];

    // 리스트 새로 복사&생성
    state = state.copyWith(todos: newTodos);
    debugPrint('addTodo: ' + state.toString());
    // notifyListeners();
  }

  void editTodo(String id, String desc) {
    final newTodos = state.todos.map((Todo todo) {
      if (todo.id == id) {
        return Todo(
          id: id,
          desc: desc,
          completed: todo.completed,
        );
      }
      return todo;
    }).toList();

    state = state.copyWith(todos: newTodos);
    // notifyListeners();
  }

  void removeTodo(String id) {
    final newTodos = state.todos.where((Todo todo) => todo.id != id).toList();
    state = state.copyWith(todos: newTodos);
    debugPrint('remove Todos: ' + state.toString());
    // notifyListeners();
  }

  void toggleTodo(String id) {
    final newTodos = state.todos.map((Todo todo) {
      // 해당 리스트만 토글
      if (todo.id == id) {
        return Todo(
          id: id,
          desc: todo.desc,
          completed: !todo.completed,
        );
      }
      // 다른 리스트는 그대로 리턴
      return todo;
    }).toList();

    // 토글된 리스트 새로 복사&생성
    state = state.copyWith(todos: newTodos);
    // notifyListeners();
  }
}

 

todo_active_count.dart - 주요 수정부분만 표시함

 

// ProxyProvider 구현 부분은 StateNotifier 로 변경, 제네릭지정 필요함
// 'with LocatorMixin' 구문으로 다른 Provider 의 상태를 직접 접근가능
class TodoActiveCount extends StateNotifier<TodoActiveCountState>
    with LocatorMixin {
  // final TodoList todoList;
  // TodoActiveCount({required this.todoList});
  TodoActiveCount() : super(TodoActiveCountState.init());

  @override
  void update(Locator watch) {
    // context 없이 직접 접근 가능함
    // 기존에는 watch<TodoList>().state.todos 로 접근가능했으나
    // 이제는 state 에 직접 접근 불가함
    // 그래서 watch<TodoListState>().todos 이렇게 변경해야 함.
    final List<Todo> todos = watch<TodoListState>().todos;
    state = state.copyWith(
        todoActiveCount:
            todos.where((Todo todo) => !todo.completed).toList().length);
    super.update(watch);
  }
}

 

filtered_todos.dart - 주요 수정부분만 표시함

 

class FilteredTodos extends StateNotifier<FilteredTodosState>
    with LocatorMixin {
  FilteredTodos() : super(FilteredTodosState.initial());

  @override
  void update(Locator watch) {
  // 다른 provider 의 상태값을 context 없이 직접 접근 가능함.
    final Filter filter = watch<TodoFilterState>().filter;
    final String searchTerm = watch<TodoSearchState>().searchTerm;
    final List<Todo> todos = watch<TodoListState>().todos;

    List<Todo> _filteredTodos;

    // 핵심 부분. 필터의 조건에 맞는 리스트를 만드는 기능
    switch (filter) {
      case Filter.active:
        _filteredTodos = todos.where((Todo todo) => !todo.completed).toList();
        break;
      case Filter.completed:
        _filteredTodos = todos.where((Todo todo) => todo.completed).toList();
        break;
      case Filter.all:
      default:
        _filteredTodos = todos;
        break;
    }
    // 로직 추가
    if (searchTerm.isNotEmpty) {
      _filteredTodos = _filteredTodos
          .where((Todo todo) => todo.desc.toLowerCase().contains(searchTerm))
          .toList();
    }
    state = state.copyWith(filteredTodos: _filteredTodos);
    super.update(watch);
  }

}

 

main.dart - 주요 수정부분만 표시함

 

return MultiProvider(
  providers: [
    StateNotifierProvider<TodoList, TodoListState>(
        create: (context) => TodoList()),
    StateNotifierProvider<TodoFilter, TodoFilterState>(
        create: (context) => TodoFilter()),
    StateNotifierProvider<TodoSearch, TodoSearchState>(
        create: (context) => TodoSearch()),
    StateNotifierProvider<TodoActiveCount, TodoActiveCountState>(
        create: (context) => TodoActiveCount()),
    StateNotifierProvider<FilteredTodos, FilteredTodosState>(
        create: (context) => FilteredTodos()),

  ],
  child: MaterialApp(
    title: 'TODOS',
    debugShowCheckedModeBanner: false,
    theme: ThemeData(
      primarySwatch: Colors.blue,
    ),
    home: const TodosScreen(),
  ),
);

 

todos_screen.dart - 주요 수정부분만 표시함(watch 부분만 수정)

 

class TodoHeader extends StatelessWidget {
  const TodoHeader({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        const Text(
          'TODO',
          style: TextStyle(fontSize: 40.0),
        ),
        Text(
          // '${context.watch<TodoActiveCount>().state.todoActiveCount} items left',
          // 제네릭은 TodoActiveCount --> TodoActiveCountState
          // state 제거, .state.todoActiveCount --> .todoActiveCount
          '${context.watch<TodoActiveCountState>().todoActiveCount} items left',
          style: const TextStyle(
            fontSize: 20.0,
            color: Colors.redAccent,
          ),
        ),
      ],
    );
  }
}

 

Color textColor(BuildContext context, Filter filter) {
  var currentFilter = context.watch<TodoFilterState>().filter;
  return currentFilter == filter ? Colors.blueAccent : Colors.grey;
}

FontWeight textFontWeight(BuildContext context, Filter filter) {
  var currentFilter = context.watch<TodoFilterState>().filter;
  return currentFilter == filter ? FontWeight.bold : FontWeight.normal;
}

 

class ShowTodos extends StatelessWidget {
.....

  @override
  Widget build(BuildContext context) {
// final todos = context.watch<FilteredTodos>().state.filteredTodos;
final todos = context.watch<FilteredTodosState>().filteredTodos;

 

 

 

 

[참고자료] udemy - Flutter Provider Essential 코스 (Korean)