본문 바로가기
Development/Flutter

Flutter - 8. TodoList App (1) 기본 UI 구성하기

by du.it.ddu 2021. 1. 18.
반응형

이전 포스팅에서 예고했던 대로, Flutter로 간단한 TodoList App을 만들어 볼 것이다.

이번 포스팅은 TodoListApp의 기본 UI를 구성해보도록 하겠다.

위와 같은 UI를 작성할 것이며, BottomNavigationBar의 아이템을 클릭하면 현재의 아이템도 변경되도록 작업 할 것이다.
위 UI를 기반으로 기능을 붙여나가는 방식으로 진행 할 것이다.


1. 루트 페이지 만들기

class TabItem {
  String _title;
  IconData _icon;

  TabItem(String title, IconData icon) {
    _title = title;
    _icon = icon;
  }

  String getTitle() => _title;
  IconData getIcon() => _icon;
}

BottomNavigationBar의 TabItem은 타이틀(Label)과 아이콘을 갖는다.
위와 같은 클래스를 작성하자.

void main() {
  runApp(TodoListApp());
}

class TodoListApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _TodoListAppState();
}

class _TodoListAppState extends State<TodoListApp> {
  final List<TabItem> _tabItems = [
    TabItem("Todo", Icons.clear),
    TabItem("In Progress", Icons.loop),
    TabItem("Done", Icons.check),
  ];

  int _currentTabIndex = 0;

  @override
  Widget build(BuildContext context) {
  }
}

그리고 main.dart를 위와 같이 변경하자.
현재 선택한 아이템의 Index의 상태를 갖는 StatefulWidget이 루트 페이지가 된다.
탭 아이템들은 final로 리스트로 갖도록 정의한다.

이제 _TodoListAppState의 build 함수를 구현한다.

class _TodoListAppState extends State<TodoListApp> {
  ...

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter TodoList',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Scaffold(
          appBar: AppBar(title: Text(_tabItems[_currentTabIndex].getTitle())),
      )
    );
  }
}

Scaffold를 사용하도록 한다. 
appBar를 생성하고, 현재 선택된 탭의 인덱스로 데이터를 가져오고 Title을 설정해준다.

class _TodoListAppState extends State<TodoListApp> {
  ...

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      ...
      home: Scaffold(
          ...
          bottomNavigationBar: _createBottomNavigationBar(),
      )
    );
  }
  
    Widget _createBottomNavigationBar() {
    return BottomNavigationBar(
      items: _tabItems.map((tabItem) =>
          BottomNavigationBarItem(icon: Icon(tabItem.getIcon()), label: tabItem.getTitle()),
      ).toList(),
      currentIndex: _currentTabIndex,
      onTap: (int index) => {
        _onTabClick(index)
      },
    );
  }

  void _onTabClick(int index) {
    setState(() {
      _currentTabIndex = index;
    });
  }
}

이제 위와 같이 작성하여 BottomNavigationBar를 생성해준다.

List<TabItem>을 List<BottomNavigationBarItem>으로 변경하기 위해 map 메서드를 사용했다.

BottomNavigationBar의 onTap을 구현해준다. onTap은 선택된 아이템의 index를 반환해준다. 
_onTabClick에 index를 넘기고 setState() 함수를 사용하여 _currentTabIndex의 상태를 변경해준다.

이제 여기까지 완성되었을 것이다. 탭을 클릭하면 클릭된 탭으로 이동되는 것을 확인할 수 있다.


2. TodoList 페이지 만들기

class TodoListPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _TodoListPageState();
}

class _TodoListPageState extends State<TodoListPage> {
  @override
  Widget build(BuildContext context) {
    return _createTodoList();
  }
  
  ...
}

todo_list.dart 파일을 만들고 위 처럼 기본적인 StatefulWidget의 뼈대를 만든다.
추후에 목록이 추가되고 삭제되고 변경되면서 상태가 변경되는 Page이기 때문에 StatefulWidget으로 작성한다.
build함수에선 _createTodoList() 함수를 호출하여 화면을 그리도록 할 것이다.

class _TodoListPageState extends State<TodoListPage> {
  @override
  Widget build(BuildContext context) {
    return _createTodoList();
  }
  
  Widget _createTodoList() {
    return ListView.separated(
      itemCount: 10,
      itemBuilder: (BuildContext context, int index) {
        return _createTodoCard();
      },
      separatorBuilder: (BuildContext context, int index) {
        return Divider(
          thickness: 8.0,
          height: 8.0,
          color: Colors.transparent,
        );
      },
    );
  }
  
  ...
}

_createTodoList 함수를 구현한다. ListView는 ListView.separated를 사용하여 생성한다.

현재 아이템은 없으므로 임의로 10을 준다.
그리고 itemBuilder를 구현하는데, _crateTodoCard() 함수를 호출하도록 한다.

separatorBuilder는 간단하게 8.0px의 Divider를 갖도록 구현하였다.

class _TodoListPageState extends State<TodoListPage> {
  @override
  Widget build(BuildContext context) {
    return _createTodoList();
  }
  ...
  
  Widget _createTodoCard() {
    return Card(
      elevation: 4.0,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8.0))),
      child: Container(
          padding: EdgeInsets.all(16.0), 
          child: _createTodoItemRow()
      ),
    );
  }
  
  ...
}

createTodoCard 함수를 구현한다.

Card Widget을 사용하여 귀퉁이가 약간 둥근 형태의 카드를 만든다.
child로 16px의 패딩을 갖는 Container를 가진다

내부 Container는 _createTodoItemRow를 호출하여 child를 생성한다.

class _TodoListPageState extends State<TodoListPage> {
  @override
  Widget build(BuildContext context) {
    return _createTodoList();
  }
  ...

  Widget _createTodoItemRow() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      crossAxisAlignment: CrossAxisAlignment.center,
      children: [
        _createTodoItemContentWidget(),
        Icon(Icons.keyboard_arrow_right, color: Colors.blue)
      ],
    );
  }
  
  ...
}

_createTodoItemRow 함수를 구현한다.

Row를 사용하여 좌측엔 _createTodoItemContentWidget을, 우측엔 오른쪽 화살표 모양의 아이콘을 갖는 Row를 반환한다.

class _TodoListPageState extends State<TodoListPage> {
  @override
  Widget build(BuildContext context) {
    return _createTodoList();
  }
  ...

  Widget _createTodoItemContentWidget() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text("Todo Item Title",
            style: TextStyle(fontSize: 24.0, color: Colors.blue)),
        Divider(
          thickness: 8.0,
          height: 8.0,
          color: Colors.transparent,
        ),
        Text("2021.01.18",
            style: TextStyle(fontSize: 18.0, color: Colors.blueGrey))
      ],
    );
  }
}

_createTodoItemContentWidget 함수를 구현한다.

Column을 사용하여 구현한다. 위는 Todo Item의 Title을, 아래에는 Todo Item의 생성 날짜를 나타내는 Text로 구성한다.

여기까지 잘 따라왔다면 위와 같은 TodoList가 보여질 것이다.


현재까지의 코드는 아래 Github에 저장되어 있다.
Github : 
github.com/DuItDDu/Flutter-Codelabs/tree/master/Flutter-TodoList/flutter_todolist

아직은 UI만 존재할 뿐이다.

이를 바탕으로 정말 TodoItem을 입력하여 추가하고 상태를 변경해보자.

더 나아가서 SQLite를 연동하여 데이터베이스 처리까지 할 것이며, Flutter에서 권장하는 아키텍처 패턴도 다뤄 볼 것이다.

반응형