如何在Android API 33中使用.NET MAUI FileSaver实现文件保存?
.NET MAUI Android API 33下FileSaver无法保存文件的解决方法
问题描述
开发.NET MAUI应用时,需从远程API获取图片并保存至设备存储,但在Android API 33版本遇到权限问题:
- MAUI Community Toolkit的
FileSaver类仍依赖已废弃的WRITE_EXTERNAL_STORAGE权限,该权限在API33下默认被拒绝 - 调用
FileSaver时无权限弹窗,直接抛出「存储权限未授予」异常 - 目标SDK设为31时可正常运行,但不符合Google Play商店政策要求
现有Android清单
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="si.stroka.signature365" android:versionName="1"> <application android:allowBackup="true" android:icon="@mipmap/appicon" android:supportsRtl="true" android:label="Signature365 Android"></application> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.GET_ACCOUNTS" /> <uses-permission android:name="android.permission.GET_ACCOUNTS_PRIVILEGED" /> <uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.ACCOUNT_MANAGER" /> <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> </manifest>
现有调用代码
using var stream = new MemoryStream(Encoding.Default.GetBytes("Hello from the Community Toolkit!")); var fileSaverResult = await FileSaver.Default.SaveAsync("test.txt", stream, token.Token); if (fileSaverResult.IsSuccessful) { await Toast.Make($"The file was saved successfully to location: {fileSaverResult.FilePath}").Show(token.Token); } else { await Toast.Make($"The file was not saved successfully with error: {fileSaverResult.Exception.Message}").Show(token.Token); }
解决方案
1. 升级MAUI Community Toolkit到兼容API33的版本
将CommunityToolkit.Maui包升级至1.10.0及以上的稳定版,新版本已适配Android API33权限机制,不再强制依赖WRITE_EXTERNAL_STORAGE。
2. 修正Android权限配置
移除已废弃的WRITE_EXTERNAL_STORAGE权限,根据文件类型添加API33对应的媒体权限,同时保留低版本兼容配置:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="si.stroka.signature365" android:versionName="1"> <application android:allowBackup="true" android:icon="@mipmap/appicon" android:supportsRtl="true" android:label="Signature365 Android"></application> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> <!-- 保留必要的账户权限 --> <uses-permission android:name="android.permission.GET_ACCOUNTS" /> <uses-permission android:name="android.permission.GET_ACCOUNTS_PRIVILEGED" /> <uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.ACCOUNT_MANAGER" /> <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" /> <!-- API33图片权限 --> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" android:maxSdkVersion="33" /> <!-- 兼容低版本的存储权限 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> </manifest>
3. 手动处理权限请求
在调用FileSaver前主动检查并请求对应权限:
// 检查并请求权限 var status = OperatingSystem.IsAndroidVersionAtLeast(33) ? await Permissions.CheckStatusAsync<Permissions.MediaImages>() : await Permissions.CheckStatusAsync<Permissions.StorageWrite>(); if (status != PermissionStatus.Granted) { status = OperatingSystem.IsAndroidVersionAtLeast(33) ? await Permissions.RequestAsync<Permissions.MediaImages>() : await Permissions.RequestAsync<Permissions.StorageWrite>(); } if (status == PermissionStatus.Granted) { // 执行文件保存逻辑 using var stream = new MemoryStream(Encoding.Default.GetBytes("Hello from the Community Toolkit!")); var fileSaverResult = await FileSaver.Default.SaveAsync("test.txt", stream, token.Token); if (fileSaverResult.IsSuccessful) { await Toast.Make($"文件已保存至: {fileSaverResult.FilePath}").Show(token.Token); } else { await Toast.Make($"保存失败: {fileSaverResult.Exception.Message}").Show(token.Token); } } else { await Toast.Make("需要授予存储权限才能保存文件").Show(token.Token); }
4. 替代方案:使用MediaStore保存到公共媒体目录
若FileSaver仍有问题,可直接通过Android原生MediaStoreAPI保存图片,无需依赖第三方库:
public async Task SaveImageToGalleryAsync(Stream imageStream, string fileName, CancellationToken token) { // 权限检查 if (OperatingSystem.IsAndroidVersionAtLeast(33)) { var status = await Permissions.CheckStatusAsync<Permissions.MediaImages>(); if (status != PermissionStatus.Granted) { status = await Permissions.RequestAsync<Permissions.MediaImages>(); if (status != PermissionStatus.Granted) return; } } else { var status = await Permissions.CheckStatusAsync<Permissions.StorageWrite>(); if (status != PermissionStatus.Granted) { status = await Permissions.RequestAsync<Permissions.StorageWrite>(); if (status != PermissionStatus.Granted) return; } } var context = Android.App.Application.Context; var contentValues = new ContentValues(); contentValues.Put(MediaStore.Images.Media.Columns.DisplayName, fileName); contentValues.Put(MediaStore.Images.Media.Columns.MimeType, "image/png"); contentValues.Put(MediaStore.Images.Media.Columns.RelativePath, Environment.DirectoryPictures + "/Signature365"); var uri = context.ContentResolver.Insert(MediaStore.Images.Media.ExternalContentUri, contentValues); if (uri == null) return; using var outputStream = context.ContentResolver.OpenOutputStream(uri); await imageStream.CopyToAsync(outputStream, token); await Toast.Make("图片已保存到相册").Show(token.Token); }
5. 替代方案:使用存储访问框架(SAF)
若需让用户自定义保存位置,可使用SAF文件选择器:
public async Task SaveFileWithSAFAsync(Stream fileStream, string fileName, CancellationToken token) { var intent = new Intent(Intent.ActionCreateDocument); intent.AddCategory(Intent.CategoryOpenable); intent.SetType("image/png"); intent.PutExtra(Intent.ExtraTitle, fileName); var result = await Platform.CurrentActivity.StartActivityForResultAsync(intent, 100); if (result.ResultCode == Result.Ok && result.Data != null) { using var outputStream = Platform.CurrentActivity.ContentResolver.OpenOutputStream(result.Data.Data); await fileStream.CopyToAsync(outputStream, token); await Toast.Make("文件已保存").Show(token.Token); } }
内容的提问来源于stack exchange,提问作者primix
相关产品推荐
相关产品推荐

