Flutter云存储与媒体处理插件cloudinary_sdk的使用
Flutter云存储与媒体处理插件cloudinary_sdk的使用
Description
此包用于通过Cloudinary API进行签名和非签名上传。它允许你销毁/删除文件,并轻松访问带变换的图像URL。
Installation
首先,在你的项目中添加cloudinary_sdk
作为依赖项。你可以使用以下命令:
对于纯Dart项目
dart pub add cloudinary_sdk
对于Flutter项目
flutter pub add cloudinary_sdk
这将把cloudinary_sdk
添加到项目的pubspec.yaml
中。最后运行:
dart pub get 或 flutter pub get
这将下载依赖项到你的pub-cache
。
如何使用
初始化一个Cloudinary对象
/// 这三个参数可以直接从你的Cloudinary账户仪表板获取。
/// .full(...)工厂构造器仅适用于服务器端应用,其中[apiKey]和[apiSecret]是安全的。
final cloudinary = Cloudinary.full(
apiKey: apiKey,
apiSecret: apiSecret,
cloudName: cloudName,
);
/// .basic(...)工厂构造器适用于客户端应用,其中[apiKey]和[apiSecret]不应被使用。
/// .basic(...)构造器允许稍后进行无签名请求。
final cloudinary = Cloudinary.basic(
cloudName: cloudName,
);
执行单个文件签名上传
推荐仅用于服务器端应用。
final response = await cloudinary.uploadResource(
CloudinaryUploadResource(
filePath: file.path,
fileBytes: file.readAsBytesSync(),
resourceType: CloudinaryResourceType.image,
folder: cloudinaryCustomFolder,
fileName: 'some-name',
progressCallback: (count, total) {
print('Uploading image from file with progress: $count/$total');
},
),
);
if(response.isSuccessful) {
print('Get your image from with ${response.secureUrl}');
}
执行多个文件签名上传
推荐仅用于服务器端应用。
final resources = await Future.wait(files?.map((file) async =>
CloudinaryUploadResource(
filePath: file.path,
fileBytes: file.readAsBytesSync(),
resourceType: CloudinaryResourceType.image,
folder: cloudinaryCustomFolder,
progressCallback: (count, total) {
print('Uploading image from file with progress: $count/$total');
},
),
)));
List<CloudinaryResponse> responses = await cloudinary.uploadResources(resources);
responses.forEach((response) {
if(response.isSuccessful) {
print('Get your image from with ${response.secureUrl}');
}
});
执行单个文件无签名上传
推荐用于客户端应用。
final response = await cloudinary.unsignedUploadResource(
CloudinaryUploadResource(
uploadPreset: somePreset,
filePath: file.path,
fileBytes: file.readAsBytesSync(),
resourceType: CloudinaryResourceType.image,
folder: cloudinaryCustomFolder,
fileName: 'some-name',
progressCallback: (count, total) {
print('Uploading image from file with progress: $count/$total');
},
),
);
if(response.isSuccessful) {
print('Get your image from with ${response.secureUrl}');
}
执行多个文件无签名上传
推荐用于客户端应用。
final resources = await Future.wait(files?.map((file) async =>
CloudinaryUploadResource(
uploadPreset: somePreset,
filePath: file.path,
fileBytes: file.readAsBytesSync(),
resourceType: CloudinaryResourceType.image,
folder: cloudinaryCustomFolder,
progressCallback: (count, total) {
print('Uploading image from file with progress: $count/$total');
},
),
)));
List<CloudinaryResponse> responses = await cloudinary.uploadResources(resources);
responses.forEach((response) {
if(response.isSuccessful) {
print('Get your image from with ${response.secureUrl}');
}
});
删除单个文件(使用Cloudinary的destroy方法)
final response = await cloudinary.deleteResource(
url: url,
resourceType: CloudinaryResourceType.image,
invalidate: false,
);
if(response.isSuccessful ?? false) {
// Do something else
}
删除多个文件(使用Cloudinary的delete resources方法)
final response = await cloudinary.deleteResources(
urls: urlPhotos,
resourceType: CloudinaryResourceType.image,
);
if(response.isSuccessful ?? false) {
Map<String, dynamic> deleted = response.deleted; // 在deleted Map中可以找到所有public ids和状态'deleted'
}
加载带有某些变换的Cloudinary图像
final cloudinaryImage = CloudinaryImage(url);
String transformedUrl = cloudinaryImage.transform().width(256).height(256).thumb().face().opacity(30).angle(45).generate();
return Image.network(transformedUrl);
注意事项
建议查看测试和示例代码以更好地了解如何使用此包。
以下是完整的示例代码:
import 'dart:async';
import 'dart:io';
import 'package:cloudinary_sdk/cloudinary_sdk.dart';
import 'package:cloudinary_sdk_example/alert_utils.dart';
import 'package:flutter/material.dart';
import 'package:cloudinary_sdk_example/image_utils.dart';
import 'package:image_picker/image_picker.dart';
import 'package:dio/dio.dart';
const String apiUrl = String.fromEnvironment('CLOUDINARY_API_URL', defaultValue: 'https://api.cloudinary.com/v1_1');
const String apiKey = String.fromEnvironment('CLOUDINARY_API_KEY', defaultValue: '');
const String apiSecret = String.fromEnvironment('CLOUDINARY_API_SECRET', defaultValue: '');
const String cloudName = String.fromEnvironment('CLOUDINARY_CLOUD_NAME', defaultValue: '');
const String folder = String.fromEnvironment('CLOUDINARY_FOLDER', defaultValue: 'test/my-folder');
const String uploadPreset = String.fromEnvironment('CLOUDINARY_UPLOAD_PRESET', defaultValue: '');
final cloudinary = Cloudinary.full(
apiUrl: apiUrl, apiKey: apiKey, apiSecret: apiSecret, cloudName: cloudName);
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
[@override](/user/override)
Widget build(BuildContext context) {
return MaterialApp(
title: 'Cloudinary Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const MyHomePage(title: 'Cloudinary Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, this.title}) : super(key: key);
final String? title;
[@override](/user/override)
_MyHomePageState createState() => _MyHomePageState();
}
enum UploadMode {
single,
multiple,
}
enum FileSource {
path,
bytes,
}
enum DeleteMode {
batch,
iterative,
}
class DataTransmitNotifier {
final String path;
late final ProgressCallback? progressCallback;
final notifier = ValueNotifier<double>(0);
DataTransmitNotifier(
{required this.path, ProgressCallback? progressCallback}) {
this.progressCallback = progressCallback ??
(count, total) {
notifier.value = count.toDouble() / total.toDouble();
};
}
}
class _MyHomePageState extends State<MyHomePage> {
static const int loadImage = 1;
static const int doSignedUpload = 2;
static const int doUnsignedUpload = 3;
static const int deleteUploadedData = 4;
List<DataTransmitNotifier> dataImages = [];
List<CloudinaryResponse> cloudinaryResponses = [];
bool loading = false;
String? errorMessage;
UploadMode uploadMode = UploadMode.single;
FileSource fileSource = FileSource.path;
DeleteMode deleteMode = DeleteMode.batch;
[@override](/user/override)
void initState() {
super.initState();
}
void onUploadModeChanged(UploadMode? value) => setState(() => uploadMode = value!);
void onUploadSourceChanged(FileSource? value) => setState(() => fileSource = value!);
void onDeleteModeChanged(DeleteMode? value) => setState(() => deleteMode = value!);
Widget get uploadModeView => Column(
children: [
const Text("Upload mode"),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: RadioListTile<UploadMode>(
title: const Text("Single"),
value: UploadMode.single,
groupValue: uploadMode,
onChanged: onUploadModeChanged),
),
Expanded(
child: RadioListTile<UploadMode>(
title: const Text("Multiple"),
value: UploadMode.multiple,
groupValue: uploadMode,
onChanged: onUploadModeChanged),
),
],
)
],
);
Widget get uploadSourceView => Column(
children: [
const Text("File source"),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: RadioListTile<FileSource>(
title: const Text("Path"),
value: FileSource.path,
groupValue: fileSource,
onChanged: onUploadSourceChanged),
),
Expanded(
child: RadioListTile<FileSource>(
title: const Text("Bytes"),
value: FileSource.bytes,
groupValue: fileSource,
onChanged: onUploadSourceChanged),
),
],
)
],
);
Widget get deleteModeView => Column(
children: [
const Text("Delete mode"),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: RadioListTile<DeleteMode>(
title: const Text("Batch"),
value: DeleteMode.batch,
groupValue: deleteMode,
onChanged: onDeleteModeChanged),
),
Expanded(
child: RadioListTile<DeleteMode>(
title: const Text("Iterative"),
value: DeleteMode.iterative,
groupValue: deleteMode,
onChanged: onDeleteModeChanged),
),
],
)
],
);
Widget imageFromPathView(DataTransmitNotifier data) {
return SizedBox(
width: 100,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.file(
File(data.path),
width: 100,
height: 100,
),
ValueListenableBuilder<double>(
key: ValueKey(data.path),
valueListenable: data.notifier,
builder: (context, value, child) {
if (value == 0 && !loading) return const SizedBox();
return Column(
mainAxisSize: MainAxisSize.min,
children: [
LinearProgressIndicator(
value: value,
),
Text('${(value * 100).toInt()} %'),
],
);
},
),
],
),
);
}
Widget imageFromUrlView(CloudinaryResponse resource) {
final image = resource.cloudinaryImage;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.network(
image!.transform().width(256).thumb().generate()!,
width: 100,
height: 100,
),
SizedBox(
width: 100,
child: Text(
resource.originalFilename ??
resource.publicId ??
resource.secureUrl ??
'Unknown',
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
],
);
}
Widget imageGalleryView({
bool fromPath = true,
}) {
List imagesSource = fromPath ? dataImages : cloudinaryResponses;
final imageViews = List.generate(imagesSource.length, (index) {
final source = imagesSource[index];
return fromPath ? imageFromPathView(source) : imageFromUrlView(source);
});
if (loading && !fromPath) {
imageViews.add(const Center(
child: CircularProgressIndicator(),
));
}
return Wrap(
alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
runAlignment: WrapAlignment.center,
spacing: 8,
runSpacing: 8,
children: imageViews,
);
}
[@override](/user/override)
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title!),
),
body: Center(
child: Scrollbar(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const SizedBox(height: 16),
const Text(
'Photos from file',
),
const SizedBox(
height: 16,
),
imageGalleryView(fromPath: true),
ElevatedButton(
onPressed: loading || dataImages.isEmpty
? null
: () {
dataImages = [];
setState(() {});
},
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
return states.contains(MaterialState.disabled)
? null
: Colors.deepPurple;
}),
),
child: const Text(
'Clear list',
textAlign: TextAlign.center,
),
),
const Divider(
height: 48,
),
const Text(
'Photos from cloudinary',
),
const SizedBox(
height: 16,
),
imageGalleryView(fromPath: false),
const SizedBox(
height: 32,
),
Visibility(
visible: errorMessage?.isNotEmpty ?? false,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"$errorMessage",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18, color: Colors.red.shade900),
),
const SizedBox(
height: 128,
),
],
)),
ElevatedButton(
onPressed: loading || cloudinaryResponses.isEmpty
? null
: () {
cloudinaryResponses = [];
setState(() {});
},
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
return states.contains(MaterialState.disabled)
? null
: Colors.purple;
}),
),
child: const Text(
'Clear list',
textAlign: TextAlign.center,
),
),
const SizedBox(
height: 32,
),
uploadModeView,
const SizedBox(
height: 16,
),
uploadSourceView,
const SizedBox(
height: 16,
),
deleteModeView,
const SizedBox(
height: 32,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: loading ||
(dataImages.isEmpty && cloudinaryResponses.isEmpty)
? null
: () {
dataImages = [];
cloudinaryResponses = [];
setState(() {});
},
child: const Text(
'Clear all',
textAlign: TextAlign.center,
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: loading || dataImages.isEmpty
? null
: () => onClick(doSignedUpload),
style: ButtonStyle(
padding:
MaterialStateProperty.all(const EdgeInsets.all(8)),
backgroundColor:
MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
return states.contains(MaterialState.disabled)
? null
: Colors.orange;
}),
),
child: const Column(
children: [
Text(
'Signed upload',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(height: 4),
Text(
'Signed uploads are recommended only for server side, because it requires an api key and api secret to be able to upload images to Cloudinary. For uploading images from client side like mobile or web app consider "Unsigned upload"',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12, fontWeight: FontWeight.normal),
),
],
),
),
),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: loading || dataImages.isEmpty
? null
: () => onClick(doUnsignedUpload),
style: ButtonStyle(
padding:
MaterialStateProperty.all(const EdgeInsets.all(8)),
backgroundColor:
MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
return states.contains(MaterialState.disabled)
? null
: Colors.deepOrange;
}),
),
child: const Column(
children: [
Text(
'Unsigned upload',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(height: 4),
Text(
'Unsigned uploads are recommended from client side like mobile or web app. This upload doesn\'t require an api key or api secret to upload to Cloudinary.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12, fontWeight: FontWeight.normal),
),
],
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: loading || cloudinaryResponses.isEmpty
? null
: () => onClick(deleteUploadedData),
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
return states.contains(MaterialState.disabled)
? null
: Colors.red.shade600;
}),
),
child: const Text(
'Delete uploaded images',
textAlign: TextAlign.center,
),
),
),
],
),
),
SizedBox(
height: MediaQuery.of(context).padding.bottom,
),
],
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => onClick(loadImage),
tooltip: 'Choose photo',
child: const Icon(Icons.photo),
),
);
}
void onNewImages(List<String> filePaths) {
if (filePaths.isNotEmpty) {
for (final path in filePaths) {
if (path.isNotEmpty) {
dataImages.add(DataTransmitNotifier(path: path));
}
}
setState(() {});
}
}
Future<List<int>> getFileBytes(String path) async {
return await File(path).readAsBytes();
}
Future<void> doSingleUpload({bool signed = true}) async {
try {
final data = dataImages.first;
List<int>? fileBytes;
if (fileSource == FileSource.bytes) {
fileBytes = await getFileBytes(data.path);
}
final resource = CloudinaryUploadResource(
filePath: data.path,
fileBytes: fileBytes,
resourceType: CloudinaryResourceType.image,
folder: folder,
fileName: 'single-${DateTime.now().millisecondsSinceEpoch}',
progressCallback: data.progressCallback,
uploadPreset: uploadPreset,
);
CloudinaryResponse response = signed
? await cloudinary.uploadResource(resource)
: await cloudinary.unsignedUploadResource(resource);
if (response.isSuccessful && response.secureUrl!.isNotEmpty) {
cloudinaryResponses.add(response);
} else {
errorMessage = response.error;
}
} catch (e) {
errorMessage = e.toString();
print(e);
}
}
Future<void> doMultipleUpload({bool signed = true}) async {
try {
List<CloudinaryUploadResource> resources = await Future.wait(
dataImages.map((data) async => CloudinaryUploadResource(
filePath: fileSource == FileSource.path ? data.path : null,
fileBytes: fileSource == FileSource.bytes
? await getFileBytes(data.path)
: null,
resourceType: CloudinaryResourceType.image,
folder: folder,
progressCallback: data.progressCallback,
uploadPreset: uploadPreset,
)));
List<CloudinaryResponse> responses = signed
? await cloudinary.uploadResources(resources)
: await cloudinary.unsignedUploadResources(resources);
for (var response in responses) {
if (response.isSuccessful) {
cloudinaryResponses.add(response);
} else {
errorMessage = response.error;
}
}
} catch (e) {
errorMessage = e.toString();
print(e);
}
}
Future<void> upload({bool signed = true}) async {
showLoading();
switch (uploadMode) {
case UploadMode.multiple:
return doMultipleUpload(signed: signed);
case UploadMode.single:
return doSingleUpload(signed: signed);
default:
}
}
Future<void> doBatchDelete() async {
CloudinaryResponse response = await cloudinary.deleteResources(
urls: cloudinaryResponses.map((e) => e.secureUrl!).toList(),
resourceType: CloudinaryResourceType.image);
if (response.isSuccessful) {
cloudinaryResponses = [];
// Check for deleted status...
// Map<String, dynamic> deleted = response.deleted;
} else {
errorMessage = response.error;
}
}
Future<void> doIterativeDelete() async {
for (int i = 0; i < cloudinaryResponses.length; i++) {
String url = cloudinaryResponses[i].secureUrl!;
final response = await cloudinary.deleteResource(
url: url,
resourceType: CloudinaryResourceType.image,
invalidate: false,
);
if (response.isSuccessful) {
cloudinaryResponses.removeWhere((element) => element.secureUrl == url);
--i;
}
}
}
Future<void> delete() async {
showLoading();
switch (deleteMode) {
case DeleteMode.batch:
return doBatchDelete();
case DeleteMode.iterative:
return doIterativeDelete();
default:
}
}
void onClick(int id) async {
errorMessage = null;
try {
switch (id) {
case loadImage:
AlertUtils.showImagePickerModal(
context: context,
onImageFromCamera: () async {
onNewImages(await handleImagePickerResponse(
ImageUtils.takePhoto(cameraDevice: CameraDevice.rear)));
},
onImageFromGallery: () async {
onNewImages(await handleImagePickerResponse(
ImageUtils.pickImageFromGallery()));
},
);
break;
case doSignedUpload:
await upload();
break;
case doUnsignedUpload:
await upload(signed: false);
break;
case deleteUploadedData:
await delete();
break;
}
} catch (e) {
print(e);
loading = false;
setState(() => errorMessage = e.toString());
} finally {
if (loading) hideLoading();
}
}
void showLoading() => setState(() => loading = true);
void hideLoading() => setState(() => loading = false);
Future<List<String>> handleImagePickerResponse(Future getImageCall) async {
Map<String, dynamic> resource =
await (getImageCall as FutureOr<Map<String, dynamic>>);
if (resource.isEmpty) return [];
switch (resource['status']) {
case 'SUCCESS':
Navigator.pop(context);
return resource['data'];
default:
ImageUtils.showPermissionExplanation(
context: context, message: resource['message']);
break;
}
return [];
}
}
更多关于Flutter云存储与媒体处理插件cloudinary_sdk的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter云存储与媒体处理插件cloudinary_sdk的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,以下是一个关于如何在Flutter中使用cloudinary_sdk
插件进行云存储与媒体处理的示例代码。这个示例将展示如何上传图片并获取处理后的URL。
首先,确保你已经在pubspec.yaml
文件中添加了cloudinary_sdk
依赖:
dependencies:
flutter:
sdk: flutter
cloudinary_sdk: ^x.y.z # 请替换为最新版本号
然后运行flutter pub get
来安装依赖。
接下来,在你的Flutter项目中,你需要进行以下步骤:
-
配置Cloudinary: 在Cloudinary官网创建一个账户并获取你的Cloudinary配置参数(Cloud Name, API Key, API Secret)。
-
初始化Cloudinary客户端: 在你的Flutter应用中初始化Cloudinary客户端。
-
上传图片并获取处理后的URL。
下面是一个完整的示例代码:
import 'package:flutter/material.dart';
import 'package:cloudinary_sdk/cloudinary_sdk.dart';
import 'dart:io';
import 'dart:typed_data';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: CloudinaryUploadPage(),
);
}
}
class CloudinaryUploadPage extends StatefulWidget {
@override
_CloudinaryUploadPageState createState() => _CloudinaryUploadPageState();
}
class _CloudinaryUploadPageState extends State<CloudinaryUploadPage> {
final Cloudinary _cloudinary = Cloudinary(
cloudName: 'your_cloud_name', // 替换为你的Cloud Name
apiKey: 'your_api_key', // 替换为你的API Key
apiSecret: 'your_api_secret', // 替换为你的API Secret
);
File? _imageFile;
String? _uploadedImageUrl;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Cloudinary Upload Example'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
_imageFile == null
? Text('No image selected.')
: Image.file(_imageFile!),
SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
_pickImage(context);
},
child: Text('Pick Image'),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _imageFile == null ? null : () async {
await _uploadImage();
},
child: Text('Upload Image'),
),
if (_uploadedImageUrl != null)
SizedBox(height: 20),
if (_uploadedImageUrl != null)
Text(
'Uploaded Image URL: $_uploadedImageUrl',
style: TextStyle(color: Colors.blue, decoration: TextDecoration.underline),
),
],
),
),
);
}
Future<void> _pickImage(BuildContext context) async {
final picker = ImagePicker();
final pickedFile = await picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
setState(() {
_imageFile = File(pickedFile.path);
});
} else {
print('No image selected.');
}
}
Future<void> _uploadImage() async {
if (_imageFile == null) return;
final Uint8List imageData = await _imageFile!.readAsBytes();
final Map<String, dynamic> uploadResult = await _cloudinary.upload(
imageData,
publicId: DateTime.now().millisecondsSinceEpoch.toString(), // 为上传的文件设置一个唯一的publicId
resourceType: 'image',
);
if (uploadResult.containsKey('url')) {
setState(() {
_uploadedImageUrl = uploadResult['url'];
});
} else {
print('Failed to upload image: ${uploadResult['error']['message']}');
}
}
}
说明:
-
依赖导入: 导入
cloudinary_sdk
包和其他必要的Flutter包。 -
Cloudinary初始化: 在
_CloudinaryUploadPageState
类中初始化Cloudinary客户端,替换your_cloud_name
、your_api_key
和your_api_secret
为你的Cloudinary配置参数。 -
图片选择: 使用
image_picker
包(未直接在代码中导入,但你需要添加image_picker
依赖并在需要时导入)来选择图片。 -
图片上传: 使用Cloudinary SDK的
upload
方法上传图片,并获取处理后的URL。 -
UI显示: 在UI中显示选择的图片和上传后的URL。
请确保在真实项目中处理好错误和异常情况,并根据需要进行更多的配置和优化。