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

Xamarin.Forms如何从FirebaseStorage下载非图片文件到内部存储并显示进度

Xamarin.Forms实现Firebase Storage文件下载+通知栏进度显示方案

日志说明

你贴出的两条输出并非程序报错,是Android系统的常规调试日志,翻译如下:

显式并发复制GC释放了3个(16KB)AllocSpace对象,0个(0B)大对象空间(LOS)对象,49%可用,3014KB/6028KB,暂停84微秒,总耗时21.880毫秒
[Surface] 操作服务为空,值为false

原有代码的实际问题如下:

  • HttpClient的GetByteArrayAsync会将整个文件加载到内存,大文件场景下容易出现内存溢出
  • 存储路径选择了应用私有目录,用户无法在系统文件管理器中找到下载的文件
  • WebClient的DownloadFile是同步方法,放在异步方法中会阻塞主线程,且未指定完整存储路径
  • 没有进度回调逻辑,无法获取实时下载进度更新通知栏

实现步骤

1. 提前申请必要权限

  • Android端:需要申请存储权限,Android 13及以上版本需要申请READ_MEDIA_DOCUMENTS权限,显示前台通知需要申请FOREGROUND_SERVICE权限
  • iOS端:需要在Info.plist中配置NSDocumentDirectoryUsageDescription权限说明,若要允许用户访问下载的文件需要开启iTunes文件共享

2. 定义跨平台下载服务接口

在共享项目中定义依赖服务接口,用于调用各平台的下载和通知逻辑:

public interface IDownloadService
{
    /// <summary>
    /// 下载文件
    /// </summary>
    /// <param name="url">文件下载地址</param>
    /// <param name="fileName">保存的文件名</param>
    /// <param name="progressCallback">进度回调,返回0-100的进度值</param>
    Task DownloadFileAsync(string url, string fileName, Action<int> progressCallback);
    /// <summary>
    /// 更新通知栏下载进度
    /// </summary>
    /// <param name="progress">进度值</param>
    /// <param name="fileName">文件名</param>
    void UpdateNotification(int progress, string fileName);
}

3. 按钮点击逻辑调整

修改你的按钮点击事件,获取Firebase Storage下载地址后调用依赖服务执行下载:

private async void DownloadButton_Clicked(object sender, EventArgs e)
{
    // 先申请存储权限,权限通过后再执行下载
    var status = await Permissions.CheckStatusAsync<Permissions.StorageWrite>();
    if (status != PermissionStatus.Granted)
    {
        status = await Permissions.RequestAsync<Permissions.StorageWrite>();
        if (status != PermissionStatus.Granted)
        {
            await DisplayAlert("提示", "需要存储权限才能下载文件", "确定");
            return;
        }
    }

    try
    {
        var fc = new FirebaseStorage("你自己的firebase存储bucket地址.appspot.com");
        var fileUrl = await fc.Child("NonImage").Child("Pdf").Child("test.pdf").GetDownloadUrlAsync();
        var downloadService = DependencyService.Get<IDownloadService>();
        
        await downloadService.DownloadFileAsync(fileUrl, "test.pdf", progress =>
        {
            // 进度更新时同步更新通知栏
            downloadService.UpdateNotification(progress, "test.pdf");
        });
        
        await DisplayAlert("提示", "文件下载完成,已保存到系统下载目录", "确定");
    }
    catch (Exception ex)
    {
        await DisplayAlert("错误", $"下载失败:{ex.Message}", "确定");
    }
}

4. Android平台服务实现

在Android项目中实现IDownloadService接口,包含分块下载逻辑和通知更新逻辑:

[assembly: Dependency(typeof(DownloadService_Android))]
namespace YourAppName.Droid
{
    public class DownloadService_Android : IDownloadService
    {
        private const int DownloadNotificationId = 1001;
        private const string DownloadChannelId = "download_notification_channel";

        public async Task DownloadFileAsync(string url, string fileName, Action<int> progressCallback)
        {
            using var httpClient = new HttpClient();
            // 只读取响应头,不立即加载全部内容
            using var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
            response.EnsureSuccessStatusCode();
            
            var totalBytes = response.Content.Headers.ContentLength ?? -1L;
            var downloadedBytes = 0L;
            // 保存到系统公共下载目录,用户可在文件管理器中找到
            var downloadDir = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDownloads).AbsolutePath;
            var filePath = Path.Combine(downloadDir, fileName);

            using var contentStream = await response.Content.ReadAsStreamAsync();
            using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true);
            var buffer = new byte[8192];
            int bytesRead;

            while ((bytesRead = await contentStream.ReadAsync(buffer)) > 0)
            {
                await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead));
                downloadedBytes += bytesRead;
                if (totalBytes > 0)
                {
                    var progress = (int)((downloadedBytes * 100) / totalBytes);
                    progressCallback?.Invoke(progress);
                }
            }
        }

        public void UpdateNotification(int progress, string fileName)
        {
            var context = Android.App.Application.Context;
            var notificationManager = context.GetSystemService(Android.Content.Context.NotificationService) as NotificationManager;

            // Android 8.0及以上需要创建通知渠道
            if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.O)
            {
                var channel = new NotificationChannel(DownloadChannelId, "下载通知", NotificationImportance.Low)
                {
                    Description = "显示文件下载进度",
                    VibrationEnabled = false,
                    Sound = null
                };
                notificationManager.CreateNotificationChannel(channel);
            }

            var builder = new NotificationCompat.Builder(context, DownloadChannelId)
                .SetSmallIcon(Resource.Drawable.ic_download) // 替换为你自己的通知图标
                .SetContentTitle(progress >= 100 ? "下载完成" : $"正在下载 {fileName}")
                .SetOngoing(progress < 100);

            if (progress < 100)
            {
                builder.SetProgress(100, progress, false);
                builder.SetContentText($"{progress}%");
            }
            else
            {
                builder.SetProgress(0, 0, false);
                builder.SetContentText($"文件已保存到下载目录");
            }

            notificationManager.Notify(DownloadNotificationId, builder.Build());
        }
    }
}

5. iOS平台服务实现

iOS端实现逻辑类似,下载路径选择Documents目录,通知用UserNotifications框架实现即可。

注意事项

  • 大文件下载建议搭配Android的前台服务、iOS的后台下载能力实现,避免应用切后台后下载被系统终止
  • 可添加下载任务队列逻辑,支持同时下载多个文件
  • 下载前可先判断目标文件是否已存在,避免重复下载

内容的提问来源于stack exchange,提问作者Steven-Carrot

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.26 18:27:03