본문 바로가기

Flutter/10 app Todo with provider

[Flutter] App Todo(with Provider) - 1단계 화면구성

상태관리에서 배운 provider를 이용하여 todo 어플을 만들어 보려고 합니다.

오늘은 화면을 먼저만들어 보겠습니다.

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

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

 

Release 02_screen_layout · mike-bskim/todo_test

 

github.com

 

전체 진행순서는 아래와 같다.

1. 화면 구성

2. 모델링 및 todo 리스트 관리(생성 및 완료체크)

3. 필터별 리스트 관리

4. 검색 기능추가(필터&검색, 필터와 검색 기능을 and 조건으로 구현)

5. 할일 개수 표시

6. todo 리스트 삭제 및 편집 기능

 

 

기본 화면 구성은 아래와 같다.

 

 

화면은 위와 같은 구성이다. 세부 위젯구성은 아래와 같다.

1. 어플 제목

2. Todo 생성

3. 검색 및 필터(전체, 미완료 리스트, 완료 리스트)

4. Todo 리스트

 

main.dart

 

import 'package:flutter/material.dart';

import 'screens/todos_screen.dart';


void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'TODOS',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const TodosScreen(),
    );
  }
}

 

todos_screen.dart

 

import 'package:flutter/material.dart';

class TodosScreen extends StatefulWidget {
  const TodosScreen({Key? key}) : super(key: key);

  @override
  State<TodosScreen> createState() => _TodosScreenState();
}

class _TodosScreenState extends State<TodosScreen> {
  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        body: SingleChildScrollView(
          child: Padding(
            padding:
                const EdgeInsets.symmetric(horizontal: 20.0, vertical: 40.0),
            child: Column(
              children: const [
                TodoHeader(),
                CreateTodo(),
                SizedBox(height: 20.0),
                SearchAndFilterTodo(),
                ShowTodos(),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

// AppBar 사용하지 않고 제목 표시함.
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(
          '0 items left',
          style: TextStyle(
            fontSize: 20.0,
            color: Colors.redAccent,
          ),
        ),
      ],
    );
  }
}

// 새로운 todo 리스트 생성
class CreateTodo extends StatefulWidget {
  const CreateTodo({Key? key}) : super(key: key);

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

class _CreateTodoState extends State<CreateTodo> {
  final newTodoController = TextEditingController();

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

  @override
  Widget build(BuildContext context) {
    return TextFormField(
      controller: newTodoController,
      decoration: const InputDecoration(labelText: 'What to do?'),
      onFieldSubmitted: (String? todoDesc) {
        debugPrint('CreateTodo Clicked: ${newTodoController.text}');
      },
    );
  }
}

// 찾기 및 필터처리(전체/미완료/완료 리스트)
class SearchAndFilterTodo extends StatefulWidget {
  const SearchAndFilterTodo({Key? key}) : super(key: key);

  @override
  State<SearchAndFilterTodo> createState() => _SearchAndFilterTodoState();
}

class _SearchAndFilterTodoState extends State<SearchAndFilterTodo> {
  String clickedType = 'all';

  @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, 'all'),
            filterButton(context, 'active'),
            filterButton(context, 'completed'),
          ],
        ),
      ],
    );
  }

  Widget filterButton(BuildContext context, String filter) {
    return TextButton(
      onPressed: () {
        clickedType = filter;
        debugPrint('Clicked button $clickedType');
        setState(() {});
      },
      child: Text(
        filter == 'all'
            ? 'All'
            : filter == 'active'
                ? 'Active'
                : 'Completed',
        style: TextStyle(
          fontSize: 18.0,
          color: textColor(context, filter),
        ),
      ),
    );
  }

  Color textColor(BuildContext context, String filter) {
    var currentFilter = clickedType;
    return currentFilter == filter ? Colors.blue : Colors.grey;
  }
}

// todo 클래스, 모델을 만들어서 별도의 파일로 분리예정.
class Todo {
  String id;
  String desc;
  bool completed;

  Todo({
    required this.id,
    required this.desc,
    this.completed = false,
  });

}

// 임시 화면표시용 리스트
List<Todo> todos = [
  Todo(id: '1', desc: 'Clean the room'),
  Todo(id: '2', desc: 'Wash the dish'),
  Todo(id: '3', desc: 'Do homework'),
];

// todo 리스트 보여주는 부분.
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) {

    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) {
    return ListTile(
      leading: Checkbox(
        value: widget.todo.completed,
        onChanged: (bool? checked) {
          widget.todo.completed = !widget.todo.completed;
          debugPrint('value(${widget.todo.desc}): ${widget.todo.completed.toString()}');
          setState(() {

          });
        },
      ),
      title: Text(widget.todo.desc),
    );
  }
}

 

차후에 Provider 로 업데이트에 따라 각 위젯의 타입을 "StatelessWidget" 으로 변경예정임.

 

 

 

 

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