You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 18:05:55