Flutter를 사용한 Parse 데이터 핸들링 가이드
이 가이드는 Flutter에서 Parse 데이터 유형을 소개하며, main.dart 파일의 예제를 사용합니다. Parse 데이터 저장소는 ParseObject를 중심으로 하며, 이는 JSON 호환 데이터의 키-값 쌍을 포함하고 있어 스키마가 없습니다. 이는 사전 정의된 스키마 없이도 데이터를 저장할 수 있음을 의미합니다.
원하는 키-값 쌍을 설정할 수 있으며, 우리의 백엔드는 이를 저장합니다.
예를 들어, 게임의 최고 점수를 추적한다고 가정해 보겠습니다. 단일 Parse 객체는 다음을 포함할 수 있습니다:
점수: 1337, 플레이어 이름: "Sean Plott", 치트 모드: false
코드 가독성과 유지 관리를 향상시키기 위해 NameYourClassesLikeThis 및 nameYourKeysLikeThis를 사용하는 것을 권장합니다.
우리 앱 이해하기
Back4app을 더 잘 이해하기 위해, 주요 지원 데이터 유형과 함께 Flutter 애플리케이션에서 Parse 작업의 코드 예제를 살펴보겠습니다. 이 가이드는 Parse와 Flutter를 사용하는 데 초점을 맞추기 때문에 Flutter 앱 코드에 대한 설명은 포함되지 않습니다.
전제 조건
이 튜토리얼을 완료하려면 다음이 필요합니다:
- 백4앱에 연결된 플러터 앱.
- 안드로이드 또는 iOS를 실행하는 장치(또는 가상 장치).
1 - 파스 객체 작업하기
각 ParseObject는 서로 다른 데이터 유형을 구분하는 데 사용되는 클래스 이름(예: GamePoint)을 가지고 있습니다. 다음은 데이터 유형으로 새 Parse 객체를 생성하고 저장하는 방법입니다 문자열, 정수, & 부울:
final gamePoint = ParseObject('GamePoint')
..set('score', 1337)
..set('playerName', 'Sean Plott')
..set('cheatMode', false);
await gamePoint.save();새로운 Parse 객체를 쿼리하고 데이터 유형을 검색하려면:
final QueryBuilder<ParseObject> queryGamePoints = QueryBuilder<ParseObject>(ParseObject('GamePoint'));
final ParseResponse response = await queryGamePoints.query();
if (response.success && response.results != null) {
for (var gamePoint in response.results!) {
final score = gamePoint.get<int>('score');
final playerName = gamePoint.get<String>('playerName');
final cheatMode = gamePoint.get<bool>('cheatMode');
}2 - 카운터
ParseObject에서 정수 필드를 증가시키거나 감소시키려면 set() 메서드를 사용하세요.
하지만, 이것은 효과적이지 않으며 여러 클라이언트가 동일한 카운터를 업데이트하려고 할 경우 문제를 일으킬 수 있습니다. Parse는 카운터 유형 데이터를 저장하기 위해 자동으로 모든 숫자 필드를 증가 및 감소시키는 두 가지 방법을 제공합니다.
setIncrement()
setDecrement()
카운터 int 값을 증가시키는 업데이트는 다음과 같이 작성됩니다:
final gamePoint = ParseObject('GamePoint')
..objectId = 'yourObjectId'
..setIncrement('intField', 1);
await gamePoint.save();감소시키려면:
final gamePoint = ParseObject('GamePoint')
..objectId = 'yourObjectId'
..setDecrement('intField', 1);
await gamePoint.save();setIncrement() 및 setDecrement()를 save() 호출과 함께 사용하면 여러 필드를 수정할 수 있는 더 큰 저장 작업의 일환으로 값을 업데이트할 수 있습니다. 이는 추가 네트워크 요청을 피하는 데 더 좋습니다.
3 - 목록
Parse는 setAdd, setAddUnique, setRemove, 및 해당 모두 버전을 포함하여 목록 데이터 작업을 위한 메서드를 제공합니다.
목록: ["a","b","c"]
3.1 - setAdd
final gamePoint = ParseObject('GamePoint')
..objectId = 'yourObjectId'
..setAdd('listStringField', 'd');
await gamePoint.save();결과: ["a","b","c","d"]
3.2 - setAddAll
final gamePoint = ParseObject('GamePoint')
..objectId = 'yourObjectId'
..setAddAll('listStringField', ['e','f']);
await gamePoint.save();결과: ["a","b","c","d","e","f"]
setAddAll는 목록에 이미 포함된 중복 요소를 추가하지 않습니다.
3.3 - setAddUnique
final gamePoint = ParseObject('GamePoint')
..objectId = 'yourObjectId'
..setAddUnique('listStringField', ['a', 'e', 'g']);
await gamePoint.save();결과: ["a","b","c","d","e","f","g"]
3.4 - setRemove
final gamePoint = ParseObject('GamePoint')
..objectId = 'yourObjectId'
..setRemove('listStringField', 'd');
await gamePoint.save();결과: ["a","b","c","e","f","g"]
4 - ParseObject에서 필드 제거
객체에서 단일 필드를 삭제하려면 unset 작업을 사용하십시오:
final gamePoint = ParseObject('GamePoint')
..objectId = 'yourObjectId'
..unset("listStringField");5 - 파일
ParseFile을 사용하면 클라우드에서 애플리케이션 파일을 저장하고 검색할 수 있습니다. 아래는 로컬 이미지 파일을 Back4App에 업로드하는 기본 예제입니다:
// Create ParseFile object
final parseFile = ParseFile(File(image.path));
final response = await parseFile.save();
if (response.success) {
print('File uploaded successfully: ${parseFile.url}');
// Save this file in an object
final gamePoint = ParseObject('GamePoint')
..set('title', 'Post with an Image')
..set('imageFile', parseFile); // Saving the file as a field
final postResponse = await gamePoint.save();
}이 코드 조각은 이미지 파일을 검색하는 방법을 보여줍니다:
final query = QueryBuilder<ParseObject>(ParseObject('GamePoint'));
final response = await query.query();
if (response.success && response.results != null) {
for (var gamePoint in response.results!) {
ParseFileBase? varFile = gamePoint.get<ParseFileBase>('file');
if (varFile != null) {
final fileUrl = varFile.url;
print('Game Title: Image URL: $fileUrl');
} else {
print('No file found for this object.');
}
}
}Parse Server 5.2.3부터는 파일 업로드 시 오류를 유발할 수 있는 변경 사항이 있습니다. 이 가이드를 따라 업로드 문제를 해결하십시오.
나중에 가이드에서 Parse로 파일을 저장하고 표시하는 방법에 대해 더 논의하고 템플릿을 보여줄 것입니다.
6 - GeoPoint
Parse는 객체와 함께 실제 세계의 위도 및 경도 좌표를 GeoPoint 데이터 유형으로 연결할 수 있도록 합니다. ParseObject에 ParseGeoPoint를 추가하면 객체가 기준점에 얼마나 가까운지를 고려하는 쿼리를 가능하게 합니다.
예:
// Request permission here and get the current location: the `position` object.
// Create GeoPoint object
final geoPoint = ParseGeoPoint(latitude: position.latitude, longitude: position.longitude);
// Create an object with the GeoPoint
final gamePoint = ParseObject('GamePoint')
..set('name', 'Current Location')
..set('location', geoPoint); // Save the GeoPoint in a field
final response = await gamePoint.save();Geopoint 위치를 저장하는 독립 실행형 예제를 이 main.dart 파일에서 찾을 수 있습니다.
전체 앱 예제
이 예제는 다음을 보여줍니다:
- 생성, 삭제 및 업데이트 GamePoint Parse 객체.
- 문자열, 더블, 불리언, 파일, 지리적 포인트, 목록 및 날짜를 포함한 다양한 데이터 유형을 처리합니다.
iOS에서는 시뮬레이터에 필요한 접근 권한을 부여해야 합니다.
웹에서 Flutter 앱을 실행하는 경우 위 단계를 건너뛸 수 있습니다:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:parse_server_sdk_flutter/parse_server_sdk_flutter.dart';
import 'package:image_picker/image_picker.dart';
import 'package:flutter/foundation.dart';
import 'dart:io';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
const keyParseApplicationId = 'YOUR_APPLICATION_ID';
const keyParseClientKey = 'YOUR_CLIENT_KEY';
const keyParseServerUrl = 'https://parseapi.back4app.com';
await Parse().initialize(
keyParseApplicationId,
keyParseServerUrl,
clientKey: keyParseClientKey,
autoSendSessionId: true,
debug: true,
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'DataTypes',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final TextEditingController _stringController = TextEditingController();
final TextEditingController _doubleController = TextEditingController();
final TextEditingController _listStringController = TextEditingController();
final TextEditingController _listIntController = TextEditingController();
final TextEditingController _pointerController = TextEditingController();
final TextEditingController _objectIdController = TextEditingController();
final TextEditingController _uniqueValueController = TextEditingController();
final TextEditingController _removeValueController = TextEditingController();
bool _boolValue = false;
DateTime _selectedDate = DateTime.now();
final String _responseMessage = '';
XFile? pickedFile;
bool isLoading = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('DataTypes'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_buildTextField('String Field', _stringController),
const SizedBox(height: 20),
_buildTextField('Double Field', _doubleController,
isNumeric: true),
const SizedBox(height: 20),
_buildSwitch('Bool Field', _boolValue, (value) {
setState(() {
_boolValue = value;
});
}),
const SizedBox(height: 20),
TextButton(
onPressed: () => _selectDate(context),
child: Text(
'Select Date: ${_selectedDate.toLocal()}'.split(' ')[0]),
),
const SizedBox(height: 20),
_buildTextField(
'List String Field (comma-separated)', _listStringController),
const SizedBox(height: 20),
_buildTextField(
'List Int Field (comma-separated)', _listIntController),
const SizedBox(height: 20),
_buildTextField('Pointer Field (Object ID)', _pointerController),
const SizedBox(height: 20),
_buildImagePicker(),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _saveData,
child: const Text('Save'),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
await _deleteData(_objectIdController.text);
// await dummyFunction();
},
child: const Text('Delete'),
),
const SizedBox(height: 20),
_buildTextField(
'Object ID to Delete or Update', _objectIdController),
ElevatedButton(
onPressed: () async {
await _updateData(_objectIdController.text);
},
child: const Text('Update'),
),
const SizedBox(height: 20),
_buildIncrementDecrementButtons(),
const SizedBox(height: 20),
_buildListOperations(),
const SizedBox(height: 20),
Text(_responseMessage),
],
),
),
),
);
}
Widget _buildTextField(String label, TextEditingController controller,
{bool isNumeric = false}) {
return TextField(
controller: controller,
keyboardType: isNumeric ? TextInputType.number : TextInputType.text,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: label,
),
);
}
Widget _buildSwitch(String label, bool value, Function(bool) onChanged) {
return SwitchListTile(
title: Text(label),
value: value,
onChanged: onChanged,
);
}
Widget _buildImagePicker() {
return GestureDetector(
child: pickedFile != null
? Container(
width: 250,
height: 250,
decoration: BoxDecoration(border: Border.all(color: Colors.blue)),
child: kIsWeb
? Image.network(pickedFile!.path)
: Image.file(File(pickedFile!.path)),
)
: Container(
width: 250,
height: 250,
decoration: BoxDecoration(border: Border.all(color: Colors.blue)),
child: const Center(
child: Text('Click here to pick image from GamePoint')),
),
onTap: () async {
final picker = ImagePicker();
XFile? image = (await picker.pickImage(source: ImageSource.gallery));
if (image != null) {
setState(() {
pickedFile = image;
});
}
},
);
}
Widget _buildIncrementDecrementButtons() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: ElevatedButton(
onPressed: () async {
await _incrementField(_objectIdController.text);
},
child: const Text('+ Double'),
),
),
const SizedBox(width: 10), // Adding some space between buttons
Expanded(
child: ElevatedButton(
onPressed: () async {
await _decrementField(_objectIdController.text);
},
child: const Text('- Double'),
),
),
],
);
}
Widget _buildListOperations() {
return Column(
children: [
_buildTextField('Unique Value to Add', _uniqueValueController),
ElevatedButton(
onPressed: () async {
await _addUniqueToList(
_objectIdController.text, _uniqueValueController.text);
},
child: const Text('Add Unique'),
),
const SizedBox(height: 20),
_buildTextField('Value to Remove', _removeValueController),
ElevatedButton(
onPressed: () async {
await _removeFromList(
_objectIdController.text, _removeValueController.text);
},
child: const Text('Remove'),
),
],
);
}
Future<void> _saveData() async {
//Parse values
String stringValue = _stringController.text;
List<String> listStringValue = _listStringController.text.isNotEmpty
? _listStringController.text
.split(',') // Split by comma
.map((e) =>
e.trim()) // Remove any surrounding whitespace from each element
.toList()
: [];
List<int> listtIntValue = _listIntController.text.isNotEmpty
? _listIntController.text
.split(',') // Split by comma
.map(
(e) => int.parse(e.trim())) // Convert each string to an integer
.toList()
: [];
double? doubleValue;
if (_doubleController.text.isNotEmpty) {
doubleValue = double.parse(_doubleController.text);
}
bool boolValue = _boolValue;
DateTime selectedDate = _selectedDate;
final gamePoint = ParseObject('GamePoint')
..set('bool', boolValue)
..set('date', selectedDate);
if (stringValue.isNotEmpty) gamePoint.set('string', stringValue);
if (doubleValue != null) gamePoint.set('double', doubleValue);
if (listStringValue.isNotEmpty) {
gamePoint.setAddAll('listString', listStringValue);
}
if (listtIntValue.isNotEmpty) gamePoint.setAddAll('listint', listtIntValue);
if (_pointerController.text.isNotEmpty) {
gamePoint.set('pointer', _parsePointer(_pointerController.text));
}
if (pickedFile != null) {
setState(() {
isLoading = true;
});
ParseFileBase? parseFile;
if (kIsWeb) {
parseFile =
ParseWebFile(await pickedFile!.readAsBytes(), name: 'file.jpg');
} else {
parseFile = ParseFile(File(pickedFile!.path));
}
await parseFile.save();
gamePoint.set('file', parseFile);
}
var apiResponse = await gamePoint.save();
if (apiResponse.success && apiResponse.results != null) {
setState(() {
isLoading = false;
pickedFile = null;
});
ScaffoldMessenger.of(context)
..removeCurrentSnackBar()
..showSnackBar(const SnackBar(
content: Text(
'File saved successfully on Back4app',
style: TextStyle(color: Colors.white),
),
duration: Duration(seconds: 3),
backgroundColor: Colors.blue,
));
} else {
print("This is your request error: ${apiResponse.error}");
}
}
Future<void> _updateData(String objectId) async {
final gamePoint = ParseObject('GamePoint')..objectId = objectId;
// Update fields with new values
if (_stringController.text.isNotEmpty) {
gamePoint.set('string', _stringController.text);
}
if (_doubleController.text.isNotEmpty) {
gamePoint.set('double', double.parse(_doubleController.text));
}
gamePoint.set('bool', _boolValue);
gamePoint.set('date', _selectedDate);
if (_listStringController.text.isNotEmpty) {
List<String> listStringValue =
_listStringController.text.split(',').map((e) => e.trim()).toList();
gamePoint.setAddAll('listString', listStringValue);
}
if (_listIntController.text.isNotEmpty) {
List<int> listIntValue = _listIntController.text
.split(',')
.map((e) => int.parse(e.trim()))
.toList();
gamePoint.setAddUnique('listint', listIntValue);
}
if (_pointerController.text.isNotEmpty) {
gamePoint.set('pointer', _parsePointer(_pointerController.text));
}
if (pickedFile != null) {
ParseFileBase? parseFile;
if (kIsWeb) {
parseFile =
ParseWebFile(await pickedFile!.readAsBytes(), name: 'file.jpg');
} else {
parseFile = ParseFile(File(pickedFile!.path));
}
await parseFile.save();
gamePoint.set('file', parseFile);
}
// Save the updated object
var response = await gamePoint.save();
if (response.success) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Data updated successfully!')),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error updating data: ${response.error!.message}')),
);
}
}
Future<void> _incrementField(String objectId) async {
final gamePoint = ParseObject('GamePoint')
..objectId = objectId
..setIncrement('double', 1);
await gamePoint.save();
}
Future<void> _decrementField(String objectId) async {
final gamePoint = ParseObject('GamePoint')
..objectId = objectId
..setDecrement('double', -1);
await gamePoint.save();
}
Future<void> _addUniqueToList(String objectId, String value) async {
final gamePoint = ParseObject('GamePoint')
..objectId = objectId
..setAddUnique('listString', value);
await gamePoint.save();
}
Future<void> _removeFromList(String objectId, String value) async {
final gamePoint = ParseObject('GamePoint')
..objectId = objectId
..setRemove('listString', value);
await gamePoint.save();
}
Future<void> _selectDate(BuildContext context) async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _selectedDate,
firstDate: DateTime(2000),
lastDate: DateTime(2101),
);
if (picked != null && picked != _selectedDate) {
setState(() {
_selectedDate = picked;
});
}
}
Future<void> _deleteData(String objectId) async {
// Delete data logic
final parseObject = ParseObject('GamePoint')
..objectId = objectId
..unset("listString");
await parseObject.save();
}
// Convert Pointer field to a suitable format
ParseObject _parsePointer(String objectId) {
// There should be an existing Game class with objects
final pointer = ParseObject('Game')..objectId = objectId;
return pointer;
}
}이 코드는 Flutter에서 Parse SDK를 초기화하고, 주요 애플리케이션을 설정하며, 제목이 있는 간단한 홈 페이지를 표시합니다.
결론
이 가이드에서는 ParseObjects와 Parse에서 사용할 수 있는 다양한 데이터 유형에 대해 배웠습니다. 또한 back4app 백엔드에서 데이터 유형을 저장하고 검색하는 작업을 처리하는 방법도 배웠습니다.