---
title: Flutter 中解析数据类型的使用指南
slug: docs-ch/flutter/parse-sdk/data-objects/flutter-data-types
description: 全面解析在 Flutter 中使用 Parse 数据类型，涵盖数据读取与保存。
image: https://www.back4app.com/images/logo-social.png
createdAt: 2024-03-15T14:18:16.681Z
---

本指南介绍了Flutter中的Parse数据类型，使用来自`main.dart`文件的示例。Parse数据存储围绕ParseObject展开，它包含JSON兼容数据的键值对，使其无模式。这意味着您可以存储任何数据，而无需提前定义模式。

您可以设置任何您想要的键值对，我们的后端将存储它们。

例如，假设您正在跟踪游戏的高分。一个单一的Parse对象可以包含：

:::BlockQuote
score: 1337, playerName: "Sean Plott", cheatMode: false
:::

:::hint{type="info"}
我们建议您`NameYourClassesLikeThis`和`nameYourKeysLikeThis`以增强代码的可读性和可维护性。
:::

## 理解我们的应用程序

为了更好地理解Back4app，让我们探索Flutter应用程序中Parse操作的代码示例，以及主要支持的数据类型。本指南不会解释任何Flutter应用程序代码，因为主要关注点是将Parse与Flutter结合使用。

## 先决条件

要完成本教程，您需要：

:::hint{type="info"}
- [Android Studio ](https://developer.android.com/studio)或 [安装 VS Code](https://code.visualstudio.com/)（带有 [插件](https://docs.flutter.dev/get-started/editor) Dart 和 Flutter）
- 一个应用 [创建](https://www.back4app.com/docs/get-started/new-parse-app) 在 Back4app 上:
  - **注意:&#x20;**&#x8BF7;遵循 [新解析应用教程](https://www.back4app.com/docs/get-started/new-parse-app) 以了解如何在 Back4app 上创建解析应用。
- 一个连接到 Back4app 的 Flutter 应用。
  - **注意:&#x20;**&#x8BF7;遵循 [在 Flutter 项目中安装 Parse SDK](https://www.back4app.com/docs/flutter/parse-sdk/parse-flutter-sdk) 以创建一个连接到 Back4app 的 Flutter 项目。
- 一个运行 Android 或 iOS 的设备（或虚拟设备）。
:::

## 1 - 使用 Parse 对象

每个 ParseObject 都有一个类名（例如，GamePoint），用于区分不同的数据类型。以下是如何创建和保存一个新的 Parse 对象及其数据类型 `字符串`, `整数`, 和 `布尔值`:

```dart
final gamePoint = ParseObject('GamePoint')
  ..set('score', 1337)
  ..set('playerName', 'Sean Plott')
  ..set('cheatMode', false);
await gamePoint.save();
```

**查询新的 Parse 对象并检索数据类型：**

```dart
  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 - 计数器

您可以使用 `set()` 方法在 ParseObject 中递增或递减整数字段。

然而，这并不有效，如果多个客户端试图更新同一个计数器，可能会导致问题。Parse 提供了两种方法，可以自动递增和递减任何数字字段，以存储计数器类型的数据。

`setIncrement()`

`setDecrement()`

更新以递增一个计数器 `int` 值将被写为：

```dart
 final gamePoint = ParseObject('GamePoint')
  ..objectId = 'yourObjectId'
  ..setIncrement('intField', 1);
 await gamePoint.save();
```

要递减：

```dart
 final gamePoint = ParseObject('GamePoint')
  ..objectId = 'yourObjectId'
  ..setDecrement('intField', 1);
 await gamePoint.save();
```

使用 `setIncrement()` 和 `setDecrement()` 与 `save()` 调用允许您在更大的保存操作中更新一个值，在该操作中您可能会修改多个字段。这更好地避免了额外的网络请求。

## 3 - 列表

Parse 提供了处理列表数据的方法，包括 `setAdd`, `setAddUnique`, `setRemove`, 及其各自的 `All` 版本。

列表: `["a","b","c"]`

### 3.1 - setAdd

```dart
final gamePoint = ParseObject('GamePoint')
..objectId = 'yourObjectId'
..setAdd('listStringField', 'd');
await gamePoint.save();
```

结果: `["a","b","c","d"]`

### 3.2 - setAddAll

```dart
final gamePoint = ParseObject('GamePoint')
..objectId = 'yourObjectId'
..setAddAll('listStringField', ['e','f']);
await gamePoint.save();
```

结果: `["a","b","c","d","e","f"]`

`setAddAll` 不会添加重复元素，如果列表已经包含它们。

### 3.3 - setAddUnique

```dart
final gamePoint = ParseObject('GamePoint')
..objectId = 'yourObjectId'
..setAddUnique('listStringField', ['a', 'e', 'g']);
await gamePoint.save();
```

结果: `["a","b","c","d","e","f","g"]`

### 3.4 - setRemove

```dart
final gamePoint = ParseObject('GamePoint')
 ..objectId = 'yourObjectId'
 ..setRemove('listStringField', 'd');
 await gamePoint.save();
```

结果: `["a","b","c","e","f","g"]`

## 4 - 从 ParseObject 中移除字段

您可以通过使用 unset 操作从对象中删除单个字段：

```dart
final gamePoint = ParseObject('GamePoint')
..objectId = 'yourObjectId'
..unset("listStringField");
```

## 5 - 文件

ParseFile 让您在云中存储和检索应用程序文件。
下面是将本地图像文件上传到 Back4App 的基本示例：

```dart
// 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();
   }
```

此代码片段演示了如何检索图像文件：

```dart
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` 开始，存在可能导致上传文件时出现错误的重大更改。请遵循 [此指南](https://www.back4app.com/docs/platform/parse-server-version#O-CiL) 来修复您可能遇到的任何上传问题。

稍后的指南将进一步讨论并展示如何 [使用 Parse 保存和显示文件](https://www.back4app.com/docs/flutter/parse-sdk/flutter-save-file).

## 6 - 地理坐标点

Parse 允许您将现实世界的纬度和经度坐标与其 GeoPoint 数据类型的对象关联。将 ParseGeoPoint 添加到 ParseObject 将使查询能够考虑对象与参考点的接近程度。

示例:

```dart
 // 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();
```

您可以在这个 [main.dart](https://github.com/templates-back4app/Flutter_DataTypes/blob/18c4c372eb49c1cd950eeaac355564134a5e70c4/lib/src/main.dart) 文件中找到保存地理坐标点位置的独立示例。

## 完整应用示例

此示例演示如何：

- 创建、删除和更新一个 `GamePoint` Parse 对象。
- 处理各种数据类型，包括字符串、双精度浮点数、布尔值、文件、地理点、列表和日期。

在 iOS 上，您需要授予模拟器必要的访问权限。

:::hint{type="info"}
- iOS 提供了关于受保护资源的开发者指南。该指南中的示例应用程序将需要 [位置](https://developer.apple.com/documentation/bundleresources/information-property-list/nslocationwheninuseusagedescription) 和  [照片](https://developer.apple.com/documentation/bundleresources/information-property-list/nsphotolibraryaddusagedescription) 权限键才能正常工作。将这些键添加到 `Info.plist` 文件中，该文件位于 `/ios/Runner/Info.plist`:
:::

如果您在网页上运行 Flutter 应用程序，可以跳过上述步骤：

```dart
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后端检索的操作。
