Form(
key: _formKey,
child: Column(
children: [
TextFormField(
maxLength: 50, // 최대길이
decoration: const InputDecoration(
label: Text('Name'),
),
validator: (value) {
if (value == null ||
value.isEmpty ||
value.trim().length <= 1 ||
value.trim().length > 50) {
return 'Must be between 1 and 50 characters';
}
return null;
},
onSaved: (value) {
_enteredName = value!;
},
),
플러터에서 기본적으로 입력을 받으려면, TextEditingController 를 사용해야함.
다만 문제가 되는 것은 입력이 여러개일 경우, 컨트롤러 관리가 상당히 곤란하기때문에,
Form 위젯을 사용하여 여러개의 입력을 관리하는 것이 요구됨
key 파라미터GlobalKey : 이 key를 통해서 들어올 입력 값들을 구별하고 유효성 검사시 사용됨child 파라미터TextFormField 위치사용자의 입력은 항상 개발자의 의도대로 들어오는 것이 아니기 때문에, 사용자의 입력이 요구되는 것들을 다 충족시켰는지 걸러주는 유효성 검사가 반드시 필요함.
Flutter 공식문서 유효성검사 : https://docs.flutter.dev/cookbook/forms/validation
validator: (value) {
if (value == null ||
value.isEmpty ||
value.trim().length <= 1 ||
value.trim().length > 50) {
return 'Must be between 1 and 50 characters';
}
return null;
강의에선 이 validator 옵션을 통해서,null, isEmpty(""),1자 미만 혹은 50자 초과하는 입력은
입력조건을 알려주는 메시지를 반환하는 기능을 구현한다.
onSaved: (value) {
_enteredName = value!;
},
onsaved 옵션을 통해서 사용자로 부터 받은 입력값(value)를 _enteredName이라는 변수에 담는다.
유효성 검사를 통과한 경우(true) onSaved 옵션에 따라 변수에 값을 저장하게 된다.
validator: (value) {
if (value == null ||
value.isEmpty ||
int.tryParse(value) == null ||
int.tryParse(value)! <= 0) {
return 'Must be a valid, positive Numbers';
}
return null;
},
onSaved: (value) {
_enteredQuantity = int.parse(value!);
},
위와 마찬가지로, Quantity를 입력받을때 유효성검사와 onsaved를 통해서 문자열로 입력되는 입력값을 정수형으로 바꾸어 변수에 저장하는 기능을 담당한다.
다트에선 아주 친절하게도 http 관련 기능들을 번들화한 패키지가 존재한다. 이를 활용하여 더 편리한 데이터 통신이 가능하다.
void _saveItem() async {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
final url = Uri.https(
'flutter-prep-16168-default-rtdb.asia-southeast1.firebasedatabase.app',
'shopping-list.json');
await http.post(
url,
headers: {
'Content-Type': 'application/json',
},
body: json.encode(
{
'name': _enteredName,
'quantity': _enteredQuantity,
'category': _selectedCategory.title,
},
),
);
// Navigator.of(context).pop();
}
}
Firebase의 API Docs 에따라서 url 을 설정하고, Uri constructor를 활용하여 문자열을 Uri로 파싱
async/await을 활용하여 비동기 처리.
그리고 Post는 Future타입의 인스턴스를 생성함
if (!context.mounted) {
return; // 더이상 진행 X, 본 함수를 종료, pop 함수 진행 안함
}
플러터에서 StatefulWidget을 사용시 화면을 갱신하기위해 Setstate를 사용하는데,
가끔 위젯이 마운트 되지 않은 상태에서 setState함수를 호출하게 되면 문제가 발생한다고 한다.
(플러터 공식문서에는 It is an error to call setState unless mounted is true. 이렇게 쓰여있다.)
따라서 mount 상태를 확인하는 단계가 필요할때 사용하는 옵션.
Future<List<GroceryItem>> _loadItems() async {
final url = Uri.https(
// 'flutter-prep-16169-default-rtdb.asia-southeast1.firebasedatabase.app',
'flutter-prep-16168-default-rtdb.asia-southeast1.firebasedatabase.app',
'shopping-list.json');
final response = await http.get(url);
if (response.statusCode >= 400) {
throw Exception('Faild to fetch grocery items');
}
if (response.body == 'null') {
return [];
}
final Map<String, dynamic> listdata = json.decode(response.body);
final List<GroceryItem> loadedItems = [];
for (final item in listdata.entries) {
final category = categories.entries
.firstWhere(
(catItem) => catItem.value.title == item.value['category'])
.value;
loadedItems.add(
GroceryItem(
id: item.key,
name: item.value['name'],
quantity: item.value['quantity'],
category: category,
),
);
}
return loadedItems;
}
Error Check
만약 url이 잘못되는등의 문제가 발생했을때, StatusCode는 보통 4,500번대이기 때문에 StatusCode가 400 이상이라면 에러상황이라고 판단, Exception 클라스를 통해서 에러처리를 하여, 이후 함수들이 실행되지 않도록 하였다.
카테고리 가져오기
카테고리 같은 경우 categories 인스턴스를 entries 옵션을 통해 리스트화 시키고,
이중 firstwhere를 통해 첫번째 요소가 백엔드로부터 가져온 데이터의 Categroy 부분과 같으면, 이 데이터를 가져옴으로써 카테고리 가져오기 기능을 구현하였다.
Categories = {
Categories.vegetables: Category(
'Vegetables',
Color.fromARGB(255, 0, 255, 128),
),
Categories.fruit: Category(
'Fruit',
Color.fromARGB(255, 145, 255, 0),
),
void _removeItem(GroceryItem item) async {
final index = _groceryItems.indexOf(item);
final url = Uri.https(
'flutter-prep-16168-default-rtdb.asia-southeast1.firebasedatabase.app',
'shopping-list/${item.id}.json');
final response = await http.delete(url);
if (response.statusCode >= 400) {
setState(() {
_groceryItems.insert(index, item);
});
}
}
삭제시엔 특정 item을 삭제하는 것이기 때문에, uri 클래스 활용시, id를 파라미터로 달아주어 원하는 아이템을 삭제시키는 것이 필요하다.
if (response.statusCode >= 400) {
setState(() {
_error = 'Failed to fetch data. Please Try again Later';
});
}
if (response.body == 'null') {
setState(() {
_isLoading = false;
});
return;
}
// 추후 변경
if (response.body == 'null') {
return [];
}
데이터를 불러올때, 아예 아무런 데이터가 없는 상황에선 계속 값을 불러온다고 생각하여 에러가 발생할 수 있는데,
_isLoading 변수를 만들어서, Nodata 상황일 경우 false로 설정하여 값이 없음을 명확하게 하였다.
그 다음, 리스트에 데이터를 담아서 가져오기 때문에 빈 리스트를 활용하는 방식으로 변경하였다.
final response = await http.get(url);
if (response.statusCode >= 400) {
throw Exception('Faild to fetch grocery items');
// 이후 밑에 있는 코드들은 진행 금지
}
함수 수행 도중 의도치 않은 상황(404...) throw로 Execption 클래스를 호출, 에러를 보여주도록 하였다.
다만, 이를 자주 활용하는 것은 지양한다.
try {
final response = await http.get(url);
if (response.statusCode >= 400) {
setState(() {
_error = 'Failed to fetch data. Please Try again Later';
});
}
if (response.body == 'null') {
setState(() {
_isLoading = false;
});
return;
}
final Map<String, dynamic> listdata = json.decode(response.body);
final List<GroceryItem> loadedItems = [];
for (final item in listdata.entries) {
final category = categories.entries
.firstWhere(
(catItem) => catItem.value.title == item.value['category'])
.value;
loadedItems.add(
GroceryItem(
id: item.key,
name: item.value['name'],
quantity: item.value['quantity'],
category: category,
),
);
}
setState(() {
_groceryItems = loadedItems;
_isLoading = false;
});
} catch (error) {
setState(() {
_error = 'Something went wrong. Please Try again Later';
});
}
}
try는 문제 가 없을 경우, catch는 에러가 발생한 경우로 나누어서 코드가 진행된다.
Exception 이 발생해도 단어의 뜻처럼 결국 실행된다.
try {
breedMoreLlamas();
} catch (e) {
print('Error: $e'); // Handle the exception first.
} finally {
cleanLlamaStalls(); // Then clean up.
}
https://velog.io/@adbr/Dart-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC-try-on-vs-try-catch
https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
body: FutureBuilder(
future: _loadedItems,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const enter(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return enter(
child: Text(
snapshot.error.toString(),
));
}
if (snapshot.data!.isEmpty) {
return const enter(child: Text('No items added yet'));
}
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (ctx, index) => Dismissible(
background: Container(color: Colors.red),
onDismissed: (direction) {
_removeItem(snapshot.data![index]);
},
key: ValueKey(snapshot.data![index].id),
child: ListTile(
title: Text(snapshot.data![index].name),
leading: Container(
width: 24,
height: 24,
color: snapshot.data![index].category.color,
),
trailing: Text(
snapshot.data![index].quantity.toString(),
), //표시기 출력목적
),
),
);
},
),
Future를 사용하는 이유는 데이터를 다 받기 전에 데이터 없이 먼저 UI를 그려낼 수 없기 때문에, 데이터 없이 그릴 수 없는 부분을 그리기 위함.
FutureBuilder로 데이터를 가져올 동안 보여줄 UI를 관리하고, 이용 가능하게 되면 보여줄 위젯을 관리하는 것이 가능함.
=> 최적화 목적
> 💡 FutureBuilder 안에 Future데이터 정의는 절대 금물!
FutureBuilder의 상위요소가 데이터를 불러올때마다 이를 다시 빌드 하기때문, 이를 해결하기 위헤선 initState에서 얻어와서 빌더에 전달하면 됨.
ConnectionState.wating/done 상태를 활용할 수있음.본 강의에선 waiting 일경우 LoadingSpinner를, done일 경우 데이터 출력을 하도록 구현