Flutter 개발-6: 앱 UI 개선 및 기능 확장

Flutter를 사용한 UI 구현은 개발자에게 무한한 가능성을 제공합니다. 이번 포스팅에서는 이전에 만든 To-Do 리스트 UI구현을 개선하고 몇 가지 새로운 기능을 추가해 보겠습니다. Flutter의 풍부한 위젯 라이브러리를 활용하여 UI 구현을 한 단계 더 발전시켜 보겠습니다.

1. 앱 UI 구현 개선하기

UI 시각 효과 개선하기

Flutter의 Material Design 위젯을 활용하여 UI 구현을 개선해 보겠습니다. 먼저, 각 To-Do 항목을 Card 위젯으로 감싸 시각적으로 더 구분되게 만들겠습니다. 이는 UI 구현에서 중요한 요소인 사용자 경험(UX)을 향상시키는 방법 중 하나입니다.

lib/main.dart 파일의 TodoList 클래스를 다음과 같이 수정합니다:

class TodoList extends StatelessWidget {
  final List<Todo> todos;
  final Function(Todo) onTodoToggle;
  final Function(Todo) onTodoDelete;

  TodoList({
    required this.todos,
    required this.onTodoToggle,
    required this.onTodoDelete,
  });

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: todos.length,
      itemBuilder: (context, index) {
        return Card(
          elevation: 2,
          margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
          child: ListTile(
            leading: Checkbox(
              value: todos[index].isCompleted,
              onChanged: (_) => onTodoToggle(todos[index]),
            ),
            title: Text(
              todos[index].title,
              style: TextStyle(
                decoration: todos[index].isCompleted
                    ? TextDecoration.lineThrough
                    : null,
              ),
            ),
            trailing: IconButton(
              icon: Icon(Icons.delete),
              onPressed: () => onTodoDelete(todos[index]),
            ),
          ),
        );
      },
    );
  }
}

이렇게 Card 위젯을 사용하면 각 To-Do 항목이 더 명확히 구분되어 보이며, UI 구현의 품질을 한 단계 높일 수 있습니다.

앱 UI 구현의 반응성 높이기

Flutter의 장점 중 하나는 반응형 UI 구현이 쉽다는 점입니다. setState 메서드를 사용하여 To-Do 항목의 상태가 변경될 때마다 UI를 즉시 업데이트할 수 있습니다.

void _toggleTodo(Todo todo) {
  setState(() {
    todo.isCompleted = !todo.isCompleted;
    _saveTodos();
  });
}

이러한 반응형 앱 UI 구현은 사용자에게 즉각적인 피드백을 제공하여 앱의 사용성을 크게 향상시킵니다.

앱 UI 구현의 일관성 유지하기

앱 UI 구현에서 중요한 또 다른 측면은 디자인의 일관성입니다. Flutter의 ThemeData를 사용하여 앱 전체의 색상과 스타일을 일관되게 관리할 수 있습니다.

MaterialApp(
  title: 'To-Do List',
  theme: ThemeData(
    primarySwatch: Colors.blue,
    // 다른 테마 속성들...
  ),
  home: TodoListScreen(),
)

이렇게 설정한 테마는 앱 전체에 적용되어, 일관된 UI 구현을 가능하게 합니다.

2. 할 일 삭제 기능 추가

이제 각 To-Do 항목에 삭제 버튼을 추가했으니, 실제로 삭제 기능을 구현해 봅시다. _TodoListScreenState 클래스에 다음 메서드를 추가합니다:

void _deleteTodo(Todo todo) {
  setState(() {
    todos.remove(todo);
    _saveTodos();
  });
}

그리고 build 메서드 내의 TodoList 위젯 생성 부분을 다음과 같이 수정합니다:

TodoList(
  todos: todos,
  onTodoToggle: _toggleTodo,
  onTodoDelete: _deleteTodo,
),

3. 할 일 수정 기능 추가

마지막으로, 기존 To-Do 항목을 수정할 수 있는 기능을 추가해 보겠습니다. _TodoListScreenState 클래스에 다음 메서드를 추가합니다:

void _editTodo(Todo todo) {
  showDialog(
    context: context,
    builder: (BuildContext context) {
      String editedTodo = todo.title;
      return AlertDialog(
        title: Text('Edit todo'),
        content: TextField(
          onChanged: (value) {
            editedTodo = value;
          },
          controller: TextEditingController(text: todo.title),
        ),
        actions: <Widget>[
          TextButton(
            child: Text('Cancel'),
            onPressed: () {
              Navigator.of(context).pop();
            },
          ),
          TextButton(
            child: Text('Save'),
            onPressed: () {
              setState(() {
                todo.title = editedTodo;
                _saveTodos();
              });
              Navigator.of(context).pop();
            },
          ),
        ],
      );
    },
  );
}

그리고 TodoList 클래스의 ListTileonTap 속성을 추가하여 수정 기능을 연결합니다:

ListTile(
  // ... 기존 코드 ...
  onTap: () => onTodoEdit(todos[index]),
),

TodoList 클래스의 생성자와 _TodoListScreenStatebuild 메서드도 적절히 수정해야 합니다.

4. 최종 코드

이제 전체 main.dart 파일은 다음과 같이 됩니다:

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'To-Do List',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: TodoListScreen(),
    );
  }
}

class TodoListScreen extends StatefulWidget {
  @override
  _TodoListScreenState createState() => _TodoListScreenState();
}

class _TodoListScreenState extends State<TodoListScreen> {
  List<Todo> todos = [];

  @override
  void initState() {
    super.initState();
    _loadTodos();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('My To-Do List'),
      ),
      body: TodoList(
        todos: todos,
        onTodoToggle: _toggleTodo,
        onTodoDelete: _deleteTodo,
        onTodoEdit: _editTodo,
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _addTodo,
        child: Icon(Icons.add),
      ),
    );
  }

  void _addTodo() {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        String newTodo = "";
        return AlertDialog(
          title: Text('Add a new todo'),
          content: TextField(
            onChanged: (value) {
              newTodo = value;
            },
          ),
          actions: <Widget>[
            TextButton(
              child: Text('Add'),
              onPressed: () {
                setState(() {
                  todos.add(Todo(title: newTodo));
                  _saveTodos();
                });
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }

  void _toggleTodo(Todo todo) {
    setState(() {
      todo.isCompleted = !todo.isCompleted;
      _saveTodos();
    });
  }

  void _deleteTodo(Todo todo) {
    setState(() {
      todos.remove(todo);
      _saveTodos();
    });
  }

  void _editTodo(Todo todo) {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        String editedTodo = todo.title;
        return AlertDialog(
          title: Text('Edit todo'),
          content: TextField(
            onChanged: (value) {
              editedTodo = value;
            },
            controller: TextEditingController(text: todo.title),
          ),
          actions: <Widget>[
            TextButton(
              child: Text('Cancel'),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
            TextButton(
              child: Text('Save'),
              onPressed: () {
                setState(() {
                  todo.title = editedTodo;
                  _saveTodos();
                });
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }

  void _loadTodos() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    String? todosString = prefs.getString('todos');
    if (todosString != null) {
      List<dynamic> todoList = jsonDecode(todosString);
      setState(() {
        todos = todoList.map((item) => Todo.fromJson(item)).toList();
      });
    }
  }

  void _saveTodos() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    String todosString = jsonEncode(todos.map((todo) => todo.toJson()).toList());
    await prefs.setString('todos', todosString);
  }
}

class TodoList extends StatelessWidget {
  final List<Todo> todos;
  final Function(Todo) onTodoToggle;
  final Function(Todo) onTodoDelete;
  final Function(Todo) onTodoEdit;

  TodoList({
    required this.todos,
    required this.onTodoToggle,
    required this.onTodoDelete,
    required this.onTodoEdit,
  });

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: todos.length,
      itemBuilder: (context, index) {
        return Card(
          elevation: 2,
          margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
          child: ListTile(
            leading: Checkbox(
              value: todos[index].isCompleted,
              onChanged: (_) => onTodoToggle(todos[index]),
            ),
            title: Text(
              todos[index].title,
              style: TextStyle(
                decoration: todos[index].isCompleted
                    ? TextDecoration.lineThrough
                    : null,
              ),
            ),
            trailing: IconButton(
              icon: Icon(Icons.delete),
              onPressed: () => onTodoDelete(todos[index]),
            ),
            onTap: () => onTodoEdit(todos[index]),
          ),
        );
      },
    );
  }
}

class Todo {
  String title;
  bool isCompleted;

  Todo({required this.title, this.isCompleted = false});

  factory Todo.fromJson(Map<String, dynamic> json) {
    return Todo(
      title: json['title'],
      isCompleted: json['isCompleted'],
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'title': title,
      'isCompleted': isCompleted,
    };
  }
}

마무리

이번 포스팅에서는 Flutter를 사용한 UI 구현의 다양한 측면을 살펴보았습니다. 시각적 개선, 기능적 요소 추가, 반응성 향상, 그리고 디자인의 일관성 유지 등 UI 구현의 여러 측면을 고려하여 To-Do 리스트 앱을 개선했습니다.

Flutter를 사용한 UI 구현은 이제 막 시작일 뿐입니다. Flutter의 공식 문서와 위젯 카탈로그를 참고하면 더 다양하고 세련된 UI 구현이 가능합니다.

다음 포스팅에서는 상태 관리 라이브러리를 도입하여 앱의 구조를 개선하고 UI 구현의 효율성을 높이는 방법에 대해 알아보겠습니다. Flutter 앱 UI 구현에 대해 더 궁금한 점이 있다면 댓글로 남겨주세요!

관련 포스트

Flutter 개발-2: 프로젝트 생성 및 구조 파악 – CSAI

Flutter 개발-3: To-Do 리스트 앱 UI 구현하기 – CSAI

Flutter 개발-4: 앱 상태 관리 추가하기 – CSAI

Flutter-5: 로컬 저장소 추가 – CSAI

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다