Flutter云存储插件cloudkit_flutter的使用

Flutter云存储插件cloudkit_flutter的使用

CloudKit Flutter 插件

CloudKit 支持通过 CloudKit Web Services 在 Flutter 中使用。

支持

目前,该库仅支持 Android(iOS 虽然也支持,但其用处相当有限)。由于缺少 Flutter Web 支持,其中一个依赖项 webview_flutter 不支持 Flutter Web 平台。

设置

在您的应用中,设置此库涉及两个阶段。首先,您必须使用您的 CloudKit 容器、环境和 API 令牌初始化 API 管理器。其次,您必须根据 CloudKit 中的记录类型创建您的模型类。

API 初始化

在调用 CloudKit API 之前,必须向 CKAPIManager 提供三个值:

  • CloudKit 容器:CloudKit 使用的容器 ID,通常是 iCloud. 加上您的 bundle ID。
  • CloudKit API 令牌:必须通过 CloudKit 仪表板创建的令牌。重要的是,您必须选择最后一个选项(‘cloudkit-’ + 容器 ID + ‘://’)作为 ‘Sign in Callback’ 的 ‘URL Redirect’。自定义 URL 可以是任何短字符串,例如 ‘redirect’。
  • CloudKit 环境:更改使用的生产或开发环境。对应的值在 CKEnvironment 类中提供为常量。

要初始化管理器,必须将这三个值传递给 CKAPIManager.initManager(String container, String apiToken, CKEnvironment environment) async。建议与反射设置一起进行,如下面所述。

await CKAPIManager.initManager(ckContainer, ckAPIToken, ckEnvironment);

模型类 - 注解

在此库中,模型类必须注释并扫描,以便使用反射无缝地将 JSON CloudKit 记录转换为本地 Dart 对象。

在模型文件中使用的主要注释有以下几种:

  • @CKRecordTypeAnnotation:用于指定 CloudKit 上的记录类型名称,并放置在类声明之前。
  • @CKRecordNameAnnotation:用于标记本地类中存储 CloudKit 记录名称(UUID)的字段。
  • @CKFieldAnnotation:用于将本地 Dart 对象中的字段与 CloudKit 中的记录字段关联起来。

此外,为了让类可以通过反射扫描,您必须在类声明前添加 @reflector。

以下是这些注释在 Dart 文件中使用的示例:

import 'package:cloudkit_flutter/cloudkit_flutter_model.dart';

@reflector
@CKRecordTypeAnnotation("Schedule")  // The name of the CloudKit record type is included in the annotation
class Schedule
{
  @CKRecordNameAnnotation() // No CloudKit record field name is needed as the field is always 'recordName'
  String? uuid;
  
  @CKFieldAnnotation("scheduleCode") // The name of the CloudKit record field is included in the annotation
  String? code;
  
  @CKFieldAnnotation("periodTimes")
  List<String>? blockTimes;
  
  @CKFieldAnnotation("periodNumbers")
  List<int>? blockNumbers;
}

模型类 - 支持的字段类型

目前,大多数 CloudKit 支持的字段类型可以在本地模型类中使用。

许多是基本的:

  • String
  • int
  • double
  • DateTime
  • List<String>
  • List<int>

有一些需要一些解释:

  • CKReference / List<CKReference>: CloudKit 中的引用字段类型用于创建两个记录类型之间的关系。CKReference 类用于表示这种关系。要获取与引用关联的对象,只需调用 fetchFromCloud<T>() 函数,并在执行时提供相应的本地类型(代替 T)。
  • CKAsset: CloudKit 中的资产字段类型允许将数据作为独立的资产存储。一个常见的用途是存储图像。CKAsset 类用于表示这种类型,并且具有 fetchAsset() 函数来检索和缓存存储的字节。它还包括一个 getAsImage() 函数,如果可能的话,可以将缓存的字节转换为图像。
  • 子类 CKCustomFieldType: 详见下文。

更多基础字段类型将在后续版本中添加

模型类 - 自定义字段类型

有时,CloudKit 数据库中的字段只存储原始值,然后在到达应用程序时将其转换为枚举或其他更完整的类。为了允许在模型类中使用自定义类作为类型,创建了 CKCustomFieldType 类。

一个 CKCustomFieldType 子类有几个要求:

  • 类本身必须在类声明中提供原始值类型。
  • 必须有一个默认构造函数,调用 super.fromRecordField(T rawValue)。
  • 必须有一个 fromRecordField(T raw) 构造函数。
  • 类必须标记为 @reflector,类似于模型类。

以下是自定义字段类型类 Gender 的基本示例,它的原始值类型为 int:

import 'package:cloudkit_flutter/cloudkit_flutter_model.dart';

@reflector
class Gender extends CKCustomFieldType<int>
{
  // Static instances of Gender with a raw value and name
  static final female = Gender.withName(0, "Female");
  static final male = Gender.withName(1, "Male");
  static final other = Gender.withName(2, "Other");
  static final unknown = Gender.withName(3, "Unknown");
  static final genders = [female, male, other, unknown];
  
  String name;
  
  // Required constructors
  Gender() : name = unknown.name, super.fromRecordField(unknown.rawValue);
  Gender.fromRecordField(int raw) : name = genders[raw].name, super.fromRecordField(raw);
  
  // Used to create static instances above
  Gender.withName(int raw, String name) : name = name, super.fromRecordField(raw);
  
  // The default toString() for CKCustomFieldType outputs the rawValue, but here it makes more sense to output the name
  [@override](/user/override)
  String toString() => name;
}

模型类 - 反射设置

每当您更改模型类或 CKCustomFieldType 子类时,都必须重新生成对象代码,以允许库内部使用反射。首先,确保 build_runner 包已安装在您的应用的 pubspec 中,因为它需要运行以下命令。 接下来,通过运行 flutter pub run build_runner build lib 从您的 Flutter 项目的根目录生成对象代码。

在代码生成后,在您的应用启动时调用 initializeReflectable()(在生成的 *.reflectable.dart 文件中找到),在其他库调用之前。最后,您必须指示 CKRecordParser 类应该扫描哪些模型类。为此,调用 CKRecordParser.createRecordStructures(List<Type>) 函数,将本地模型类的直接名称列在列表中。例如,要扫描 Schedule 类,我们应调用 CKRecordParser.createRecordStructures([Schedule])。这最好与 API 初始化一起完成,如上所述。

使用

访问 CloudKit API 的主要方式是通过 CKOperation,并通过 execute() 函数运行。有多种操作,如下所述。

操作

创建所有操作时,都需要一个字符串参数来指定使用的数据库(公共、共享、私人)。可选地,可以传递特定的 CKAPIManager 实例,尽管默认使用共享实例。此外,操作还可以可选地接受一个 BuildContext,以防万一需要 iCloud 登录视图。

CKCurrentUserOperation

此操作获取当前用户的 CloudKit ID。这是测试用户是否已登录到 iCloud 的最简单方法,这是访问私有数据库所必需的。因此,可以在应用启动时或通过按钮调用此操作以启动 iCloud 登录提示。

除了上述操作的默认参数外,此操作不需要任何其他参数。

从 execute() 调用返回的是已登录用户的 CloudKit ID。

var getCurrentUserOperation = CKCurrentUserOperation(CKDatabase.PUBLIC_DATABASE, context: context);
var operationCallback = await getCurrentUserOperation.execute();

switch (operationCallback.state)
{
  case CKOperationState.success:
    var currentUserID = operationCallback.response as String;
    widget.callback(CKSignInState.IS_SIGNED_IN, currentUserID);
    break;

  case CKOperationState.authFailure:
    widget.callback(CKSignInState.NOT_SIGNED_IN, "Authentication failure");
    break;

  case CKOperationState.unknownError:
    widget.callback(CKSignInState.NOT_SIGNED_IN, "Unknown error");
    break;
}

CKRecordQueryOperation

此操作是检索 CloudKit 记录的主要方法。

创建操作时,必须传递一个本地类型供操作接收。例如:CKRecordQueryOperation<Schedule>(CKDatabase.PUBLIC_DATABASE) 将从公共数据库中获取所有 Schedule 记录。可选地,您可以传递特定的 CKZone (zoneID)、List<CKFilter> (filters) 或 List<CKSortDescriptor> (sortDescriptors) 来组织结果。您还可以传递一个布尔值 (preloadAssets) 来指示是否应在获取的记录中预加载任何 CKAsset 字段。

从 execute() 调用返回的是带有所提供类型的本地对象数组。

var queryPeopleOperation = CKRecordQueryOperation<UserSchedule>(CKDatabase.PRIVATE_DATABASE, preloadAssets: true, context: context);
CKOperationCallback queryCallback = await queryPeopleOperation.execute();

List<UserSchedule> userSchedules = [];
if (queryCallback.state == CKOperationState.success) userSchedules = queryCallback.response;

请求模型

除了多种操作之外,CloudKit 还在其 API 中提供了几个请求参数,由以下类表示。

CKFilter

过滤器是通过四个主要值创建的:要比较的 CloudKit 记录字段名称 (fieldName)、该记录字段的 CKFieldType (fieldType)、要比较的值 (fieldValue) 和所需的比较 CKComparator 对象。

CKSortDescriptor

排序描述符是通过两个主要值创建的:要按其排序的 CloudKit 记录字段名称 (fieldName) 和一个布尔值来指示方向 (ascending)。

CKZone

区对象目前只是包含一个区 ID 字符串 (zoneName) 的容器,并可用于指定特定的 CloudKit 区域。具有空区名的区对象将设置为默认区域。

CKQuery

查询对象是包含 CloudKit 记录类型 (recordType)、List<CKFilter> (filterBy) 和 List<CKSortDescriptor> (sortBy) 的容器。

CKRecordQueryRequest

记录查询请求对象代表执行 CKRecordQueryOperation 所需的信息,包括 CKZone (zoneID)、结果限制 (resultsLimit) 和 CKQuery 对象 (query)。

导入点

为了减少包含的类的数量,您可以选择导入库的一部分,如下面所述。

cloudkit_flutter.dart

包含所有公开的类。

cloudkit_flutter_init.dart

包含初始化 API 管理器 (CKAPIManager) 和记录解析器 (CKRecordParser) 所需的类。

cloudkit_flutter_model.dart

包含注释模型文件 (CKRecordTypeAnnotation、CKRecordNameAnnotation、CKFieldAnnotation)、使用特殊字段类型 (CKReference、CKAsset) 和创建自定义字段类型 (CKCustomFieldType) 所需的类。

cloudkit_flutter_api.dart

包含调用 CloudKit API (CKOperation + 子类、CKZone、CKFilter、CKSortDescriptor) 所需的类。

示例代码

以下是一个完整的示例,展示了如何使用 cloudkit_flutter 插件。

import 'package:flutter/material.dart';
import 'dart:developer';

import 'package:cloudkit_flutter/cloudkit_flutter_init.dart';
import 'package:cloudkit_flutter/cloudkit_flutter_api.dart';

import 'model/schedule.dart';
import 'model/week_schedule.dart';
import 'model/user_schedule.dart';

import 'main.reflectable.dart'; // Import generated code.
// Run `flutter pub run build_runner build example` from the root directory to generate example.reflectable.dart code

void main() async
{
  await initializeCloudKit();
  runApp(CKTestApp());
}

// To run this example code, you must have a CloudKit container with the following structure (as can be inferred from model/user_schedule.dart):
// UserSchedule: {
//   periodNames: List<String>
//   profileImage: CKAsset
//   genderRaw: int
// }
//
// Once the container is created, enter the CloudKit container and API token (set up via the CloudKit dashboard & with the options specified in README.md) below:

Future<void> initializeCloudKit() async
{
  const String ckContainer = ""; // YOUR CloudKit CONTAINER NAME HERE
  const String ckAPIToken = ""; // YOUR CloudKit API TOKEN HERE
  const CKEnvironment ckEnvironment = CKEnvironment.DEVELOPMENT_ENVIRONMENT;

  initializeReflectable();

  CKRecordParser.createRecordStructures([
    Schedule,
    WeekSchedule,
    UserSchedule
  ]);

  await CKAPIManager.initManager(ckContainer, ckAPIToken, ckEnvironment);
}

class CKTestApp extends StatelessWidget
{
  // This widget is the root of your application.
  [@override](/user/override)
  Widget build(BuildContext context)
  {
    return MaterialApp(
      title: 'iCloud Test',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: CKTestPage(title: "iCloud Test"),
    );
  }
}

class CKTestPage extends StatefulWidget
{
  CKTestPage({Key? key, required this.title}) : super(key: key);

  final String title;

  [@override](/user/override)
  _CKTestPageState createState() => _CKTestPageState();
}

class _CKTestPageState extends State<CKTestPage>
{
  CKSignInState isSignedIn = CKSignInState.NOT_SIGNED_IN;
  String currentUserOutput = "Get current user ID (and check if signed in)";
  String userScheduleOutput = "Fetch user schedule";

  void getCurrentUserCallback(CKSignInState isSignedIn, String currentUserOutput)
  {
    setState(() {
      this.isSignedIn = isSignedIn;
      this.currentUserOutput = currentUserOutput;
    });
  }

  void getUserScheduleCallback(String schedulesOutput)
  {
    setState(() {
      this.userScheduleOutput = schedulesOutput;
    });
  }

  [@override](/user/override)
  Widget build(BuildContext context)
  {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          children: [
            Text(currentUserOutput),
            CKSignInButton(isSignedIn: isSignedIn, callback: getCurrentUserCallback),
            Padding(padding: EdgeInsets.all(8.0)),
            Text(userScheduleOutput),
            FetchUserScheduleTestButton(isSignedIn: isSignedIn, callback: getUserScheduleCallback),
          ],
          mainAxisAlignment: MainAxisAlignment.center,
        ),
      ),
    );
  }
}

class CKSignInButton extends StatefulWidget
{
  final Function(CKSignInState, String) callback;
  final CKSignInState isSignedIn;

  CKSignInButton({Key? key, required this.isSignedIn, required this.callback}) : super(key: key);

  [@override](/user/override)
  State<StatefulWidget> createState() => CKSignInButtonState();
}

enum CKSignInState
{
  NOT_SIGNED_IN,
  SIGNING_IN,
  RE_SIGNING_IN,
  IS_SIGNED_IN
}

class CKSignInButtonState extends State<CKSignInButton>
{
  IconData getIconForCurrentState()
  {
    switch (widget.isSignedIn)
    {
      case CKSignInState.NOT_SIGNED_IN:
        return Icons.check_box_outline_blank;
      case CKSignInState.SIGNING_IN:
        return Icons.indeterminate_check_box_outlined;
      case CKSignInState.RE_SIGNING_IN:
        return Icons.indeterminate_check_box;
      case CKSignInState.IS_SIGNED_IN:
        return Icons.check_box;
    }
  }

  [@override](/user/override)
  Widget build(BuildContext context)
  {
    return ElevatedButton(
        onPressed: () async {
          if (widget.isSignedIn == CKSignInState.IS_SIGNED_IN)
          {
            widget.callback(CKSignInState.RE_SIGNING_IN, "Re-signing in...");
          }
          else
          {
            widget.callback(CKSignInState.SIGNING_IN, "Signing in...");
          }

          var getCurrentUserOperation = CKCurrentUserOperation(CKDatabase.PUBLIC_DATABASE, context: context);
          var operationCallback = await getCurrentUserOperation.execute();

          switch (operationCallback.state)
          {
            case CKOperationState.success:
              var currentUserID = operationCallback.response as String;
              widget.callback(CKSignInState.IS_SIGNED_IN, currentUserID);
              break;

            case CKOperationState.authFailure:
              widget.callback(CKSignInState.NOT_SIGNED_IN, "Authentication failure");
              break;

            case CKOperationState.unknownError:
              widget.callback(CKSignInState.NOT_SIGNED_IN, "Unknown error");
              break;
          }
        },
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          mainAxisSize: MainAxisSize.min,
          children: [
            Text("Sign In with iCloud"),
            Padding(padding: EdgeInsets.all(4.0)),
            Icon(getIconForCurrentState())
          ],
        )
    );
  }
}

class FetchUserScheduleTestButton extends StatefulWidget
{
  final Function(String) callback;
  final CKSignInState isSignedIn;

  FetchUserScheduleTestButton({Key? key, required this.isSignedIn, required this.callback}) : super(key: key);

  [@override](/user/override)
  State<StatefulWidget> createState() => FetchUserScheduleTestButtonState();
}

class FetchUserScheduleTestButtonState extends State<FetchUserScheduleTestButton>
{
  [@override](/user/override)
  Widget build(BuildContext context)
  {
    return ElevatedButton(
        onPressed: () async {
          if (widget.isSignedIn != CKSignInState.IS_SIGNED_IN)
          {
            widget.callback("Catch: Not signed in");
            return;
          }

          var queryPeopleOperation = CKRecordQueryOperation<UserSchedule>(CKDatabase.PRIVATE_DATABASE, preloadAssets: true, context: context);
          CKOperationCallback queryCallback = await queryPeopleOperation.execute();

          List<UserSchedule> userSchedules = [];
          if (queryCallback.state == CKOperationState.success) userSchedules = queryCallback.response;

          switch (queryCallback.state)
          {
            case CKOperationState.success:
              if (userSchedules.length > 0)
              {
                testUserSchedule(userSchedules[0]);
                widget.callback("Success");
              }
              else
              {
                widget.callback("No UserSchedule records");
              }
              break;

            case CKOperationState.authFailure:
              widget.callback("Authentication failure");
              break;

            case CKOperationState.unknownError:
              widget.callback("Unknown error");
              break;
          }
        },
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          mainAxisSize: MainAxisSize.min,
          children: [
            Text("Fetch UserSchedules"),
          ],
        )
    );
  }
}

void testUserSchedule(UserSchedule userSchedule) async
{
  log(userSchedule.toString());

  // These are the class names for each period in userSchedule, automatically converted from CloudKit to the local object
  var periodNames = userSchedule.periodNames ?? [];
  log(periodNames.toString());

  // This is the data for a profile image, which can be casted (via .getAsImage()) due to `preloadAssets: true` when the operation was called
  var _ = (userSchedule.profileImage?.getAsImage() ?? AssetImage("assets/generic-user.png")) as ImageProvider;
  // If `preloadAssets: false`, the asset would have to be downloaded directly:
  await userSchedule.profileImage?.fetchAsset();
  log(userSchedule.profileImage?.size.toString() ?? 0.toString());

  // This is a custom `Gender` object, converted from a raw int form in CloudKit
  var gender = userSchedule.gender ?? Gender.unknown;
  log(gender.toString());
}

更多关于Flutter云存储插件cloudkit_flutter的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter云存储插件cloudkit_flutter的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是如何在Flutter项目中使用cloudkit_flutter插件来实现云存储功能的代码示例。cloudkit_flutter是一个Flutter插件,允许你与Apple的CloudKit服务进行交互,以实现数据的云端存储和检索。

首先,你需要在你的Flutter项目中添加cloudkit_flutter依赖。打开你的pubspec.yaml文件,并添加以下依赖:

dependencies:
  flutter:
    sdk: flutter
  cloudkit_flutter: ^x.y.z  # 请替换为最新版本号

然后运行flutter pub get来安装依赖。

接下来,你需要配置CloudKit。这包括在Apple Developer网站上为你的应用创建一个CloudKit容器,并配置适当的权限和数据库架构。由于这部分涉及到Apple Developer账户和iCloud的设置,这里不详细展开,但你可以参考Apple的官方文档进行配置。

一旦CloudKit配置完成,你可以在Flutter代码中使用cloudkit_flutter插件。以下是一个简单的示例,展示了如何初始化CloudKit客户端、记录数据以及查询数据。

import 'package:flutter/material.dart';
import 'package:cloudkit_flutter/cloudkit_flutter.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late CloudKitClient cloudKitClient;

  @override
  void initState() {
    super.initState();
    // 初始化CloudKit客户端
    cloudKitClient = CloudKitClient(
      container: 'YourContainerIdentifier',  // 替换为你的CloudKit容器标识符
      appleId: 'YourAppleId',  // 可选,用于身份验证,如果不提供,则使用当前设备的Apple ID
    );
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('CloudKit Flutter Example'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              ElevatedButton(
                onPressed: () async {
                  // 记录数据
                  var record = CloudKitRecord(
                    recordType: 'MyRecordType',  // 替换为你的记录类型
                    recordId: CloudKitRecordId(recordName: 'myRecord'),
                    fields: {
                      'myField': CloudKitFieldValue.string('Hello, CloudKit!'),
                    },
                  );

                  var response = await cloudKitClient.saveRecord(record);
                  if (response.isSuccess) {
                    print('Record saved successfully');
                  } else {
                    print('Failed to save record: ${response.error?.localizedDescription}');
                  }
                },
                child: Text('Save Record'),
              ),
              ElevatedButton(
                onPressed: () async {
                  // 查询数据
                  var query = CloudKitQuery(
                    recordType: 'MyRecordType',
                    predicate: CloudKitPredicate.recordId(CloudKitRecordId(recordName: 'myRecord')),
                  );

                  var response = await cloudKitClient.performQuery(query);
                  if (response.isSuccess) {
                    var records = response.records ?? [];
                    if (records.isNotEmpty) {
                      var record = records.first;
                      var fieldValue = record.fields?['myField'] as CloudKitFieldValueString?;
                      print('Record retrieved: ${fieldValue?.value}');
                    } else {
                      print('No records found');
                    }
                  } else {
                    print('Failed to perform query: ${response.error?.localizedDescription}');
                  }
                },
                child: Text('Query Record'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

在这个示例中,我们创建了一个简单的Flutter应用,其中有两个按钮:一个用于保存记录到CloudKit,另一个用于查询记录。你需要替换YourContainerIdentifier为你的CloudKit容器标识符,以及根据你的记录类型和数据字段进行相应的调整。

请注意,这只是一个基本示例,实际应用中你可能需要处理更多的错误情况、权限管理以及更复杂的数据结构。此外,由于CloudKit是Apple的服务,因此这个插件和相关代码只能在iOS和macOS平台上运行。

回到顶部