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

如何通过异步 /await 提升 Flutter 网络请求的稳定性

2021-06-035.4k 阅读

一、Flutter 网络请求基础

1.1 Flutter 常用网络请求库

在 Flutter 开发中,有几个常用的网络请求库,例如 http 库和 dio 库。http 库是 Flutter 官方提供的一个简单易用的 HTTP 客户端,它可以发送各种类型的 HTTP 请求,如 GET、POST、PUT、DELETE 等。以下是使用 http 库发送 GET 请求的简单示例:

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

Future<String> fetchData() async {
  final response = await http.get(Uri.parse('https://example.com/api/data'));
  if (response.statusCode == 200) {
    return response.body;
  } else {
    throw Exception('Failed to load data');
  }
}

dio 库则是一个强大的 HTTP 客户端,它提供了更丰富的功能,如拦截器、请求重试、FormData 上传等。使用 dio 发送 GET 请求的示例如下:

import 'package:dio/dio.dart';

Future<String> fetchData() async {
  try {
    final dio = Dio();
    final response = await dio.get('https://example.com/api/data');
    return response.data.toString();
  } catch (e) {
    throw Exception('Failed to load data');
  }
}

1.2 网络请求面临的问题

网络请求在实际应用中面临着诸多不稳定因素。首先是网络延迟,不同的网络环境(如 Wi-Fi、4G、5G)以及服务器负载情况都会影响请求的响应时间。例如,在弱网环境下,请求可能需要很长时间才能得到响应,甚至可能超时。其次是网络中断,用户在移动过程中可能会从 Wi-Fi 切换到移动数据,或者遇到信号不好的区域,这都可能导致网络连接中断,使得正在进行的网络请求失败。另外,服务器端的问题也不容忽视,服务器可能出现故障、维护或者过载,从而无法正常处理请求并返回响应。

二、异步编程基础

2.1 异步编程的概念

在传统的同步编程中,代码是按照顺序依次执行的。例如:

void main() {
  print('Start');
  // 模拟一个耗时操作
  for (int i = 0; i < 1000000000; i++) {}
  print('End');
}

在这段代码中,print('End') 必须等待 for 循环这个耗时操作完成后才能执行,在 for 循环执行期间,程序的其他部分无法执行,用户界面也会处于卡顿状态。

而异步编程则允许程序在等待某个操作完成(如网络请求、文件读取等耗时操作)的同时,继续执行其他代码。在 Dart 语言中,异步编程主要通过 asyncawait 关键字来实现。

2.2 async 关键字

async 关键字用于定义一个异步函数。一个异步函数返回一个 Future 对象,它表示一个可能在未来完成的操作。例如:

Future<String> asyncFunction() async {
  // 模拟一个耗时操作
  await Future.delayed(const Duration(seconds: 2));
  return 'Result';
}

在这个例子中,asyncFunction 是一个异步函数,它返回一个 Future<String>。函数内部使用 await Future.delayed(const Duration(seconds: 2)) 模拟了一个耗时 2 秒的操作,然后返回字符串 'Result'

2.3 await 关键字

await 关键字只能在 async 函数内部使用,它用于暂停异步函数的执行,直到 await 后面的 Future 对象完成(resolved)。例如:

void main() async {
  print('Start');
  String result = await asyncFunction();
  print(result);
  print('End');
}

在这个 main 函数中,await asyncFunction() 会暂停 main 函数的执行,直到 asyncFunction 返回的 Future 完成,然后将返回的结果赋值给 result,接着继续执行后面的代码。

三、异步 /await 提升网络请求稳定性原理

3.1 避免阻塞主线程

在 Flutter 应用中,主线程负责处理用户界面的渲染和交互。如果网络请求在主线程中同步执行,那么在请求等待响应的过程中,主线程会被阻塞,用户界面将无法响应触摸事件、更新 UI 等操作,导致应用看起来卡顿甚至无响应。

通过使用 asyncawait 进行异步网络请求,网络请求操作会在后台线程执行,主线程不会被阻塞。例如,在使用 http 库进行网络请求时:

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

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String _data = '';

  Future<void> fetchData() async {
    final response = await http.get(Uri.parse('https://example.com/api/data'));
    if (response.statusCode == 200) {
      setState(() {
        _data = response.body;
      });
    } else {
      throw Exception('Failed to load data');
    }
  }

  @override
  void initState() {
    super.initState();
    fetchData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Network Request'),
      ),
      body: Center(
        child: Text(_data),
      ),
    );
  }
}

在这个示例中,fetchData 函数是异步的,await http.get(Uri.parse('https://example.com/api/data')) 不会阻塞主线程。在请求过程中,用户仍然可以与界面进行交互,当请求完成后,通过 setState 更新 UI 显示请求到的数据。

3.2 处理网络请求超时

网络请求可能由于各种原因长时间没有响应,如网络延迟过高、服务器负载过大等。为了避免应用一直等待,我们可以设置网络请求的超时时间。在使用 dio 库时,可以很方便地设置超时时间:

import 'package:dio/dio.dart';

Future<String> fetchData() async {
  try {
    final dio = Dio();
    dio.options.connectTimeout = 5000; // 连接超时 5 秒
    dio.options.receiveTimeout = 3000; // 接收超时 3 秒
    final response = await dio.get('https://example.com/api/data');
    return response.data.toString();
  } catch (e) {
    if (e is DioException && e.type == DioExceptionType.connectTimeout) {
      throw Exception('连接超时');
    } else if (e is DioException && e.type == DioExceptionType.receiveTimeout) {
      throw Exception('接收超时');
    }
    throw Exception('Failed to load data');
  }
}

在这个示例中,我们设置了连接超时时间为 5 秒,接收超时时间为 3 秒。如果请求在规定时间内没有完成,await dio.get('https://example.com/api/data') 会抛出异常,我们可以在 catch 块中捕获并处理这些异常,提示用户相应的错误信息,从而提升网络请求的稳定性。

3.3 错误处理与重试机制

在网络请求过程中,可能会遇到各种错误,如网络连接失败、服务器返回错误状态码等。通过异步 /await 可以方便地进行错误处理,并实现重试机制。

http 库为例,当服务器返回非 200 状态码时,我们可以进行错误处理:

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

Future<String> fetchData() async {
  final response = await http.get(Uri.parse('https://example.com/api/data'));
  if (response.statusCode == 200) {
    return response.body;
  } else {
    throw Exception('Failed to load data, status code: ${response.statusCode}');
  }
}

如果我们希望实现重试机制,可以使用递归函数结合 try - catch 来实现:

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

Future<String> fetchData(int retryCount) async {
  try {
    final response = await http.get(Uri.parse('https://example.com/api/data'));
    if (response.statusCode == 200) {
      return response.body;
    } else {
      if (retryCount > 0) {
        await Future.delayed(const Duration(seconds: 1));
        return fetchData(retryCount - 1);
      } else {
        throw Exception('Failed to load data after multiple retries');
      }
    }
  } catch (e) {
    if (retryCount > 0) {
      await Future.delayed(const Duration(seconds: 1));
      return fetchData(retryCount - 1);
    } else {
      throw Exception('Failed to load data after multiple retries');
    }
  }
}

在这个示例中,fetchData 函数接收一个 retryCount 参数表示重试次数。如果请求失败,并且重试次数大于 0,函数会等待 1 秒后递归调用自身,减少一次重试次数,直到请求成功或者重试次数用尽。这样可以在一定程度上提升网络请求的稳定性,避免因为偶尔的网络波动或服务器短暂故障导致请求失败。

四、实践中的优化策略

4.1 缓存机制

在网络请求中,有些数据可能不会频繁变化,例如一些配置信息、商品列表的固定部分等。对于这些数据,我们可以使用缓存机制,减少不必要的网络请求,从而提高应用的响应速度和稳定性。

在 Flutter 中,可以使用 shared_preferences 库来实现简单的本地缓存。以下是一个示例:

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

Future<String> fetchData() async {
  final prefs = await SharedPreferences.getInstance();
  String? cachedData = prefs.getString('cached_data');
  if (cachedData != null) {
    return cachedData;
  }

  final response = await http.get(Uri.parse('https://example.com/api/data'));
  if (response.statusCode == 200) {
    String data = response.body;
    prefs.setString('cached_data', data);
    return data;
  } else {
    throw Exception('Failed to load data');
  }
}

在这个示例中,首先尝试从本地缓存中读取数据,如果缓存中有数据则直接返回。如果缓存中没有数据,则发起网络请求,请求成功后将数据存入缓存并返回。这样,在下次请求相同数据时,如果缓存未过期,就可以直接从本地获取,减少了网络请求的次数,提高了稳定性和性能。

4.2 优化请求频率

在一些应用场景中,可能会频繁发起网络请求,例如在一个实时数据监控应用中,可能每秒都需要请求最新的数据。这样频繁的请求不仅会消耗大量的网络流量,还可能导致服务器压力过大,从而影响请求的稳定性。

我们可以通过节流(throttle)和防抖(debounce)技术来优化请求频率。节流是指在一定时间内,只允许执行一次函数。例如,我们可以设置每 5 秒请求一次数据:

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

class ThrottleExample extends StatefulWidget {
  const ThrottleExample({super.key});

  @override
  State<ThrottleExample> createState() => _ThrottleExampleState();
}

class _ThrottleExampleState extends State<ThrottleExample> {
  String _data = '';
  bool _isRequesting = false;

  Future<void> fetchData() async {
    if (_isRequesting) return;
    _isRequesting = true;
    try {
      final response = await http.get(Uri.parse('https://example.com/api/data'));
      if (response.statusCode == 200) {
        setState(() {
          _data = response.body;
        });
      } else {
        throw Exception('Failed to load data');
      }
    } finally {
      _isRequesting = false;
      await Future.delayed(const Duration(seconds: 5));
      fetchData();
    }
  }

  @override
  void initState() {
    super.initState();
    fetchData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Throttle Example'),
      ),
      body: Center(
        child: Text(_data),
      ),
    );
  }
}

在这个示例中,_isRequesting 用于标记是否正在请求数据,如果正在请求则直接返回,避免重复请求。请求完成后,等待 5 秒再次发起请求,从而控制了请求频率。

防抖则是指在一定时间内,如果函数被多次调用,只执行最后一次。例如,在搜索框输入时,用户可能会快速输入多个字符,如果每次输入都发起搜索请求,会导致大量不必要的请求。可以使用防抖技术,只有在用户停止输入一段时间后才发起请求:

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

class DebounceExample extends StatefulWidget {
  const DebounceExample({super.key});

  @override
  State<DebounceExample> createState() => _DebounceExampleState();
}

class _DebounceExampleState extends State<DebounceExample> {
  String _searchText = '';
  String _data = '';
  Timer? _debounceTimer;

  Future<void> fetchData() async {
    final response = await http.get(Uri.parse('https://example.com/api/search?query=$_searchText'));
    if (response.statusCode == 200) {
      setState(() {
        _data = response.body;
      });
    } else {
      throw Exception('Failed to load data');
    }
  }

  void _onSearchChanged(String value) {
    setState(() {
      _searchText = value;
    });
    if (_debounceTimer != null) {
      _debounceTimer!.cancel();
    }
    _debounceTimer = Timer(const Duration(milliseconds: 500), () {
      fetchData();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Debounce Example'),
      ),
      body: Column(
        children: [
          TextField(
            onChanged: _onSearchChanged,
            decoration: const InputDecoration(
              hintText: 'Search',
            ),
          ),
          Expanded(
            child: Text(_data),
          ),
        ],
      ),
    );
  }
}

在这个示例中,每次输入框内容变化时,先取消之前的定时器(如果存在),然后设置一个新的定时器,延迟 500 毫秒后执行 fetchData 函数,这样就避免了频繁请求,提高了网络请求的稳定性。

4.3 网络状态监听

实时监听网络状态对于提升网络请求稳定性非常重要。在 Flutter 中,可以使用 connectivity_plus 库来监听网络状态的变化。例如:

import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

class NetworkStatusExample extends StatefulWidget {
  const NetworkStatusExample({super.key});

  @override
  State<NetworkStatusExample> createState() => _NetworkStatusExampleState();
}

class _NetworkStatusExampleState extends State<NetworkStatusExample> {
  String _networkStatus = 'Unknown';
  String _data = '';

  Future<void> fetchData() async {
    final response = await http.get(Uri.parse('https://example.com/api/data'));
    if (response.statusCode == 200) {
      setState(() {
        _data = response.body;
      });
    } else {
      throw Exception('Failed to load data');
    }
  }

  @override
  void initState() {
    super.initState();
    Connectivity().onConnectivityChanged.listen((ConnectivityResult result) {
      setState(() {
        if (result == ConnectivityResult.wifi) {
          _networkStatus = 'Connected via Wi-Fi';
        } else if (result == ConnectivityResult.mobile) {
          _networkStatus = 'Connected via Mobile Data';
        } else {
          _networkStatus = 'Disconnected';
        }
      });
      if (result != ConnectivityResult.none) {
        fetchData();
      }
    });
    fetchData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Network Status Example'),
      ),
      body: Column(
        children: [
          Text(_networkStatus),
          Expanded(
            child: Text(_data),
          ),
        ],
      ),
    );
  }
}

在这个示例中,通过 Connectivity().onConnectivityChanged.listen 监听网络状态的变化。当网络状态发生改变时,更新 _networkStatus 并根据网络状态决定是否发起网络请求。如果网络连接恢复,自动发起请求获取数据,这样可以在网络不稳定的情况下,及时恢复数据的更新,提升网络请求的稳定性。

五、总结常见问题及解决方案

5.1 内存泄漏问题

在使用异步操作时,如果不注意,可能会出现内存泄漏问题。例如,在一个 StatefulWidget 中发起异步网络请求,当 State 对象被销毁(如页面被关闭)时,如果异步操作还未完成,可能会导致内存泄漏。

解决方案是在 State 对象的 dispose 方法中取消未完成的异步操作。以 dio 库为例:

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

class MemoryLeakExample extends StatefulWidget {
  const MemoryLeakExample({super.key});

  @override
  State<MemoryLeakExample> createState() => _MemoryLeakExampleState();
}

class _MemoryLeakExampleState extends State<MemoryLeakExample> {
  CancelToken _cancelToken = CancelToken();
  String _data = '';

  Future<void> fetchData() async {
    try {
      final dio = Dio();
      final response = await dio.get('https://example.com/api/data', cancelToken: _cancelToken);
      setState(() {
        _data = response.data.toString();
      });
    } catch (e) {
      if (e is DioException && e.type == DioExceptionType.cancel) {
        print('Request cancelled');
      } else {
        throw Exception('Failed to load data');
      }
    }
  }

  @override
  void initState() {
    super.initState();
    fetchData();
  }

  @override
  void dispose() {
    _cancelToken.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Memory Leak Example'),
      ),
      body: Center(
        child: Text(_data),
      ),
    );
  }
}

在这个示例中,创建了一个 CancelToken 并传递给 dio.get 请求。在 dispose 方法中调用 _cancelToken.cancel() 取消未完成的请求,从而避免内存泄漏。

5.2 并发请求问题

在某些情况下,可能需要同时发起多个网络请求。如果不进行合理管理,可能会导致资源竞争、请求过多影响性能等问题。

可以使用 Future.wait 来处理并发请求,它可以等待多个 Future 对象都完成后再继续执行。例如:

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

Future<void> fetchMultipleData() async {
  Future<String> fetchData1() async {
    final response = await http.get(Uri.parse('https://example.com/api/data1'));
    if (response.statusCode == 200) {
      return response.body;
    } else {
      throw Exception('Failed to load data1');
    }
  }

  Future<String> fetchData2() async {
    final response = await http.get(Uri.parse('https://example.com/api/data2'));
    if (response.statusCode == 200) {
      return response.body;
    } else {
      throw Exception('Failed to load data2');
    }
  }

  List<Future<String>> futures = [fetchData1(), fetchData2()];
  List<String> results = await Future.wait(futures);
  print('Data1: ${results[0]}, Data2: ${results[1]}');
}

在这个示例中,通过 Future.wait 等待 fetchData1fetchData2 两个异步操作完成,并获取它们的结果。这样可以有效地管理并发请求,避免一些潜在的问题。

5.3 数据解析问题

网络请求返回的数据通常需要进行解析,例如将 JSON 格式的数据解析为 Dart 对象。如果数据格式不符合预期,可能会导致解析失败。

在解析数据时,应该进行充分的错误处理。以解析 JSON 数据为例:

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

class User {
  final String name;
  final int age;

  User({required this.name, required this.age});

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      name: json['name'] as String,
      age: json['age'] as int,
    );
  }
}

Future<User> fetchUser() async {
  final response = await http.get(Uri.parse('https://example.com/api/user'));
  if (response.statusCode == 200) {
    try {
      Map<String, dynamic> json = jsonDecode(response.body);
      return User.fromJson(json);
    } catch (e) {
      throw Exception('Failed to parse user data');
    }
  } else {
    throw Exception('Failed to load user data');
  }
}

在这个示例中,User.fromJson 方法负责将 JSON 数据解析为 User 对象。在解析过程中,使用 try - catch 捕获可能的解析错误,并抛出相应的异常,这样可以提高网络请求数据处理的稳定性。

通过以上对异步 /await 在 Flutter 网络请求中的应用及相关优化策略和常见问题的探讨,希望能帮助开发者提升 Flutter 应用中网络请求的稳定性,为用户提供更流畅、可靠的使用体验。在实际开发中,还需要根据具体的业务场景和需求,灵活运用这些技术和方法,不断优化应用的网络性能。