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
클래스의 ListTile
에 onTap
속성을 추가하여 수정 기능을 연결합니다:
ListTile(
// ... 기존 코드 ...
onTap: () => onTodoEdit(todos[index]),
),
TodoList
클래스의 생성자와 _TodoListScreenState
의 build
메서드도 적절히 수정해야 합니다.
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 구현이 가능합니다.
- Flutter 공식 홈페이지: Flutter – Build apps for any screen
- Flutter 위젯 카탈로그: Widgets | Flutter
다음 포스팅에서는 상태 관리 라이브러리를 도입하여 앱의 구조를 개선하고 UI 구현의 효율성을 높이는 방법에 대해 알아보겠습니다. Flutter 앱 UI 구현에 대해 더 궁금한 점이 있다면 댓글로 남겨주세요!
관련 포스트
Flutter 개발-2: 프로젝트 생성 및 구조 파악 – CSAI
Flutter 개발-3: To-Do 리스트 앱 UI 구현하기 – CSAI
Flutter 개발-4: 앱 상태 관리 추가하기 – CSAI