MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Flutter网络请求的进度指示:提升用户体验

2021-05-206.5k 阅读

Flutter网络请求概述

在Flutter应用开发中,网络请求是获取数据、与后端服务器交互的重要手段。常见的网络请求场景包括获取用户信息、加载列表数据、提交表单等。Flutter提供了多种网络请求的方式,如使用http包、dio包等。

http包是Flutter官方提供的网络请求库,它基于HttpClient实现,支持基本的HTTP请求方法,如GET、POST、PUT、DELETE等。以下是使用http包发送GET请求的简单示例:

import 'package:http/http.dart' as http;

Future<void> fetchData() async {
  final response = await http.get(Uri.parse('https://example.com/api/data'));
  if (response.statusCode == 200) {
    print(response.body);
  } else {
    print('Request failed with status: ${response.statusCode}.');
  }
}

dio包则是一个强大的、基于http封装的网络请求库,它提供了更丰富的功能,如拦截器、请求取消、上传下载进度监听等。以下是使用dio发送GET请求的示例:

import 'package:dio/dio.dart';

Future<void> fetchData() async {
  try {
    Dio dio = Dio();
    Response response = await dio.get('https://example.com/api/data');
    print(response.data);
  } catch (e) {
    print(e);
  }
}

网络请求中的用户体验问题

在网络请求过程中,用户可能会遇到各种情况,导致体验不佳。例如,当网络请求时间较长时,用户可能会以为应用程序出现了卡顿或崩溃,从而关闭应用。这是因为没有给用户提供明确的反馈,让用户知道请求正在进行中。

另外,在一些大文件的下载或上传场景中,如果没有进度指示,用户无法预估剩余时间,会感到焦虑。比如在下载一个大型应用更新包时,用户不知道还需要等待多久,就可能放弃下载。

实现网络请求进度指示的重要性

实现网络请求进度指示可以极大地提升用户体验。通过向用户展示请求的进度,用户可以清楚地了解到请求的状态,知道应用正在正常工作,而不是出现故障。在文件上传或下载场景中,进度指示可以让用户预估剩余时间,合理安排自己的等待时间。这不仅能减少用户的焦虑感,还能提高用户对应用的信任度和满意度。

使用dio实现网络请求进度指示

下载进度指示

dio包提供了方便的方法来监听下载进度。以下是一个示例,展示如何下载文件并显示下载进度:

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';

class DownloadPage extends StatefulWidget {
  @override
  _DownloadPageState createState() => _DownloadPageState();
}

class _DownloadPageState extends State<DownloadPage> {
  double _progress = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Download Example'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          LinearProgressIndicator(
            value: _progress,
          ),
          ElevatedButton(
            onPressed: () async {
              Dio dio = Dio();
              try {
                await dio.download(
                  'https://example.com/file.zip',
                  'path/to/save/file.zip',
                  onReceiveProgress: (received, total) {
                    setState(() {
                      _progress = received / total;
                    });
                  },
                );
                print('Download completed');
              } catch (e) {
                print(e);
              }
            },
            child: Text('Download File'),
          ),
        ],
      ),
    );
  }
}

在上述代码中,dio.download方法接收三个参数:下载链接、保存路径和一个onReceiveProgress回调函数。在回调函数中,received表示已经接收的字节数,total表示文件的总字节数。通过将received除以total,可以得到下载进度,并通过setState更新UI显示。

上传进度指示

上传文件时同样可以监听进度。以下是一个上传文件的示例:

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';

class UploadPage extends StatefulWidget {
  @override
  _UploadPageState createState() => _UploadPageState();
}

class _UploadPageState extends State<UploadPage> {
  double _progress = 0;

  Future<File> getFile() async {
    final directory = await getTemporaryDirectory();
    final file = File('${directory.path}/example.txt');
    await file.writeAsString('Some content to upload');
    return file;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Upload Example'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          LinearProgressIndicator(
            value: _progress,
          ),
          ElevatedButton(
            onPressed: () async {
              File file = await getFile();
              Dio dio = Dio();
              try {
                FormData formData = FormData.fromMap({
                  'file': await MultipartFile.fromFile(file.path),
                });
                await dio.post(
                  'https://example.com/api/upload',
                  data: formData,
                  onSendProgress: (sent, total) {
                    setState(() {
                      _progress = sent / total;
                    });
                  },
                );
                print('Upload completed');
              } catch (e) {
                print(e);
              }
            },
            child: Text('Upload File'),
          ),
        ],
      ),
    );
  }
}

在这个示例中,首先通过getFile方法创建一个临时文件。然后在onPressed回调中,将文件包装成MultipartFile并添加到FormData中。dio.post方法的onSendProgress回调用于监听上传进度,sent表示已经发送的字节数,total表示文件的总字节数,同样通过setState更新UI显示上传进度。

使用http包实现网络请求进度指示(拓展思路)

虽然http包本身没有直接提供进度监听的方法,但我们可以通过一些拓展思路来实现。一种方法是通过继承HttpClient并覆盖相关方法来实现进度监听。以下是一个简单的示例:

import 'dart:async';
import 'dart:io';
import 'package:http/http.dart' as http;

class ProgressHttpClient extends HttpClient {
  StreamController<double> _progressController = StreamController<double>();
  Stream<double> get progressStream => _progressController.stream;

  @override
  Future<HttpClientRequest> putUrl(Uri url) async {
    final request = await super.putUrl(url);
    request.addListener((List<int> data) {
      // 这里可以根据已发送的数据计算上传进度
      // 假设知道总数据大小为totalSize
      int totalSize = 1024; // 示例值,实际需根据具体情况获取
      double progress = data.length / totalSize;
      _progressController.add(progress);
    });
    return request;
  }

  @override
  Future<HttpClientResponse> getUrl(Uri url) async {
    final request = await super.getUrl(url);
    final response = await request.close();
    Completer<int> lengthCompleter = Completer<int>();
    int totalLength = 0;
    response.listen((List<int> data) {
      totalLength += data.length;
      // 这里可以根据已接收的数据计算下载进度
      // 假设知道总数据大小为totalSize
      int totalSize = 1024; // 示例值,实际需根据具体情况获取
      double progress = totalLength / totalSize;
      _progressController.add(progress);
    }, onDone: () {
      lengthCompleter.complete(totalLength);
    });
    await lengthCompleter.future;
    return response;
  }
}

使用时可以这样:

Future<void> fetchDataWithProgress() async {
  ProgressHttpClient client = ProgressHttpClient();
  client.progressStream.listen((progress) {
    print('Progress: $progress');
  });
  final response = await client.getUrl(Uri.parse('https://example.com/api/data'));
  if (response.statusCode == 200) {
    print(await response.transform(utf8.decoder).join());
  } else {
    print('Request failed with status: ${response.statusCode}.');
  }
}

在上述代码中,ProgressHttpClient继承自HttpClient,重写了putUrlgetUrl方法。在putUrl方法中,通过监听请求数据的发送来计算上传进度;在getUrl方法中,通过监听响应数据的接收来计算下载进度。通过StreamController将进度数据发送出去,外部通过监听progressStream来获取进度。

处理复杂网络请求场景下的进度指示

并发请求进度指示

在实际应用中,可能会同时发起多个网络请求,需要综合展示这些请求的进度。例如,一个应用可能同时从多个API获取不同的数据,如用户信息、商品列表等。

可以通过创建一个类来管理多个请求的进度。以下是一个简单的示例:

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';

class MultiRequestProgress {
  Map<String, double> progressMap = {};
  double totalProgress = 0;

  void updateProgress(String requestKey, double progress) {
    progressMap[requestKey] = progress;
    double sum = 0;
    progressMap.forEach((key, value) {
      sum += value;
    });
    totalProgress = sum / progressMap.length;
  }
}

class MultiRequestPage extends StatefulWidget {
  @override
  _MultiRequestPageState createState() => _MultiRequestPageState();
}

class _MultiRequestPageState extends State<MultiRequestPage> {
  MultiRequestProgress _progressManager = MultiRequestProgress();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Multi - Request Example'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          LinearProgressIndicator(
            value: _progressManager.totalProgress,
          ),
          ElevatedButton(
            onPressed: () async {
              Dio dio = Dio();
              List<Future> requests = [
                dio.get('https://example.com/api/user'),
                dio.get('https://example.com/api/products')
              ];
              int index = 0;
              requests.forEach((request) {
                request.then((response) {
                  setState(() {
                    _progressManager.updateProgress('request$index', 1);
                  });
                }).catchError((e) {
                  setState(() {
                    _progressManager.updateProgress('request$index', 0);
                  });
                });
                index++;
              });
              await Future.wait(requests);
            },
            child: Text('Start Multi - Requests'),
          ),
        ],
      ),
    );
  }
}

在上述代码中,MultiRequestProgress类用于管理多个请求的进度。updateProgress方法根据每个请求的完成情况更新总进度。在MultiRequestPage中,同时发起两个请求,并在请求完成或失败时更新进度管理器中的进度,从而实时展示多个请求的综合进度。

链式请求进度指示

链式请求是指一个请求的结果作为下一个请求的输入。例如,先获取用户的认证令牌,然后使用该令牌获取用户的详细信息。

可以通过在每个请求中传递进度信息来实现链式请求的进度指示。以下是一个示例:

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';

class ChainRequestPage extends StatefulWidget {
  @override
  _ChainRequestPageState createState() => _ChainRequestPageState();
}

class _ChainRequestPageState extends State<ChainRequestPage> {
  double _progress = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Chain - Request Example'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          LinearProgressIndicator(
            value: _progress,
          ),
          ElevatedButton(
            onPressed: () async {
              Dio dio = Dio();
              try {
                // 第一个请求获取令牌
                Response tokenResponse = await dio.get('https://example.com/api/token');
                setState(() {
                  _progress = 0.5;
                });
                String token = tokenResponse.data['token'];
                // 第二个请求使用令牌获取用户信息
                Response userResponse = await dio.get(
                  'https://example.com/api/user',
                  options: Options(headers: {'Authorization': 'Bearer $token'}),
                );
                setState(() {
                  _progress = 1;
                });
                print(userResponse.data);
              } catch (e) {
                print(e);
              }
            },
            child: Text('Start Chain - Requests'),
          ),
        ],
      ),
    );
  }
}

在这个示例中,第一个请求获取令牌完成后,将进度更新为0.5。第二个请求使用获取到的令牌获取用户信息,完成后将进度更新为1,从而清晰地展示链式请求的进度。

优化网络请求进度指示的用户体验

进度动画效果

除了简单地显示进度条,还可以添加一些动画效果来提升用户体验。例如,在进度条的填充过程中添加一些过渡动画。

可以使用AnimatedLinearProgressIndicator来实现:

import 'package:flutter/material.dart';

class AnimatedProgressPage extends StatefulWidget {
  @override
  _AnimatedProgressPageState createState() => _AnimatedProgressPageState();
}

class _AnimatedProgressPageState extends State<AnimatedProgressPage> {
  double _progress = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Animated Progress Example'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          AnimatedLinearProgressIndicator(
            value: _progress,
            backgroundColor: Colors.grey[200],
            valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
            // 可以添加动画曲线
            curve: Curves.easeInOut,
          ),
          ElevatedButton(
            onPressed: () {
              setState(() {
                _progress = 0.5;
              });
            },
            child: Text('Update Progress'),
          ),
        ],
      ),
    );
  }
}

在上述代码中,AnimatedLinearProgressIndicator提供了平滑的进度变化动画。curve属性可以设置动画曲线,如Curves.easeInOut使进度变化更加自然。

错误处理与进度重置

在网络请求过程中,如果发生错误,不仅要向用户显示错误信息,还需要合理处理进度指示。一般来说,需要将进度重置为0,以便重新发起请求时进度指示能正确显示。

以下是在dio下载示例中添加错误处理和进度重置的代码:

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';

class DownloadPageWithErrorHandling extends StatefulWidget {
  @override
  _DownloadPageWithErrorHandlingState createState() => _DownloadPageWithErrorHandlingState();
}

class _DownloadPageWithErrorHandlingState extends State<DownloadPageWithErrorHandling> {
  double _progress = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Download Example with Error Handling'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          LinearProgressIndicator(
            value: _progress,
          ),
          ElevatedButton(
            onPressed: () async {
              Dio dio = Dio();
              try {
                await dio.download(
                  'https://example.com/file.zip',
                  'path/to/save/file.zip',
                  onReceiveProgress: (received, total) {
                    setState(() {
                      _progress = received / total;
                    });
                  },
                );
                print('Download completed');
              } catch (e) {
                setState(() {
                  _progress = 0;
                });
                print('Download error: $e');
              }
            },
            child: Text('Download File'),
          ),
        ],
      ),
    );
  }
}

在上述代码中,当下载过程中发生错误时,通过setState_progress重置为0,并打印错误信息,这样用户可以清楚地知道下载失败,并可以重新尝试下载。

预估剩余时间显示

在下载或上传大文件时,显示预估剩余时间可以让用户更好地了解等待情况。可以根据已完成的进度和过去一段时间内的下载/上传速度来预估剩余时间。

以下是在下载示例中添加预估剩余时间显示的代码:

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'dart:async';

class DownloadPageWithETA extends StatefulWidget {
  @override
  _DownloadPageWithETAState createState() => _DownloadPageWithETAState();
}

class _DownloadPageWithETAState extends State<DownloadPageWithETA> {
  double _progress = 0;
  String _eta = 'Calculating...';
  Stopwatch _stopwatch = Stopwatch();

  @override
  void initState() {
    super.initState();
    _stopwatch.start();
  }

  @override
  void dispose() {
    _stopwatch.stop();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Download Example with ETA'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          LinearProgressIndicator(
            value: _progress,
          ),
          Text('Estimated Time Remaining: $_eta'),
          ElevatedButton(
            onPressed: () async {
              Dio dio = Dio();
              try {
                await dio.download(
                  'https://example.com/file.zip',
                  'path/to/save/file.zip',
                  onReceiveProgress: (received, total) {
                    setState(() {
                      _progress = received / total;
                      double speed = received / _stopwatch.elapsed.inSeconds;
                      if (speed > 0) {
                        int remainingBytes = total - received;
                        int etaSeconds = (remainingBytes / speed).round();
                        _eta = '${Duration(seconds: etaSeconds).toString().split('.')[0]}';
                      }
                    });
                  },
                );
                print('Download completed');
              } catch (e) {
                setState(() {
                  _progress = 0;
                  _eta = 'Calculating...';
                });
                print('Download error: $e');
              }
            },
            child: Text('Download File'),
          ),
        ],
      ),
    );
  }
}

在上述代码中,通过Stopwatch记录下载开始的时间。在onReceiveProgress回调中,根据已接收的字节数和已花费的时间计算下载速度,进而预估剩余时间并显示给用户。当下载发生错误时,重置进度和预估剩余时间。

通过以上方法,可以全面地提升Flutter应用中网络请求的进度指示,为用户提供更好的体验。无论是简单的单请求,还是复杂的并发、链式请求场景,都能有效地向用户展示请求状态,减少用户的焦虑,提高应用的可用性和用户满意度。同时,通过优化进度指示的动画效果、错误处理和预估剩余时间显示等方面,进一步提升用户体验,使应用更加完善。在实际开发中,应根据具体的业务需求和场景,灵活选择和组合这些方法,打造出流畅、友好的网络交互体验。