본문 바로가기

Flutter/10 app Todo with provider

[Flutter] App Todo(with Provider) - 6단계 리스트 삭제, 편집

오늘은 todo 리스트 내용을 편집하거나 리스트 자체를 삭제하는 기능을 구현하겠습니다.

그리고 active 리스트 개수와 FilteredTodos 의 생성자도 약간 수정했습니다.

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

소스코드 위치 - 07_remove_edit_todoList · mike-bskim/todo_test (github.com)

 

Release 07_remove_edit_todoList · mike-bskim/todo_test

 

github.com

 

todo_list.dart 수정 - 편집, 삭제 기능 추가

 

// 수정, id 값이 같으면 desc 를 변경.
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();
}

// 삭제, id 값이 다른 리스트만 반환.
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();
}

 

todos_screen.dart 수정 - 

 

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

// 삭제시, 아이콘 및 붉은 배경색 표시
  Widget showBackground(int direction) {
    return Container(
      margin: const EdgeInsets.all(4.0),
      padding: const EdgeInsets.symmetric(horizontal: 10.0),
      color: Colors.red,
      alignment: direction == 0 ? Alignment.centerLeft : Alignment.centerRight,
      child: const Icon(
        Icons.delete,
        size: 30.0,
        color: Colors.white,
      ),
    );
  }

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

    return ListView.separated(
      primary: false,
      shrinkWrap: true,
      itemCount: todos.length,
      separatorBuilder: (BuildContext context, int index) {
        return const Divider(color: Colors.grey);
      },
      itemBuilder: (BuildContext context, int index) {
      // 삭제용 위젯
        return Dismissible(
          key: ValueKey(todos[index].id),
          background: showBackground(0),
          secondaryBackground: showBackground(1),
          onDismissed: (_) {
          // 해당 리스트 삭제, 리스트의 id 를 전달
            context.read<TodoList>().removeTodo(todos[index].id);
          },
          confirmDismiss: (_) {
          // 삭제시 확인용 팝업창 표시
            return showDialog(
              context: context,
              barrierDismissible: false, // 다이얼로그 외부 클릭시 없어지지 않음
              builder: (context) {
                return AlertDialog(
                  title: const Text('Are you sure?'),
                  content: const Text('Do you really want to delete?'),
                  actions: [
                    TextButton(
                      onPressed: () => Navigator.pop(context, false),
                      child: const Text('NO'),
                    ),
                    TextButton(
                    // if true, onDismissed 실행
                      onPressed: () => Navigator.pop(context, true),
                      child: const Text('YES'),
                    ),
                  ],
                );
              },
            );
          },
          child: TodoItem(todo: todos[index]),
        );
      },
    );
  }
}

class TodoItem extends StatefulWidget {
  final Todo todo;

  const TodoItem({Key? key, required this.todo}) : super(key: key);

  @override
  _TodoItemState createState() => _TodoItemState();
}

class _TodoItemState extends State<TodoItem> {
// 새로운 desc 를 저장할 controller
  late final TextEditingController textController;

  @override
  void initState() {
    super.initState();
    textController = TextEditingController();
  }

  @override
  void dispose() {
    textController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {

    return ListTile(
      onTap: () {
        showDialog(
          context: context,
          builder: (context) {
            bool _error = false;
            textController.text = widget.todo.desc;
            return StatefulBuilder(       // errorText 를 다시 그려야 해서 사용함.
              builder: (BuildContext context, StateSetter setState) {
                return AlertDialog(
                  title: const Text('Edit Todo'),
                  content: TextFormField(
                    controller: textController,
                    autofocus: true,
                    decoration: InputDecoration(
                      errorText: _error ? 'Value cannot be empty' : null,
                    ),
                  ),
                  actions: [
                    TextButton(
                      onPressed: () => Navigator.pop(context),
                      child: const Text('CANCEL'),
                    ),
                    TextButton(
                      onPressed: () {
                        setState(() {
                          _error = textController.text.isEmpty ? true : false;

                          if (!_error) { // if empty, skip if.
                            context.read<TodoList>().editTodo(
                              widget.todo.id,
                              textController.text,
                            );
                            Navigator.pop(context);
                          }
                        });
                      },
                      child: const Text('EDIT'),
                    ),
                  ],
                );
              },
            );
          },
        );
      },
      leading: Checkbox(
        value: widget.todo.completed,
        onChanged: (bool? checked) {
          context.read<TodoList>().toggleTodo(widget.todo.id);
          debugPrint(
              'value(${widget.todo.desc}): ${widget.todo.completed
                  .toString()}');
        },
      ),
      title: Text(widget.todo.desc),
    );
  }
}

 

main.dart 수정

 

providers: [
  ChangeNotifierProvider<TodoList>(create: (context) => TodoList()),
  ChangeNotifierProvider<TodoFilter>(create: (context) => TodoFilter()),
  ChangeNotifierProvider<TodoSearch>(create: (context) => TodoSearch()),
  ChangeNotifierProxyProvider3<TodoFilter, TodoSearch, TodoList,
      FilteredTodos>(
  // 생성시 TodoList 를 확인해서 초기화함.
    create: (context) => FilteredTodos(
      initFilteredTodos: context.read<TodoList>().state.todos,
    ),
  // 생성시 TodoList 를 확인해서 초기화하지 않아도 update 시 처리됨.
    update: (
      BuildContext context,
      TodoFilter todoFilter,
      TodoSearch todoSearch,
      TodoList todoList,
      FilteredTodos? filteredTodos,
    ) =>
        filteredTodos!..update(todoFilter, todoSearch, todoList),
  ),
  ChangeNotifierProxyProvider<TodoList, TodoActiveCount>(
  // 생성시 TodoList 를 확인해서 초기화함.
    create: (context) => TodoActiveCount(
      initTodoActiveCount: context.read<TodoList>().state.todos.length,
    ),
  // 생성시 TodoList 를 확인해서 초기화하지 않아도 update 시 처리됨.
    update: (
      BuildContext context,
      TodoList todoList,
      TodoActiveCount? todoActiveCount,
    ) =>
        todoActiveCount!..update(todoList),
  )
],

 

todo_active_count.dart 수정 - 도입부만 수정

 

class TodoActiveCount with ChangeNotifier {
  // TodoActiveCountState _state = TodoActiveCountState.init();
  late TodoActiveCountState _state;
  final int initTodoActiveCount;

  TodoActiveCount({
    required this.initTodoActiveCount,
  }) {
    debugPrint('initTodoActiveCount: '+ initTodoActiveCount.toString());
    _state = TodoActiveCountState(todoActiveCount: initTodoActiveCount);
  }

  TodoActiveCountState get state => _state;

 

filtered_todos.dart 수정 - 도입부만 수정

 

class FilteredTodos with ChangeNotifier {
  // FilteredTodosState _state = FilteredTodosState.initial();
  late FilteredTodosState _state;
  final List<Todo> initFilteredTodos;

  FilteredTodos({required this.initFilteredTodos}){
    debugPrint('initFilteredTodos: ' + initFilteredTodos.toString());
    _state = FilteredTodosState(filteredTodos: initFilteredTodos);
  }

  FilteredTodosState get state => _state;

 

그외 검색시 시간 딜레이를 추가하여 검색 부담을 줄임.

 

debounce.dart 생성

 

import 'dart:async';

import 'package:flutter/material.dart';

class Debounce {
  final int milliseconds;
  Debounce({
    this.milliseconds = 500,
  });

  Timer? _timer;

  void run(VoidCallback action) {
    if (_timer != null) {
      _timer!.cancel();
    }
    _timer = Timer(Duration(milliseconds: milliseconds), action);
  }
}

 

 

 

 

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