본문 바로가기

Flutter/10 app Todo with provider

[Flutter] App Todo(with Provider) - 3단계 필터 및 리스트, cascade notation

오늘은 전체/할일/완료일을 구분할수 있는 필터 기능과 해당 필터에 맞는 리스트를 출력하는 기능을 구현해보자.

이전에는 필터 기능이 없어서 어떤 버튼(all/active/completed)을 선택해도 동일하게 Todo 리스트를 보여주었는데, 이번에는 필터에 따른 리스트 보여주기 기능을 추가할 예정이다.

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

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

 

Release 04_filter_Filtered_Todos · mike-bskim/todo_test

 

github.com

 

필터 추가 및 해당 필터별 리스트는 아래 동영상처럼 동작한다.

 

 

todo_filter.dart - 필터 상태를 저장하는 provider

 

import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';

import '../models/todo_model.dart';

class TodoFilterState extends Equatable {
  final Filter filter;
  const TodoFilterState({
    required this.filter,
  });

  factory TodoFilterState.init() {
    return const TodoFilterState(filter: Filter.all);
  }

  @override
  List<Object> get props => [filter];

  @override
  bool get stringify => true;

  TodoFilterState copyWith({
    Filter? filter,
  }) {
    return TodoFilterState(
      filter: filter ?? this.filter,
    );
  }
}

class TodoFilter with ChangeNotifier {
  TodoFilterState _state = TodoFilterState.init();
  TodoFilterState get state => _state;

  void changeFilter(Filter newFilter) {
    _state = _state.copyWith(filter: newFilter);
    notifyListeners();
  }
}

 

filtered_todos.dart

 

import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';

import '../models/todo_model.dart';
import 'providers.dart';


class FilteredTodosState extends Equatable {
  final List<Todo> filteredTodos;
  const FilteredTodosState({
    required this.filteredTodos,
  });

  factory FilteredTodosState.initial() {
    return const FilteredTodosState(filteredTodos: []);
  }

  @override
  List<Object> get props => [filteredTodos];

  @override
  bool get stringify => true;


  FilteredTodosState copyWith({
    List<Todo>? filteredTodos,
  }) {
    return FilteredTodosState(
      filteredTodos: filteredTodos ?? this.filteredTodos,
    );
  }
}

class FilteredTodos with ChangeNotifier {
  FilteredTodosState _state = FilteredTodosState.initial();
  FilteredTodosState get state => _state;

  void update(
      TodoFilter todoFilter,
      TodoList todoList,
      ) {
    List<Todo> _filteredTodos;

    // 핵심 부분. 필터의 조건에 맞는 리스트를 만드는 기능
    switch (todoFilter.state.filter) {
      case Filter.active:
        _filteredTodos =
            todoList.state.todos.where((Todo todo) => !todo.completed).toList();
        break;
      case Filter.completed:
        _filteredTodos =
            todoList.state.todos.where((Todo todo) => todo.completed).toList();
        break;
      case Filter.all:
      default:
        _filteredTodos = todoList.state.todos;
        break;
    }

    _state = _state.copyWith(filteredTodos: _filteredTodos);
    notifyListeners();
  }
}

 

providers.dart - provider 를 한곳에서 import 하게 모음

 

export 'todo_list.dart';
export 'todo_filter.dart';
export 'filtered_todos.dart';

 

main.dart - provider 기능 추가

 

return MultiProvider(
  providers: [
    ChangeNotifierProvider<TodoList>(create: (context) => TodoList()),
    ChangeNotifierProvider<TodoFilter>(create: (context) => TodoFilter()),
    ChangeNotifierProxyProvider2<TodoFilter, TodoList, FilteredTodos>(
      create: (context) => FilteredTodos(),
      update: (
        BuildContext context,
        TodoFilter todoFilter,
        TodoList todoList,
        FilteredTodos? filteredTodos,
      ) =>
      // .. 은 cascade notation 입니다. 자세한건 구글링해보세요.
          filteredTodos!..update(todoFilter, todoList),
    ),
  ],
  child: MaterialApp(
    title: 'TODOS',
    debugShowCheckedModeBanner: false,
    theme: ThemeData(
      primarySwatch: Colors.blue,
    ),
    home: const TodosScreen(),
  ),
);

 

cascade notation에 대해서 - https://dart.dev/guides/language/language-tour#cascade-notation

 

A tour of the Dart language

A tour of all the major Dart language features.

dart.dev

 

todo_model.dart 에 필터 enum 추가

 

enum Filter {
  all,
  active,
  completed,
}

 

todos_screen.dart 수정 - 필터버튼 로직 수정, 선택된 필더 버튼 배경색 로직 수정, 

 

// StatefulWidget => StatelessWidget 변경.
class SearchAndFilterTodo extends StatelessWidget {
  const SearchAndFilterTodo({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextFormField(
          decoration: const InputDecoration(
            labelText: 'Search todos',
            border: InputBorder.none,
            filled: true,
            prefixIcon: Icon(Icons.search),
          ),
          onChanged: (String? newSearchTerm) {
            debugPrint('Search todos: $newSearchTerm');
          },
        ),
        const SizedBox(height: 10.0),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            filterButton(context, Filter.all),
            filterButton(context, Filter.active),
            filterButton(context, Filter.completed),
          ],
        ),
      ],
    );
  }

  Widget filterButton(BuildContext context, Filter filter) {
    return TextButton(
      onPressed: () {
        context.read<TodoFilter>().changeFilter(filter);
        debugPrint('Clicked button ${context.read<TodoFilter>().state.filter}');
      },
      child: Text(
        filter == Filter.all
            ? 'All'
            : filter == Filter.active
                ? 'Active'
                : 'Completed',
        style: TextStyle(
          fontSize: 18.0,
          color: textColor(context, filter),
        ),
      ),
    );
  }

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

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) {
    // TodoList => FilteredTodos 로 변경
    // final todos = context.watch<TodoList>().state.todos;
    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 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> {
  @override
  Widget build(BuildContext context) {
    debugPrint('todo list : ${widget.todo}');

    return ListTile(
      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()}');
          // provider 처리해서 필요없음
          // setState(() {});
        },
      ),
      title: Text(widget.todo.desc),
    );
  }
}

 

 

 

 

 

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