Flutter lift state up challenge

JJ·2023년 3월 3일
0

Flutter

목록 보기
2/3
post-thumbnail

부트캠프를 들으며 Todo list 앱을 만들던 도중, lifting state up challenge를 하는데, 생각보다 원하는대로 해결이 잘 되지 않았다.


위 화면에서 오른쪽 아래에 있는 + 모양의 floating action버튼을 누르면 bottomsheet가 올라오고, bottomsheet에서 새로운 todo를 입력한 후 add를 누르면 추가되는 방식이다.

우선, 챌린지 전의 코드는 이런식의 구조였고, List<Task> tasks state는 TaskTile에 머물러있었다.

이러한 구조에서 AddTaskScreen 클래스의 TextField 에서 받아진 String을 다시 ListTile로 보내서 아래와 같은 체크박스 리스트를 띄워야했다.

구글에 flutter lift state up을 검색하여 아래와같이 좋은 블로그를 찾긴 했지만, 어떻게 todo list 앱에 적용시킬지 이리저리 고민하다가 결국 다른 방법을 찾아보기로 했다.

Medium flutter lift state up

일단 AddTaskScreenTaskList에서 모두 task에 접근이 가능하도록 TaskTile에 있던 List<Task> tasks;TaskScreen으로 옮겼다.
ListTile의 Text와 Checkbox는 Task 클래스를 통해 만들어지도록 되어있는 상태였다.

이 점을 이용해 TextField로부터 받아지는 input을 task에 저장하여 사용할 수 있게끔 코드를 짜봤다.

//tasks_screen.dart

import 'package:flutter/material.dart';
import 'package:todoey/widgets/tasks_list.dart';
import 'add_task_screen.dart';
import 'package:todoey/model/task.dart';

class TasksScreen extends StatefulWidget {
  
  State<TasksScreen> createState() => _TasksScreenState();
}

class _TasksScreenState extends State<TasksScreen> {
  List<String?> addedTasks = [];
  List<Task> tasks = [];

  void addTaskFunction() {
    setState(() {
      addedTasks = Task.tasks;
      tasks.add(Task(name: addedTasks[addedTasks.length - 1]));
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.blueGrey,
      floatingActionButton: FloatingActionButton(
        backgroundColor: Colors.blueGrey,
        child: Icon(Icons.add),
        onPressed: () {
          showModalBottomSheet(
            context: context,
            isScrollControlled: true,
            builder: (context) => SingleChildScrollView(
              child: Container(
                padding: EdgeInsets.only(
                    bottom: MediaQuery.of(context).viewInsets.bottom),
                child: AddTaskScreen(addTaskFunction: addTaskFunction),
              ),
            ),
          );
        },
      ),
      body: Column(
      //body 아래 길어서 생략
      ...
      
      child: TasksList(tasks),
      ...
      
// tasks_list.dart
import 'package:flutter/material.dart';
import 'task_tile.dart';
import 'package:todoey/model/task.dart';

class TasksList extends StatefulWidget {
  List<Task> tasks;
  TasksList(this.tasks);

  
  State<TasksList> createState() => _TasksListState();
}

class _TasksListState extends State<TasksList> {
  
  Widget build(BuildContext context) {
    return ListView.builder(
      itemBuilder: (context, index) {
        return TaskTile(
          taskTitle: widget.tasks[index].name!,
          isChecked: widget.tasks[index].isDone,
          checkboxCallback: (bool? checkboxState) {
            setState(() {
              widget.tasks[index].toggleDone();
            });
          },
        );
      },
      itemCount: widget.tasks.length,
    );
  }
}
// add_task_screen.dart
import 'package:flutter/material.dart';
import 'package:todoey/model/task.dart';

class AddTaskScreen extends StatelessWidget {
  String? addedTask;
  Function addTaskFunction;
  static TextEditingController controller = TextEditingController();

  AddTaskScreen({required this.addTaskFunction});

  
  Widget build(BuildContext context) {
    Color getColor(Set<MaterialState> states) {
      return Colors.blueGrey;
    }

    return Container(
    	// 스타일링 생략
    	...
        child: Padding(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Text(
                'Add Task',
                textAlign: TextAlign.center,
                style: TextStyle(color: Colors.blueGrey, fontSize: 25.0),
              ),
              TextField(
                controller: controller,
                onChanged: (value) {
                  addedTask = value;
                },
                textAlign: TextAlign.center,
                autofocus: true,
                decoration: InputDecoration(
                  enabledBorder: UnderlineInputBorder(
                    borderSide: BorderSide(width: 2, color: Colors.blueGrey),
                  ),
                ),
              ),
              SizedBox(
                height: 20.0,
              ),
              SizedBox(
                height: 50.0,
                child: TextButton(
                  onPressed: () {
                    Task.tasks.add(addedTask);
                    addTaskFunction();
                    controller.clear();
                    Navigator.pop(context);
                  },
                  style: ButtonStyle(
                      backgroundColor:
                          MaterialStateProperty.resolveWith(getColor)),
                  child: Text(
                    'Add',
                    
                    ...
// task.dart
class Task {
  static List<String?> tasks = [];
  late final String? name;
  late bool isDone;

  Task({required this.name, this.isDone = false});

  void toggleDone() {
    isDone = !isDone;
  }
}

결국 Task에 추가된 새로운 todo를 다시 TasksScreen의 새로운 List에 추가한 후, TaskTile로 보내는 복잡한 구조가 되어버렸지만, 내가 생각해낼 수 있는 방법은 이게 최선이었다.
이 모든게 AddTaskSreenTextField input을 TasksScreen으로 올리는 방법을 알아낼 수 없어서 일어난 일이다...

add버튼을 클릭했을 때 입력했던 글이 사라지도록 TextField에 controller를 추가해 add 버튼의 onPressed에 controller.clear();를 추가해줬다.
또한 add 버튼을 누른 후 bottomsheet가 자동으로 내려가도록 하는 법을 구글링해보니, Navigator.pop(context);를 추가하면 된다고 해서 추가해줬다.
emulator로 확인해보니 정말 bottomsheet가 잘 내려갔다!

challenge를 이렇게 얼추 끝내고 난 후 강의를 확인하니, 강사님은 간단하게 AddTaskScreenTextFieldTasksScreen으로 추가하셨다.

일단 TasksScreen으로 List<task> tasks = [];를 가져온 후 TasksList에 이 tasks를 다시 넘겨주기 위해 TasksList 생성자를 통해 tasks를 넘겨준다.

// tasks_list.dart
import 'package:flutter/material.dart';
import 'task_tile.dart';
import 'package:todoey/model/task.dart';

class TasksList extends StatefulWidget {
  final List<Task> tasks;
  TasksList(this.tasks);

  
  State<TasksList> createState() => _TasksListState();
}

class _TasksListState extends State<TasksList> {
  
  Widget build(BuildContext context) {
    return ListView.builder(
      itemBuilder: (context, index) {
        return TaskTile(
          taskTitle: widget.tasks[index].name!,
          isChecked: widget.tasks[index].isDone,
          checkboxCallback: (bool? checkboxState) {
            setState(() {
              widget.tasks[index].toggleDone();
            });
          },
        );
      },
      itemCount: widget.tasks.length,
    );
  }
}

그 후 TasksScreen의 콜백함수를 AddTaskScreen에 생성자를 통해 받아주고, add 버튼의 onPressed에 넣어준다.

// add_task_screen.dart
import 'package:flutter/material.dart';
import 'package:todoey/model/task.dart';

class AddTaskScreen extends StatelessWidget {
  final Function addTaskCallback;
  TextEditingController controller = TextEditingController();

  AddTaskScreen(this.addTaskCallback);

  
  Widget build(BuildContext context) {
    String? addedTask;
    Color getColor(Set<MaterialState> states) {
      return Colors.blueGrey;
    }

    return Container(
    	// 스타일링 생략
    	...
        child: Padding(
          padding: EdgeInsets.symmetric(horizontal: 40.0, vertical: 30.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Text(
                'Add Task',
                textAlign: TextAlign.center,
                style: TextStyle(color: Colors.blueGrey, fontSize: 25.0),
              ),
              TextField(
                controller: controller,
                onChanged: (value) {
                  addedTask = value;
                },
                textAlign: TextAlign.center,
                autofocus: true,
                decoration: InputDecoration(
                  enabledBorder: UnderlineInputBorder(
                    borderSide: BorderSide(width: 2, color: Colors.blueGrey),
                  ),
                ),
              ),
              SizedBox(
                height: 20.0,
              ),
              SizedBox(
                height: 50.0,
                child: TextButton(
                  onPressed: () {
                    addTaskCallback(addedTask); // 받아온 인풋을 콜백함수의 인자로 넣어 TasksScreen으로 넘겨준다.
                    controller.clear();
                    Navigator.pop(context);
                  },
                  style: ButtonStyle(
                      backgroundColor:
                          MaterialStateProperty.resolveWith(getColor)),
                  child: Text(
                    'Add',
                    
                    ...

이제 콜백함수의 인자로 AddTaskScreenTextField에서 받아진 text를 TasksSreen에넘겨줄 수 있다!

// tasks_screen.dart
import 'package:flutter/material.dart';
import 'package:todoey/widgets/tasks_list.dart';
import 'add_task_screen.dart';
import 'package:todoey/model/task.dart';

class TasksScreen extends StatefulWidget {
  
  State<TasksScreen> createState() => _TasksScreenState();
}

class _TasksScreenState extends State<TasksScreen> {
  List<Task> tasks = []; // 따로 새로운 List를 만들지 않아도 되어서 깔끔해졌다.

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.blueGrey,
      floatingActionButton: FloatingActionButton(
        backgroundColor: Colors.blueGrey,
        child: Icon(Icons.add),
        onPressed: () {
          showModalBottomSheet(
            context: context,
            isScrollControlled: true,
            builder: (context) => SingleChildScrollView(
              child: Container(
                padding: EdgeInsets.only(
                    bottom: MediaQuery.of(context).viewInsets.bottom),
                child: AddTaskScreen((addedTask) {
                  setState(() {
                    tasks.add(Task(name: addedTask)); // 받아온 addedTask를 이용해 task에 추가해준다.
                  }); 
                }),
              ),
            ),
          );
        },
      ),
      body: Column(
      //body 아래 길어서 생략
      ...
      
      child: TasksList(tasks),
      ...
      

새로운 코드로 바꾼 후에도 잘 작동하는 모습!

provider package를 사용하지 않은 state 관리는 여기서 마무리됐다.

끝~!

profile
신규...개발자가...되자...

0개의 댓글