오늘하루도 우힣ㅎ

Flutter) Bloc으로 Todo List 만들기 (1) 본문

flutter/BLoC

Flutter) Bloc으로 Todo List 만들기 (1)

우힣 2020. 2. 28. 22:35

 앱을 만들때도 웹을 만들때도 가장 흔한 예제중 하나는 Todo List이다. 이 Todo List를 Bloc을 통하여 만들어 볼려 한다. 이미 많은 예제 들과 많은 형식들이 존재 하지만 Bloc을 이해하기에 좋은 예라고 생각이 되어 적어 볼려한다. 

 

디렉토리 구조는 다음과 같다.
lib
|____blocs
|      |_____todoBloc
|             |____bloc.dart
|             |____todo_bloc.dart
|             |____todo_state.dart
|             |____todo_event.dart
|____models
|     |____todoModel.dart
|
|____main.dart
|____todo_add.dart
|____todo_list.dart

이번 포스팅에서는 기본적으로 List 들로 데이터를 띄우는 것에 대해 만들어 갈것이다.

 

1. pubspec_yaml.dart에 먼저 필요한 것들을 추가해주어야 한다.

 - flutter_bloc: ^2.0.0 (bloc을 위해 필요한 패키지)
 - equatable: ^0.2.0 (object의 내용이 같더라도 다른 object로 감지하는것을 방지하기 위한 것)

 

2. 여기서는 main페이지에 바로 코딩하지 않으려 한다. main 페이지에는 페이지 라우트 네임, 테마, 등을 정의할뿐 화면을 그리기 위한 작업은 각각의 페이지에 맞는 파일에 작성할것이다. 이렇게 하면 main의 페이지가 좀 더 깔끔해질 뿐만 아니라 페이지를 나누는데 더 효율적이기 때문이다.

import 'package:flutter/material.dart';
import 'package:flutter_app_todo/todo_add.dart';
import 'package:flutter_app_todo/todo_list.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import 'blocs/todoBloc/bloc.dart';

void main() => runApp(TodoApp());

class TodoApp extends StatefulWidget {
  @override
  _TodoAppState createState() => _TodoAppState();
}

class _TodoAppState extends State<TodoApp> {
  // ignore: close_sinks
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    //그냥 BlocProvider로 사용을 해주어도 되지만 나중에 기능 추가를 위하여 미리 MultiBlocProvider로 선언을 하였다.
    return MultiBlocProvider(
      providers: [
        BlocProvider<TodoBloc>(
          create: (BuildContext context) => TodoBloc(),
        ),
      ],
      child: MaterialApp(
          title: 'Todo',
          //해당 앱의 전체적인 테마를 설정해주게 될것이다.
          theme: ThemeData(
              primaryColor: Color(0xFFF8F8F8),
              backgroundColor: Color(0xFFF8F8F8),
              scaffoldBackgroundColor: Color(0xFFF8F8F8),
              accentColor: Color(0xFF3A5EFF)),
          //가장 먼저 실행이 될 부분이 무엇인지를 정해주는 부분으로 대게 splash페이지나 login 페이지로 설정을 한다.
          home: TodoList(),
          
          //Navigator.of(context).pushNamed(route name);형식으로 써주기 위해 선언을 해주는 부분
          routes: {
            "/todoList": (BuildContext context) => TodoList(),
            "/todoAdd": (BuildContext context) => TodoAdd(),
          },
       ),
    );
  }
}

3. Todo List의 경우 Todo라는 하나의 모델을 선정하여 만들면 List 관리가 더 쉽게 될것이다. 그렇기 때문에 Todo model을 만들어 준다. model의 경우 후에 필요한것을 추가하여 줄 수 있다. 하지만 그와 관련된 모든것들에 추가를 해주어야 하는 불편함이 있을수 있다. 하지만 데이터 관리와 같은 측면에서 매우 효율적이라고 느껴 모델 사용을 좋아한다.

import 'package:meta/meta.dart';
//해당 클래스를 통하여 모델을 선언하여 주고 사용할수 있도록 한다.
class Todo {
  final int id; //todo마다의 고유한 ID
  final String todo;//내가 해야할것
  final String desc;//내가 할것에 대한 부가적인 설명을 적어두기 위한 작업
  final String date;//해당 todo의 날짜
  final bool checked;//해당 todo를 완료 했는지 않았는지 확인하기 위한 용도
	
  Todo({
    @required this.id,
    @required this.todo,
    @required this.desc,
    @required this.date,
    @required this.checked,
  });
}

4. 그 후 필요한 state, event, bloc들을 선언하여 주어야 한다. 이 포스팅에서는 필요 데이터들이 화면에 그려지는 것 까지만 할 것이기 때문에 state, event, bloc에서는 많은것들을 할 필요가 없다.

4-1 todoList_state.dart 페이지는 다음과 같이 만들어 질수가 있다.

import 'package:flutter_app_todo/blocs/models/todo_model.dart';
import 'package:meta/meta.dart';

class TodoState {
//Todo model타입을 가진 리스트이므로 todoList의 모든 요소들은 Todo 모델 형식을 따라야한다.
  final List<Todo> todoList;

  TodoState({
    @required this.todoList,
  });

//현재는 저장된 데이터가 하나도 없기 때문에 다음과 같이 초기 상태를 정해주는 과정이 필요하다.
//(더미 데이터를 만들어 사용하는 것이다.)
  factory TodoState.empty() {
    return TodoState(
      todoList: [
        Todo(
            id: 0,
            todo: "토익 공부하기",
            date: "2020.02.27",
            checked: false,
            desc: "토익 공부 열심히 해야해..."),
        Todo(
            id: 1,
            todo: "swift인강듣기",
            date: "2020.02.27",
            checked: false,
            desc: "ios 위젯을 만들어야하는데..."),
        Todo(
            id: 2,
            todo: "양파 썰기",
            date: "2020.02.27",
            checked: false,
            desc: "양파야 너무 맵다..."),
        Todo(
            id: 3,
            todo: "가스비 내기!!",
            date: "2020.02.27",
            checked: false,
            desc: "통장에서 자동이체라니...으으으으 마음 아파."),
        Todo(
            id: 4,
            todo: "월세 내기!",
            date: "2020.02.27",
            checked: false,
            desc: "월세는 언제 내야 하더라..."),
        Todo(
            id: 5,
            todo: "영양제 챙겨 먹",
            date: "2020.02.27",
            checked: false,
            desc: "영양제를 꼭꼭 챙겨먹어요...특히 비타민 D가 필요해"),
      ],
    );
  }

  TodoState update({List<Todo> todoList}) {
    return copyWith(
      todoList: todoList,
    );
  }

  TodoState copyWith({List<Todo> todoList}) {
    return TodoState(todoList: todoList ?? this.todoList);
  }
}

4-2 todoList_event.dart는 다음과 같다.

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

@immutable
abstract class TodoListEvent extends Equatable {
  TodoListEvent([List props = const []]) : super(props);
}

//해당 페이지가 그려질때 일어나야 하는 event이다.
class TodoPageLoaded extends TodoListEvent {
  @override
  String toString() {
    // TODO: implement toString
    return "TodoPageLoaded";
  }
}

//각각의 todolist 들의 check box들이 클릭이 됐을때 일어날 event이다.
//indes를 받게 되는 이유는 ListView.builder를 사용하여 화면을 그려줄 예정이기 때문이다.
class TodoListCheck extends TodoListEvent {
  final int index;

  TodoListCheck({@required this.index});

  @override
  String toString() {
    // TODO: implement toString
    return "TodoListCheck";
  }
}

4-3 todoList_bloc.dart

import 'package:bloc/bloc.dart';
import 'package:flutter_app_todo/blocs/models/todo_model.dart';
import 'package:flutter_app_todo/blocs/todoBloc/bloc.dart';

class TodoBloc extends Bloc<TodoListEvent, TodoState> {
  @override
  // TODO: implement initialState
  // 가장 먼저 일어나게 state의 초기화 작업
  TodoState get initialState => TodoState.empty();

  @override
  Stream<TodoState> mapEventToState(TodoListEvent event) async* {
    // TODO: implement mapEventToState
    if (event is TodoPageLoaded) {
      yield* _mapTodoPageLoadedToState();
    } else if (event is TodoListCheck) {
      yield* _mapTodoListCheckToState(event.index);
    }
  }

  Stream<TodoState> _mapTodoPageLoadedToState() async* {
    yield state.update(todoList: state.todoList);
  }

//Todo model을 만들당시 checked라는 변수가 존재했다. 해당 변수는 내가 현재 클릭한 Todo를 선택 했었는지 말았는지 확인을 위해 만들어 둔것이다.
//그래서 선택이 되있는 상황이라면 false로, 선택이 되어있지 않은 상태였다면 true로 바꿔주는 작업이 필요하다.
//state.todoList에서 해당 index의 것을 새로이 삽입하여 업데이트 시키는 과정이다.
  Stream<TodoState> _mapTodoListCheckToState(int index) async* {
    Todo currentTodo = Todo(
        id: state.todoList[index].id,
        todo: state.todoList[index].todo,
        date: state.todoList[index].date,
        checked: state.todoList[index].checked == true ? false : true);

    List<Todo> cTodoList = state.todoList;
    cTodoList[index] = currentTodo;
    yield state.update(todoList: cTodoList);
  }
}

5. 위와 같은 작업들이 완료되면 TodoList 화면을 그려주면 된다. 앞에서 말한것과 같이 ListView.Builder를 사용하여 Todo 타입의 리스트를 불러와 하나씩 그려주게 만들것이다. 이렇게 하게 되면 list 각각의 것에 대해 접근하고 관리하기가 편해진다.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app_todo/blocs/todoBloc/bloc.dart';
import 'package:flutter_app_todo/todo_add.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class TodoList extends StatefulWidget {
  _TodoList createState() => _TodoList();
}

class _TodoList extends State<TodoList> {
  TodoBloc _todoBloc;
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _todoBloc = BlocProvider.of<TodoBloc>(context);
    _todoBloc.add(TodoPageLoaded());
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return BlocListener(
        bloc: _todoBloc,
        listener: (BuildContext context, TodoState state) {},
        child: Scaffold(
          appBar: AppBar(
            centerTitle: true,
            title: Text("Todo List"),
            actions: <Widget>[
              IconButton(
                  icon: Icon(Icons.add),
                  onPressed: () {
                    Navigator.push(
                        context,
                        MaterialPageRoute(
                            builder: (BuildContext context) =>
                                BlocProvider.value(
                                    value: _todoBloc, child: TodoAdd())));
                  })
            ],
          ),
          body: BlocBuilder(
            bloc: _todoBloc,
            builder: (BuildContext context, TodoState state) {
              return Column(
                children: <Widget>[
                  Container(
                    height: MediaQuery.of(context).size.height / 2,
                    child: ListView.builder(
                        itemCount: state.todoList.length,
                        itemBuilder: (BuildContext context, int index) {
                          return ListTile(
                            title: Text(state.todoList[index].todo),
                            //listTile을 누르게 되면 해당 list에대한 dialog가 뜨게 된다.
                            onTap: () {
                              _showDialog(state.todoList[index].todo,
                                  state.todoList[index].desc);
                            },
                            //앞부분에 체크박수를 두어 체크박스업룰 누르게 되면 해당 index의 것이 체크가 될수 있도록 해주는 작
                            leading: Checkbox(
                              value: state.todoList[index].checked,
                              onChanged: (bool newValue) {
                                _todoBloc.add(TodoListCheck(index: index));
                              },
                            ),
                          );
                        }),
                  )
                ],
              );
            },
          ),
        ));
  }

  //눌렀을때 부가적인 설명에 대한 dialog가 나올수 있도록 하기 위한 작업이다.
  //일단은 해야할것의 제목, 부가적인 설명을 받아 다이얼로그 페이지에 보일수 있도록 해두었다.
  void _showDialog(String title, String description) async {
    showDialog(
      context: context,
      barrierDismissible: false, // user must tap button!
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text(title),
          content: description.isNotEmpty
              ? Text(description)
              : Text("부가적인 설명을 적지 않았습니다."),
          actions: <Widget>[
            FlatButton(
              child: Text('OK'),
              onPressed: () {
                Navigator.pop(context, "OK");
              },
            ),
          ],
        );
      },
    );
  }
}

 

Comments