往期鸿蒙5.0全套实战文章必看:(文中附带全栈鸿蒙5.0学习资料)
本文由上下两部分构成,若您想快速浏览上半部分的精彩内容,可通过 大文件高速并发传输解决方案(上)直接访问。
3. 实践案例
案例简介
以大文件下载场景为例,本案例(Sample)基于SFFT三方库和ArkUI,实现了一个体现SFFT多线程并发下载、断点续下等功能特性的下载页面,案例中提供了关键的开发步骤和代码实现,旨在为开发者开发大文件下载场景提供案例指导。
版本约束如下:
支持的OS语言 | ArkTS |
支持的设备 | 手机/折叠屏/平板/PC |
支持的OS设备 | 单框架 |
支持的API | >=12 |
Sample效果展示
实现说明
布局说明
页面基于Navigation组件实现,菜单栏提供全部下载/暂停、全部删除和上传/下载视图切换功能。内容区为DownloadView(模式切换后为UploadView)。View由基本UI元素和DownloadItem元素项组成,DownloadItem中集成了网络库的主要功能,包括单个文件的下载、暂停和删除。效果图如下:
网络库的接口调用是本Sample的核心和重点,下面将围绕super-fast-file-trans三方库的使用的关键步骤及其细节展开介绍:
使用SFFT三方库实现文件下载功能:
(1)导入SFFT :
import { DownloadConfig, DownloadListener, DownloadManager } from "@hadss/super-fast-file-trans";
import { DownloadTask } from "@hadss/super-fast-file-trans";
import { DownloadProgressInfo } from "@hadss/super-fast-file-trans";
(2)对下载项DownloadItem进行封装,该类使用SFFT库的下载能力对单个下载项进行处理,同时作为组件供UI视图进行排版布局。
import { DownloadConfig, DownloadListener, DownloadManager } from "@hadss/super-fast-file-trans";
import { DownloadTask } from "@hadss/super-fast-file-trans";
import { DownloadProgressInfo } from "@hadss/super-fast-file-trans";
import { FileItem } from "../model/FileItem";
import MemoryTool from "../util/MemoryTool";
import { BusinessError } from "@kit.BasicServicesKit";
@Component
export struct DownloadItem {
@State itemKey: number = 0; // 下载项key,所属列表内唯一标识属性
@State fileName: string = '文件'; // 文件名
@State url: string = ''; // 下载地址url
@State concurrency: number = 1;
@State isPaused: boolean = false; // 是否暂停过
@State isDownloading: boolean = false;
@State transferredSize: number = 0;
@State totalSize: number = 0;
@State speed: number = 0;
@State currentRate: number = 0;
@Link downloadTaskList: Array<FileItem>; // 当前item所属的下载列表
@Link downloadingList?: Set<number>;
@StorageLink('isDownloadAll') @Watch('onDownloadAll') isDownloadAll: boolean = false; // 接收外部全部下载指令
@StorageLink('isPauseAll') @Watch('onPauseAll') isPauseAll: boolean = false; // 接收外部全部下载指令
@State downloadListener: DownloadListener = {};
private downloadInstance: DownloadTask | undefined;
private downloadConfig: DownloadConfig = { url: this.url, fileName: this.fileName };
private memoryTool: MemoryTool = new MemoryTool();
async aboutToAppear() {
this.downloadConfig = { url: this.url, fileName: this.fileName, concurrency: this.concurrency };
let downloadListener: DownloadListener = {
onStart: async (trialConnectionResponse: Record<string, string | string[] | undefined>) => {
this.isDownloading = true;
this.downloadingList?.add(this.itemKey);
},
onResume: async (trialConnectionResponse: Record<string, string | string[] | undefined>,
downloadProgress: DownloadProgressInfo) => {
this.isDownloading = true;
this.downloadingList?.add(this.itemKey);
},
onPause: (downloadProgress: DownloadProgressInfo) => {
this.isDownloading = false;
this.speed = 0;
this.downloadingList?.delete(this.itemKey);
},
onSuccess: (filePath: string) => {
this.isDownloading = false;
this.downloadingList?.delete(this.itemKey);
},
onFail: (err: BusinessError) => {
this.isDownloading = false;
this.downloadingList?.delete(this.itemKey);
},
onProgressUpdate: (downloadProgress: DownloadProgressInfo) => {
this.transferredSize = downloadProgress.transferredSize;
this.totalSize = downloadProgress.totalSize;
this.speed = downloadProgress.speed;
this.currentRate = this.transferredSize / this.totalSize * 100;
}
}
this.downloadInstance =
DownloadManager.getInstance().createDownloadTask(this.downloadConfig, downloadListener);
await DownloadManager.getInstance().init(getContext());
this.downloadInstance?.getProgress().then((progress: DownloadProgressInfo) => {
this.transferredSize = progress.transferredSize;
this.totalSize = progress.totalSize;
this.currentRate = progress.transferredSize / progress.totalSize * 100;
if (this.transferredSize === 0) {
this.isPaused = false;
} else {
this.isPaused = true;
}
});
}
onDownloadAll() {
if (this.isDownloadAll && !this.isDownloading) {
this.download();
}
}
async onPauseAll() {
if (this.isPauseAll && this.isDownloading) {
await this.pause();
this.isPaused = true;
}
}
async download() {
if (!this.isPaused || this.transferredSize == this.totalSize) {
await this.start();
} else {
await this.resume();
}
}
async start() {
await this.downloadInstance?.start();
}
async resume() {
await this.downloadInstance?.resume();
}
async pause() {
await this.downloadInstance?.pause();
}
async cancel() {
this.isDownloading = false;
await this.downloadInstance?.cancel();
}
build() {
Column() {
Row() {
Column() {
Image($r('app.media.logo'))
.width(46)
.height(46)
}
Column() {
Row() {
Text(this.fileName)
.fontWeight(FontWeight.Bold)
.fontColor(Color.Black)
.fontSize(18)
.opacity(0.9)
}
Row() {
Text(this.memoryTool.convert(this.transferredSize) + '/' +
this.memoryTool.convert(this.totalSize) + ' - ' +
this.memoryTool.convert(this.speed) + '/s')
.fontSize(14)
.fontColor(Color.Black)
.opacity(0.6)
}
}
.alignItems(HorizontalAlign.Start)
.justifyContent(FlexAlign.SpaceAround)
.width(164)
.height(46)
Column() {
Row() {
Image(this.isDownloading ? $r('app.media.pause') : $r('app.media.play'))
.width(24)
.height(24)
.onClick(async () => {
if (this.isDownloading) {
this.isPaused = true;
await this.pause();
} else {
await this.download();
}
})
Image($r('app.media.cancel'))
.width(24)
.height(24)
.onClick(() => {
this.cancel();
this.downloadTaskList = this.downloadTaskList.filter((item: FileItem) => {
return item.fileKey != this.itemKey;
});
})
}
.justifyContent(FlexAlign.SpaceBetween)
.width(64)
.height(24)
}
.height(46)
.justifyContent(FlexAlign.Start)
}
.justifyContent(FlexAlign.SpaceBetween)
.width(307)
.height(62)
Row() {
Progress({ value: this.currentRate, total: 100, type: ProgressType.Linear })
.width(307)
.height(24)
}
}
.width(328)
.height(86)
.justifyContent(FlexAlign.Center)
}
}
(3)以Sample提供的效果为例,开发者使用步骤(2)封装的DownloadItem组件进行下载视图DownloadView的封装。在本步骤开发者需填入下载所需的相关参数,初始化DownloadConfig传入DownloadItem中。
import { DownloadItem } from "../components/DownloadItem";
import { FileItem } from "../model/FileItem";
@Component
export struct DownloadView {
@State regularList: Array<FileItem> =
[new FileItem(0, '影视剧.mp4',
'https://vd3.bdstatic.com/mda-pid1q4efihenk6su/576p/h264/1694654179925144900/mda-pid1q4efihenk6su.mp4', 1),
new FileItem(1, '外国历史.mp4',
'https://vd3.bdstatic.com/mda-kkjr1tjxv6w6251c/hd/mda-kkjr1tjxv6w6251c.mp4', 1)];
@State multiList: Array<FileItem> = [new FileItem(2, '流行音乐.mp4',
'https://vd3.bdstatic.com/mda-qmfgk7auzqy1ph62/576p/h264/1734369330734616164/mda-qmfgk7auzqy1ph62.mp4', 4),
new FileItem(3, '中国.mp4',
'https://vd3.bdstatic.com/mda-nfm9j7syzbu70z5w/576p/cae_h264/1655914071297652987/mda-nfm9j7syzbu70z5w.mp4', 4)];
@State @Watch('onDownloadSum') downloadingList: Set<number> = new Set();
@StorageLink('isDeleteAll') @Watch('onDeleteAll') allDelete: boolean = false;
@Link isAllDownload: boolean;
onDownloadSum() {
if (this.downloadingList.size == this.regularList.length + this.multiList.length) {
this.isAllDownload = true;
AppStorage.setOrCreate('isPauseAll', false);
}
if (this.downloadingList.size == 0) {
this.isAllDownload = false;
AppStorage.setOrCreate('isDownloadAll', false);
}
}
onDeleteAll() {
AppStorage.setOrCreate('allDelete', false);
this.regularList = [];
this.multiList = [];
}
build() {
Column() {
Row() {
Text($r('app.string.regularDownload'))
.fontSize(14)
.fontColor(Color.Black)
.opacity(0.6)
}
.width(328)
.height(20)
.justifyContent(FlexAlign.Start)
Column() {
ForEach(this.regularList, (item: FileItem, index: number) => {
DownloadItem({
itemKey: item.fileKey,
fileName: item.fileName,
url: item.url,
downloadTaskList: this.regularList,
downloadingList: this.downloadingList
})
}, (item: string, index: number) => item);
}
.margin({ top: 8 })
Row() {
Text($r('app.string.multiDownload'))
.fontSize(14)
.fontColor(Color.Black)
.opacity(0.6)
}
.width(328)
.height(20)
.margin({ top: 212 - this.regularList.length * 86 })
.justifyContent(FlexAlign.Start)
Column() {
ForEach(this.multiList, (item: FileItem, index: number) => {
DownloadItem({
itemKey: item.fileKey,
fileName: item.fileName,
url: item.url,
concurrency: item.concurrency,
downloadTaskList: this.multiList,
downloadingList: this.downloadingList
})
}, (item: string, index: number) => item);
}
.margin({ top: 8 })
}
.height('100%')
.width('100%')
.justifyContent(FlexAlign.Start)
}
}
(4)开发者在Index应用首页,根据自身应用的需要使用DownloadView进行页面侧的UX布局实现
网络库Sample提供的界面对应的UX效果与代码如下:
import { DownloadView } from '../view/DownloadView';
import { UploadView } from '../view/UploadView';
import { LengthMetrics } from '@kit.ArkUI';
import { CustomMenuItem } from '../components/CustomMenuItem';
import { DownloadManager } from '@hadss/super-fast-file-trans';
@Entry
@Component
struct Index {
@State isDownloadStatus: boolean = true;
@State isAllDownload: boolean = false;
@State isClickable: boolean = true;
@Builder
MyMenu() {
Menu() {
MenuItem({ content: '下载文件' })
.width(103)
.onClick(() => {
this.isDownloadStatus = true;
AppStorage.setOrCreate('isDeleteAll', false);
})
MenuItem({ content: '上传文件' })
.width(103)
.onClick(() => {
this.isDownloadStatus = false;
AppStorage.setOrCreate('isDeleteAll', false);
})
}
.menuItemDivider({
strokeWidth: new LengthMetrics(0.5),
color: '#E6000000',
startMargin: new LengthMetrics(0),
endMargin: new LengthMetrics(0)
})
}
@Builder
DownloadMenu() {
Row() {
if (!this.isAllDownload) {
CustomMenuItem({ image: $r('app.media.download') })
.onClick(() => {
AppStorage.setOrCreate('isDownloadAll', true);
AppStorage.setOrCreate('isPauseAll', false);
})
} else {
CustomMenuItem({ image: $r('app.media.pause') })
.onClick(() => {
AppStorage.setOrCreate('isPauseAll', true);
AppStorage.setOrCreate('isDownloadAll', false);
})
}
CustomMenuItem({ image: $r('app.media.delete') })
.onClick(() => {
AppStorage.setOrCreate('isDeleteAll', true);
this.isAllDownload = false;
AppStorage.setOrCreate('isDownloadAll', false);
DownloadManager.getInstance().cleanAll(getContext());
})
CustomMenuItem({ image: $r('app.media.switch') })
.bindMenu(this.MyMenu())
}
.height(56)
.margin({ right: 16 })
.alignItems(VerticalAlign.Center)
}
@Builder
UploadMenu() {
Row() {
CustomMenuItem({ image: $r('app.media.upload') })
.onClick(() => {
AppStorage.setOrCreate('isUploadAll', true);
})
CustomMenuItem({ image: $r('app.media.delete') })
.onClick(() => {
AppStorage.setOrCreate('isDeleteAll', true);
})
CustomMenuItem({ image: $r('app.media.switch') })
.bindMenu(this.MyMenu())
}
.height(56)
.margin({ right: 16 })
.alignItems(VerticalAlign.Center)
}
build() {
Column() {
Navigation() {
Column() {
if (this.isDownloadStatus) {
DownloadView({ isAllDownload: this.isAllDownload })
.width(474)
.height(328)
.margin({ top: 50 })
} else {
UploadView()
.width(474)
.height(328)
.margin({ top: 50 })
}
}
.width('100%')
.justifyContent(FlexAlign.Start)
}
.menus(this.isDownloadStatus ? this.DownloadMenu() : this.UploadMenu())
.title($r('app.string.sampleTitle'))
.width('100%')
.height('100%')
}
}
}
Sample代码
由于上传场景的代码实现与下载场景的代码实现相似,本文不再赘述。
4. 总结与回顾
本文深入探讨了实现大文件高速传输的关键技术,介绍了super_fast_file_trans三方库(SFFT)提供的多线程并发下载、断点续下、分片上传、断点续传、自动重试等多个功能特性,旨在帮助开发者快速实现大文件传输场景,有效提高鸿蒙开发中文件数据传输的可靠性和高效性,为了使读者能更加容易理解上述特性,本文给出了上述特性的实现原理、效果展示和代码实现。除此之外,本文基于SFFT给出了一个文件下载场景的Sample实现,包括接口使用,界面实现,源码地址等,旨在协助开发者使用SFFT三方库快速又高性能地实现大文件数据传输场景。