[Flutter] 여러 가지 화면과 사용자 입력 관리
플러터에서 스크롤 기능을 구현할 때는 ListView 위젯을 사용한다.
class ExpensesList extends StatelessWidget {
const ExpensesList({super.key, required this.expenses});
final List<Expense> expenses;
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: expenses.length,
itemBuilder: (ctx, index) => Text(expenses[index].title)
);
}
}
여러 개의 항목을 위젯으로 보여줄 때, 보여줄 항목이 많으면 Column 위젯보다는 ListView 위젯을 사용해 스크롤 할 수 있도록 설계하는 편이 합리적이다.
위의 예시에서 ListView.builder 메서드는 런타임에 ListView 항목을 동적으로 생성하는 함수를 제공받는다.
itemCount 속성으로 ListView.builder가 생성해야 하는 요소의 총 개수를 지정해 메모리를 효과적으로 사용해서 필요한 요소만 렌더링한다. (lazy-loading)
항목의 수가 많거나 무거울 때 ListView.builder 방식으로 렌더링한다.
이 외에도 스크롤을 구현하는 방법은 여러 가지가 있다.
1. ListView : lazy-loading으로 화면에 표시되는 부분만 렌더링한다.
2. SingleChildScrollView : 단일 자식 위젯에 대해 스크롤 기능을 제공한다.
3. PageView : 페이지를 통째로 스크롤하는 기능으로 페이지 간 이동을 구현할 때 사용한다.
4. Scrollable : 직접 스크롤 가능한 동작을 제어할 수 있는 낮은 수준의 스크롤 인터페이스를 제공한다.
...
상황에 따라 알맞는 위젯을 사용하자.
여기서는 효과적으로 렌더링하기 위해 ListView 위젯을 사용했다.
웹에서 사용하던 것 처럼 플러터에서 모달 창을 띄우려면 showModalBottomSheet 메서드를 사용한다.
class _ExpensesState extends State<Expenses> {
...
void _openAddExpenseOverlay() {
showModalBottomSheet(context: context, builder: builder)
}
...
}
보통 build 메서드가 그렇듯, 해당 메서드는 context 파라미터와 builder 파라미터를 생성자에서 받도록 설정되어있다.
(별개로 super.key 에서 key는 위젯의 고유한 식별자로 사용된다)
플러터의 context 객체는 애플리케이션의 현재 상태와 위치에 대한 정보를 포함하고 있는 객체로 위젯 트리 (위젯들이 어떻게 관련되어있는지 나타내는 구조) 에서 해당 위젯이 어떤 위젯 아래에 위치하고 있는지에 관한 정보를 알 수 있다.
builder 파라미터는 해당 모달 창을 띄울 때 어떤 위젯을 렌더링할 지 지정하는 역할을 수행한다.
builder 함수가 실행될 때는 항상 context 객체를 파라미터로 전달받는다.
플러터의 모달 창과 비동기 프로그래밍에 대해 살펴보자.
import 'package:flutter/material.dart';
import 'package:expense_tracker/models/expense.dart';
class NewExpense extends StatefulWidget {
const NewExpense({super.key});
@override
State<NewExpense> createState() => _NewExpenseState();
}
class _NewExpenseState extends State<NewExpense> {
final _titleController = TextEditingController();
final _amountController = TextEditingController();
DateTime? _selectedDate;
void _presentDatePicker() async {
final now = DateTime.now();
final firstDate = DateTime(now.year - 1, now.month, now.day);
final pickedDate = await showDatePicker(
context: context,
initialDate: now,
firstDate: firstDate,
lastDate: now,
);
// await 후는 pickedDate가 저장된 후에 실행됨
setState(() {
_selectedDate = pickedDate;
});
}
@override
void dispose() {
_titleController.dispose();
_amountController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: _titleController,
maxLength: 50,
decoration: const InputDecoration(
label: Text('Title'),
),
),
Row(
children: [
Expanded(
child: TextField(
controller: _amountController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
prefixText: '\$ ',
label: Text('Amount'),
),
),
),
const SizedBox(
width: 16,
),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(_selectedDate == null ? 'No date selected' : formatter.format(_selectedDate!)), // ! 구문으로 널이 아님을 명시함
IconButton(
onPressed: _presentDatePicker,
icon: const Icon(Icons.calendar_month),
),
],
),
),
],
),
Row(
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () {
//....
},
child: const Text('Save')
)
],
),
],
),
);
}
}
위젯을 렌더링하는 메서드를 만들자.
StatefulWidget을 구현할 때 최상단에 material.dart 클래스를 import 하는데, 같은 기능을 제공하는 다른 클래스들이 있다.
material.dart : 구글의 material.dart 스타일의 위젯을 제공한다.
cupertino.dart : IOS의 디자인 언어인 Human Interface Guidelines 를 따르는 위젯을 제공한다.
widgets.dart : 위의 2개와 다르게 특정 디자인 시스템을 이용하지 않고 기본적인 위젯만 제공한다.
웹에서 <input> 태그처럼 사용자가 입력할 수 있는 UI를 만들 때는 TextField 위젯을 사용한다.
최대 글자 수, 어떤 키보드를 열 지 같은 설정을 수행할 수 있다.
사용자의 입력 값을 관리하거나 특정 위젯의 상태를 관리할 때는 플러터가 제공하는 Controller를 사용한다.
일반적으로 TextField나 Scrollable 위젯처럼 같은 상태를 가진 위젯에 연결돼 해당 위젯의 상태를 업데이트하고 액세스할 때 사용한다.
위의 예시에서는 사용자가 입력한 값을 컨트롤러가 관리하고 모달 창을 제출할 때 입력한 값으로 특정 작업을 수행한다.
모달 창을 닫은 후에는 컨트롤러를 사용하지 않도록 dispose 메서드를 오버라이드해서 더 이상 컨트롤러를 사용하지 않도록 설정해주자.
onChange 속성을 사용해 사용자가 값을 입력할 때 마다 특정 변수에 저장하는 방식보다 효율적이다.
showDatePicker 메서드로 날짜 값을 가져오는데.. 해당 메서드의 반환 타입은 Future<DateTime?> 이다.
Future 객체는 다트에서 미래에 어떤 값을 반환하거나 예외를 던지는 객체이다.
반환 값이나 예외를 지금 말고 나중에 받도록 약속한다.
async 키워드는 해당 함수가 비동기적으로 실행되도록 설정할 때 사용한다.
비동기 함수는 항상 Future 객체를 반환하고, 함수가 값을 반환하면 Future에 저장된다.
await 키워드는 Future가 완료될 때 까지 함수의 실행을 중단한다.
따라서 항상 async 함수 내부에서만 사용되어야 하고 Future에 값이 저장된 후 함수를 이어서 실행한다.
위의 예시에서는 Future 객체에 값이 저장된 후 setState 메서드로 위젯을 다시 렌더링한다.
class ExpensesList extends StatelessWidget {
const ExpensesList({
super.key,
required this.expenses,
required this.onRemoveExpense,
});
final List<Expense> expenses;
final void Function(Expense expense) onRemoveExpense;
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: expenses.length,
itemBuilder: (ctx, index) => Dismissible(
key: ValueKey(expenses[index]),
onDismissed: (direction) {
onRemoveExpense(expenses[index]);
},
child: ExpenseItem(
expense: expenses[index]
),
),
);
}
}
위젯을 스와이프 가능하도록 설정할 때는 Dismissible위젯을 사용한다.
리스트에서 해당 위젯을 스와이프해서 제거할 때 사용하며, 반드시 고유한 key 값을 명시해 줘야 한다.
Dismissible 위젯이 삭제됐을 때 플러터는 위젯 트리를 업데이트하기 위해 해당 key를 사용한다.
key는 리스트의 각 항목을 플러터가 구분할 수 있도록 하는 역할을 수행한다.
스와이프하는 방향에 상관없이 값을 삭제할 수 있도록 direction을 무시하도록 함수를 작성했다.
스와이프 한 후에는 실제로 리스트에서 값을 삭제해야 하는데, 이 때 지정한 key 값이 사용된다.
void _removeExpense(Expense expense) {
final expenseIndex = _registeredExpenses.indexOf(expense);
setState(() {
_registeredExpenses.remove(expense);
});
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
duration: const Duration(seconds: 3),
content: const Text('삭제되었습니다!'),
action: SnackBarAction(
label: 'Undo',
onPressed: () {
setState(() {
_registeredExpenses.insert(expenseIndex, expense);
});
},
),
),
);
}
스와이프로 요소를 삭제한 후에는 화면 하단에 SnackBar 창을 띄워서 잠시 동안 복원할 수 있도록 하자.
ScaffoldMessenger 위젯은 SnackBar 메세지를 보여줄 때 효과적이다.
of 메서드는 위젯 트리에서 현재 컨텍스트에 가장 가까운 특정 타입의 위젯을 찾을 때 사용한다.
ScaffoldMessenger.of(context) 에서는 현재 context에서 가장 가까운 ScaffoldMessenger를 반환한다.
ScaffoldMessenger는 일반적으로 Scaffold 위젯이나 MaterialApp 위젯에 포함돼있다.
그런데 만약 가까운 ScaffoldMessenger가 없다면? 플러터는 에러를 뱉는다.
위에서 진행한 것 처럼 컨트롤러를 사용해서 사용자의 입력을 관리할 수도 있지만, HTML 웹처럼 플러터는 Form 위젯을 통해 사용자의 입력을 쉽게 관리하는 기능을 제공한다.
final _formKey = GlobalKey<FormState>();
Form(
key: _formKey,
child: Column(
children: <Widget>[
TextFormField(
validator: (value) {
if (value == null || value.isEmpty) {
return '내용을 입력하세요.';
}
return null;
},
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState.validate()) {
}
},
child: Text('제출'),
),
],
),
)
버튼을 누르면 Form의 내용을 검증하는 작업을 수행한다.
이전에는 입력 필드가 4개라면 컨트롤러도 4개 만들어서 입력값을 관리해야 했지만, Form으로 입력 필드를 한 번에 관리한다.
Form 위젯은 FormState 객체를 생성하고 FormState는 FormField의 상태를 관리한다.
Form의 key를 통해 FormField에 접근할 수 있다.
이 때 key는 GlobalKey 또는 ValueKey로 구분된다.
1. GlobalKey
전체 애플리케이션에서 고유한 키로, 위젯의 내부 상태에 접근할 때 사용된다.
비용이 많이 든다.
2. ValueKey
위젯의 값을 식별할 때 사용된다.
같은 리스트나 그리드 내부에서만 고유하기에 비용이 저렴하다.
특정 리스트에서 항목의 순서를 바꾸거나 제거할 때 플러터에게 어떤 항목이 어떤 위젯에 대응하는지 알려줄 때 사용한다.
FormState에 접근하려면 GlobalKey<FormState> 를 사용해야 하기 때문에
Form을 관리할 때는 Key값을 GlobalKey 로 관리하는 편이 합리적이다.
'Mobile > Flutter' 카테고리의 다른 글
[Flutter] Riverpod 상태 관리 (0) | 2023.06.24 |
---|---|
[Flutter] Theme와 세 가지 트리 (0) | 2023.06.19 |
[Flutter] 위젯 렌더링과 다트 문법 (0) | 2023.06.16 |
[Flutter] Dart 언어와 StatefulWidget (0) | 2023.06.14 |
[Flutter] 프로젝트 구조와 Widget (1) | 2023.06.11 |
댓글
이 글 공유하기
다른 글
-
[Flutter] Riverpod 상태 관리
[Flutter] Riverpod 상태 관리
2023.06.24 -
[Flutter] Theme와 세 가지 트리
[Flutter] Theme와 세 가지 트리
2023.06.19 -
[Flutter] 위젯 렌더링과 다트 문법
[Flutter] 위젯 렌더링과 다트 문법
2023.06.16 -
[Flutter] Dart 언어와 StatefulWidget
[Flutter] Dart 언어와 StatefulWidget
2023.06.14