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
更多关于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平台上运行。