Flutter實(shí)現(xiàn)資源下載斷點(diǎn)續(xù)傳的示例代碼
協(xié)議梳理
一般情況下,下載的功能模塊,至少需要提供如下基礎(chǔ)功能:資源下載、取消當(dāng)前下載、資源是否下載成功、資源文件的大小、清除緩存文件。而斷點(diǎn)續(xù)傳主要體現(xiàn)在取消當(dāng)前下載后,再次下載時(shí)能在之前已下載的基礎(chǔ)上繼續(xù)下載。這個(gè)能極大程度的減少我們服務(wù)器的帶寬損耗,而且還能為用戶減少流量,避免重復(fù)下載,提高用戶體驗(yàn)。
前置條件:資源必須支持?jǐn)帱c(diǎn)續(xù)傳。如何確定可否支持?看看你的服務(wù)器是否支持Range請(qǐng)求即可。
實(shí)現(xiàn)步驟
1.定好協(xié)議。我們用的http庫是dio;通過校驗(yàn)md5檢測文件緩存完整性;關(guān)于代碼中的subDir,設(shè)計(jì)上認(rèn)為資源會(huì)有多種:音頻、視頻、安裝包等,每種資源分開目錄進(jìn)行存儲(chǔ)。
import 'package:dio/dio.dart';
typedef ProgressCallBack = void Function(int count, int total);
typedef CancelTokenProvider = void Function(CancelToken cancelToken);
abstract class AssetRepositoryProtocol {
/// 下載單一資源
Future<String> downloadAsset(String url,
{String? subDir,
ProgressCallBack? onReceiveProgress,
CancelTokenProvider? cancelTokenProvider,
Function(String)? done,
Function(Exception)? failed});
/// 取消下載,Dio中通過CancelToken可控制
void cancelDownload(CancelToken cancelToken);
/// 獲取文件的緩存地址
Future<String?> filePathForAsset(String url, {String? subDir});
/// 檢查文件是否緩存成功,簡單對(duì)比md5
Future<String?> checkCachedSuccess(String url, {String? md5Str});
/// 查看緩存文件的大小
Future<int> cachedFileSize({String? subDir});
/// 清除緩存
Future<void> clearCache({String? subDir});
}2.實(shí)現(xiàn)抽象協(xié)議,其中HttpManagerProtocol內(nèi)部封裝了dio的相關(guān)請(qǐng)求。
class AssetRepository implements AssetRepositoryProtocol {
AssetRepository(this.httpManager);
final HttpManagerProtocol httpManager;
@override
Future<String> downloadAsset(String url,
{String? subDir,
ProgressCallBack? onReceiveProgress,
CancelTokenProvider? cancelTokenProvider,
Function(String)? done,
Function(Exception)? failed}) async {
CancelToken cancelToken = CancelToken();
if (cancelTokenProvider != null) {
cancelTokenProvider(cancelToken);
}
final savePath = await _getSavePath(url, subDir: subDir);
try {
httpManager.downloadFile(
url: url,
savePath: savePath + '.temp',
onReceiveProgress: onReceiveProgress,
cancelToken: cancelToken,
done: () {
done?.call(savePath);
},
failed: (e) {
print(e);
failed?.call(e);
});
return savePath;
} catch (e) {
print(e);
rethrow;
}
}
@override
void cancelDownload(CancelToken cancelToken) {
try {
if (!cancelToken.isCancelled) {
cancelToken.cancel();
}
} catch (e) {
print(e);
}
}
@override
Future<String?> filePathForAsset(String url, {String? subDir}) async {
final path = await _getSavePath(url, subDir: subDir);
final file = File(path);
if (!(await file.exists())) {
return null;
}
return path;
}
@override
Future<String?> checkCachedSuccess(String url, {String? md5Str}) async {
String? path = await _getSavePath(url, subDir: FileType.video.dirName);
bool isCached = await File(path).exists();
if (isCached && (md5Str != null && md5Str.isNotEmpty)) {
// 存在但是md5驗(yàn)證不通過
File(path).readAsBytes().then((Uint8List str) {
if (md5.convert(str).toString() != md5Str) {
path = null;
}
});
} else if (isCached) {
return path;
} else {
path = null;
}
return path;
}
@override
Future<int> cachedFileSize({String? subDir}) async {
final dir = await _getDir(subDir: subDir);
if (!(await dir.exists())) {
return 0;
}
int totalSize = 0;
await for (var entity in dir.list(recursive: true)) {
if (entity is File) {
try {
totalSize += await entity.length();
} catch (e) {
print('Get size of $entity failed with exception: $e');
}
}
}
return totalSize;
}
@override
Future<void> clearCache({String? subDir}) async {
final dir = await _getDir(subDir: subDir);
if (!(await dir.exists())) {
return;
}
dir.deleteSync(recursive: true);
}
Future<String> _getSavePath(String url, {String? subDir}) async {
final saveDir = await _getDir(subDir: subDir);
if (!saveDir.existsSync()) {
saveDir.createSync(recursive: true);
}
final uri = Uri.parse(url);
final fileName = uri.pathSegments.last;
return saveDir.path + fileName;
}
Future<Directory> _getDir({String? subDir}) async {
final cacheDir = await getTemporaryDirectory();
late final Directory saveDir;
if (subDir == null) {
saveDir = cacheDir;
} else {
saveDir = Directory(cacheDir.path + '/$subDir/');
}
return saveDir;
}
}3.封裝dio下載,實(shí)現(xiàn)資源斷點(diǎn)續(xù)傳。
這里的邏輯比較重點(diǎn),首先未緩存100%的文件,我們以.temp后綴進(jìn)行命名,在每次下載時(shí)檢測下是否有.temp的文件,拿到其文件字節(jié)大??;傳入在header中的range字段,服務(wù)器就會(huì)去解析需要從哪個(gè)位置繼續(xù)下載;下載全部完成后,再把文件名改回正確的后綴即可。
final downloadDio = Dio();
Future<void> downloadFile({
required String url,
required String savePath,
required CancelToken cancelToken,
ProgressCallback? onReceiveProgress,
void Function()? done,
void Function(Exception)? failed,
}) async {
int downloadStart = 0;
File f = File(savePath);
if (await f.exists()) {
// 文件存在時(shí)拿到已下載的字節(jié)數(shù)
downloadStart = f.lengthSync();
}
print("start: $downloadStart");
try {
var response = await downloadDio.get<ResponseBody>(
url,
options: Options(
/// Receive response data as a stream
responseType: ResponseType.stream,
followRedirects: false,
headers: {
/// 加入range請(qǐng)求頭,實(shí)現(xiàn)斷點(diǎn)續(xù)傳
"range": "bytes=$downloadStart-",
},
),
);
File file = File(savePath);
RandomAccessFile raf = file.openSync(mode: FileMode.append);
int received = downloadStart;
int total = await _getContentLength(response);
Stream<Uint8List> stream = response.data!.stream;
StreamSubscription<Uint8List>? subscription;
subscription = stream.listen(
(data) {
/// Write files must be synchronized
raf.writeFromSync(data);
received += data.length;
onReceiveProgress?.call(received, total);
},
onDone: () async {
file.rename(savePath.replaceAll('.temp', ''));
await raf.close();
done?.call();
},
onError: (e) async {
await raf.close();
failed?.call(e);
},
cancelOnError: true,
);
cancelToken.whenCancel.then((_) async {
await subscription?.cancel();
await raf.close();
});
} on DioError catch (error) {
if (CancelToken.isCancel(error)) {
print("Download cancelled");
} else {
failed?.call(error);
}
}
}寫在最后
這篇文章確實(shí)沒有技術(shù)含量,水一篇,但其實(shí)是實(shí)用的。這個(gè)斷點(diǎn)續(xù)傳的實(shí)現(xiàn)有幾個(gè)注意的點(diǎn):
- 使用文件操作的方式,區(qū)分后綴名來管理緩存的資源;
- 安全性使用md5校驗(yàn),這點(diǎn)非常重要,斷點(diǎn)續(xù)傳下載的文件,在完整性上可能會(huì)因?yàn)楦鞣N突發(fā)情況而得不到保障;
- 在資源管理協(xié)議上,我們將下載、檢測、獲取大小等方法都抽象出去,在業(yè)務(wù)調(diào)用時(shí)比較靈活。
以上就是Flutter實(shí)現(xiàn)資源下載斷點(diǎn)續(xù)傳的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于Flutter資源下載斷點(diǎn)續(xù)傳的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android實(shí)現(xiàn)QQ手機(jī)管家懸浮小火箭效果
這篇文章主要介紹了Android實(shí)現(xiàn)QQ手機(jī)管家懸浮小火箭效果,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-05-05
Android adb安裝apk時(shí)提示Invalid APK file的問題
這篇文章主要介紹了Android adb安裝apk時(shí)提示Invalid APK file的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-08-08
Flutter?點(diǎn)擊兩次退出app的實(shí)現(xiàn)示例
本文主要介紹了Flutter?點(diǎn)擊兩次退出app的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05
Android中 webView調(diào)用JS出錯(cuò)的解決辦法
這篇文章主要介紹了Android中 webView調(diào)用JS出錯(cuò)的解決辦法,需要的朋友可以參考下2015-01-01
RecyclerView焦點(diǎn)跳轉(zhuǎn)BUG優(yōu)化的方法
這篇文章主要介紹了RecyclerView焦點(diǎn)跳轉(zhuǎn)BUG優(yōu)化的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-04-04
Android RecyclerView的焦點(diǎn)記憶封裝
這篇文章主要介紹了Android RecyclerView的焦點(diǎn)記憶封裝,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-04-04

