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

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

cloudkit_flutter_api.dart

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

示例代码

以下是一个完整的示例,展示了如何使用 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平台上运行。

回到顶部