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

Flutter 网络请求与 UI 更新的协同处理

2024-11-035.2k 阅读

Flutter 网络请求基础

在 Flutter 中,网络请求是实现与后端服务器交互的关键步骤。最常用的网络请求库是 http 库,它是 Dart 官方提供的用于处理 HTTP 请求的库。首先,在 pubspec.yaml 文件中添加依赖:

dependencies:
  http: ^0.13.5

然后导入库:

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

发送 GET 请求

发送 GET 请求非常简单。假设我们要获取一个 JSON 格式的用户列表,示例代码如下:

Future<List<dynamic>> fetchUsers() async {
  final response = await http.get(Uri.parse('https://example.com/api/users'));
  if (response.statusCode == 200) {
    return (response.body as List).map((e) => e as Map<String, dynamic>).toList();
  } else {
    throw Exception('Failed to load users');
  }
}

这里,http.get 方法接受一个 Uri 对象作为参数,指定请求的 URL。如果请求成功(状态码为 200),我们将响应体解析为 JSON 格式并返回。如果失败,则抛出异常。

发送 POST 请求

发送 POST 请求用于向服务器提交数据。例如,我们要创建一个新用户,示例代码如下:

Future<dynamic> createUser(Map<String, dynamic> userData) async {
  final response = await http.post(
    Uri.parse('https://example.com/api/users'),
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(userData),
  );
  if (response.statusCode == 201) {
    return jsonDecode(response.body);
  } else {
    throw Exception('Failed to create user');
  }
}

在这个例子中,http.post 方法除了 URL 之外,还接受 headersbody 参数。headers 中设置了请求体的格式为 JSON,body 使用 jsonEncode 将用户数据转换为 JSON 字符串。

UI 更新机制

Flutter 的 UI 更新基于 StatefulWidgetStateStatefulWidget 是可以改变状态的部件,而 State 类则管理这些状态。当状态发生变化时,Flutter 会重新构建 StatefulWidgetbuild 方法,从而更新 UI。

简单的状态更新示例

class CounterApp extends StatefulWidget {
  const CounterApp({Key? key}) : super(key: key);

  @override
  _CounterAppState createState() => _CounterAppState();
}

class _CounterAppState extends State<CounterApp> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Counter App'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

在这个示例中,_counter 是一个状态变量。当点击 FloatingActionButton 时,_incrementCounter 方法被调用,setState 方法会通知 Flutter 状态发生了变化,从而触发 build 方法重新构建 UI。

网络请求与 UI 更新的协同处理

网络请求完成后更新 UI

将网络请求与 UI 更新结合起来,通常是在网络请求成功后更新 UI 显示数据。以之前获取用户列表的示例为例,我们在一个 StatefulWidget 中显示用户列表:

class UserListApp extends StatefulWidget {
  const UserListApp({Key? key}) : super(key: key);

  @override
  _UserListAppState createState() => _UserListAppState();
}

class _UserListAppState extends State<UserListApp> {
  List<dynamic> _users = [];
  bool _isLoading = false;

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

  Future<void> _fetchUsers() async {
    setState(() {
      _isLoading = true;
    });
    try {
      final users = await fetchUsers();
      setState(() {
        _users = users;
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _isLoading = false;
      });
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Failed to load users')),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('User List'),
      ),
      body: _isLoading
         ? const Center(child: CircularProgressIndicator())
          : ListView.builder(
              itemCount: _users.length,
              itemBuilder: (context, index) {
                final user = _users[index];
                return ListTile(
                  title: Text(user['name']),
                  subtitle: Text(user['email']),
                );
              },
            ),
    );
  }
}

在这个示例中,_fetchUsers 方法在 initState 中被调用。在请求开始时,_isLoading 被设置为 true,UI 显示加载指示器。请求成功后,_users 被更新,_isLoading 被设置为 false,UI 显示用户列表。如果请求失败,_isLoading 同样被设置为 false,并显示一个 SnackBar 提示用户。

处理网络请求中的错误

在网络请求过程中,可能会遇到各种错误,如网络连接失败、服务器响应错误等。我们需要在代码中妥善处理这些错误,以提供良好的用户体验。

Future<List<dynamic>> fetchUsersWithErrorHandling() async {
  try {
    final response = await http.get(Uri.parse('https://example.com/api/users'));
    if (response.statusCode == 200) {
      return (response.body as List).map((e) => e as Map<String, dynamic>).toList();
    } else {
      throw Exception('Server returned non - 200 status code');
    }
  } on SocketException {
    throw Exception('No internet connection');
  } on FormatException {
    throw Exception('Invalid response format');
  }
}

这里使用 try - catch 块捕获不同类型的异常。SocketException 表示网络连接问题,FormatException 表示响应格式不正确。在 UI 中,我们可以根据不同的异常类型显示不同的提示信息。

网络请求与 UI 更新的性能优化

  1. 避免不必要的 UI 重建 在 Flutter 中,每次调用 setState 都会触发 build 方法重新构建 UI。为了避免不必要的 UI 重建,可以使用 AnimatedBuilderValueListenableBuilder。例如,当我们只需要更新一个小部件的状态而不是整个页面时:
class AnimatedCounterApp extends StatefulWidget {
  const AnimatedCounterApp({Key? key}) : super(key: key);

  @override
  _AnimatedCounterAppState createState() => _AnimatedCounterAppState();
}

class _AnimatedCounterAppState extends State<AnimatedCounterApp>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  int _counter = 0;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _incrementCounter() {
    setState(() {
      _counter++;
      _controller.forward(from: 0.0);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Animated Counter App'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            AnimatedBuilder(
              animation: _controller,
              builder: (context, child) {
                return Transform.scale(
                  scale: 1 + _controller.value,
                  child: child,
                );
              },
              child: Text(
                '$_counter',
                style: Theme.of(context).textTheme.headline4,
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

在这个示例中,AnimatedBuilder 只在 _controller 的动画值发生变化时更新其内部的 Transform.scale 部件,而不会导致整个页面重建。

  1. 缓存网络请求结果 对于一些不经常变化的数据,可以缓存网络请求的结果,避免重复请求。可以使用 SharedPreferences 或内存缓存来实现。以下是一个简单的内存缓存示例:
class CachedUserListApp extends StatefulWidget {
  const CachedUserListApp({Key? key}) : super(key: key);

  @override
  _CachedUserListAppState createState() => _CachedUserListAppState();
}

class _CachedUserListAppState extends State<CachedUserListApp> {
  List<dynamic> _users = [];
  bool _isLoading = false;
  static Map<String, List<dynamic>> _cache = {};

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

  Future<void> _fetchUsers() async {
    if (_cache.containsKey('users')) {
      setState(() {
        _users = _cache['users']!;
      });
      return;
    }
    setState(() {
      _isLoading = true;
    });
    try {
      final users = await fetchUsers();
      setState(() {
        _users = users;
        _cache['users'] = users;
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _isLoading = false;
      });
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Failed to load users')),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Cached User List'),
      ),
      body: _isLoading
         ? const Center(child: CircularProgressIndicator())
          : ListView.builder(
              itemCount: _users.length,
              itemBuilder: (context, index) {
                final user = _users[index];
                return ListTile(
                  title: Text(user['name']),
                  subtitle: Text(user['email']),
                );
              },
            ),
    );
  }
}

在这个示例中,_cache 是一个静态的内存缓存。在请求用户列表之前,先检查缓存中是否已经有数据,如果有则直接使用缓存数据,避免了重复的网络请求。

  1. 使用 Stream 进行异步数据处理 Stream 是 Flutter 中处理异步数据流的一种强大方式。在网络请求与 UI 更新的场景中,Stream 可以用于处理实时数据更新。例如,假设我们有一个服务器推送新用户数据的功能:
class StreamUserListApp extends StatefulWidget {
  const StreamUserListApp({Key? key}) : super(key: key);

  @override
  _StreamUserListAppState createState() => _StreamUserListAppState();
}

class _StreamUserListAppState extends State<StreamUserListApp> {
  final StreamController<List<dynamic>> _streamController =
      StreamController<List<dynamic>>.broadcast();
  List<dynamic> _users = [];
  bool _isLoading = false;

  @override
  void initState() {
    super.initState();
    _fetchInitialUsers();
    _startListeningForNewUsers();
  }

  Future<void> _fetchInitialUsers() async {
    setState(() {
      _isLoading = true;
    });
    try {
      final users = await fetchUsers();
      setState(() {
        _users = users;
        _streamController.add(users);
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _isLoading = false;
      });
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Failed to load users')),
      );
    }
  }

  void _startListeningForNewUsers() {
    // 模拟从服务器获取新用户数据的流
    // 实际应用中,这可能是 WebSocket 或其他实时通信方式
    Future.delayed(const Duration(seconds: 5), () {
      List<dynamic> newUsers = [
        {'name': 'New User 1', 'email': 'newuser1@example.com'},
        {'name': 'New User 2', 'email': 'newuser2@example.com'}
      ];
      _streamController.add(_users..addAll(newUsers));
    });
    _streamController.stream.listen((users) {
      setState(() {
        _users = users;
      });
    });
  }

  @override
  void dispose() {
    _streamController.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Stream User List'),
      ),
      body: _isLoading
         ? const Center(child: CircularProgressIndicator())
          : ListView.builder(
              itemCount: _users.length,
              itemBuilder: (context, index) {
                final user = _users[index];
                return ListTile(
                  title: Text(user['name']),
                  subtitle: Text(user['email']),
                );
              },
            ),
    );
  }
}

在这个示例中,_streamController 用于管理用户数据的流。初始用户数据获取后,通过 _streamController.add 将数据添加到流中。同时,模拟了一个每 5 秒获取新用户数据并添加到流中的操作。_streamController.stream.listen 监听流的变化,当有新数据时,通过 setState 更新 UI。

高级网络请求与 UI 更新技巧

使用 Dio 库进行网络请求

虽然 http 库是官方提供的基础网络请求库,但 Dio 库提供了更多强大的功能,如拦截器、请求重试、FormData 支持等。首先在 pubspec.yaml 中添加依赖:

dependencies:
  dio: ^4.0.0

然后导入库:

import 'package:dio/dio.dart';

以下是使用 Dio 发送 GET 请求的示例:

Future<List<dynamic>> fetchUsersWithDio() async {
  final dio = Dio();
  try {
    final response = await dio.get('https://example.com/api/users');
    if (response.statusCode == 200) {
      return response.data as List<dynamic>;
    } else {
      throw Exception('Failed to load users');
    }
  } catch (e) {
    throw Exception('Error: $e');
  }
}

利用 Provider 管理网络请求状态

Provider 是一个状态管理库,可以方便地在不同部件之间共享网络请求状态。例如,我们可以创建一个 UserProvider 来管理用户列表的获取和状态:

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

class UserProvider with ChangeNotifier {
  List<dynamic> _users = [];
  bool _isLoading = false;
  Dio _dio = Dio();

  List<dynamic> get users => _users;
  bool get isLoading => _isLoading;

  Future<void> fetchUsers() async {
    _isLoading = true;
    notifyListeners();
    try {
      final response = await _dio.get('https://example.com/api/users');
      if (response.statusCode == 200) {
        _users = response.data as List<dynamic>;
      } else {
        throw Exception('Failed to load users');
      }
    } catch (e) {
      // 处理错误
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
}

在 UI 中使用 Provider

class ProviderUserListApp extends StatelessWidget {
  const ProviderUserListApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final userProvider = Provider.of<UserProvider>(context);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Provider User List'),
      ),
      body: userProvider.isLoading
         ? const Center(child: CircularProgressIndicator())
          : ListView.builder(
              itemCount: userProvider.users.length,
              itemBuilder: (context, index) {
                final user = userProvider.users[index];
                return ListTile(
                  title: Text(user['name']),
                  subtitle: Text(user['email']),
                );
              },
            ),
    );
  }
}

在这个示例中,UserProvider 管理用户列表的获取状态和数据。通过 Provider.of<UserProvider>(context) 可以在不同部件中获取 UserProvider 的实例,从而实现状态共享和 UI 更新。

处理网络请求的并发与顺序执行

在实际应用中,可能会遇到需要同时发送多个网络请求或者按顺序发送网络请求的情况。

  1. 并发请求 使用 Future.wait 可以实现并发请求。例如,我们需要同时获取用户列表和用户统计信息:
Future<void> fetchUserData() async {
  final userListFuture = fetchUsers();
  final userStatsFuture = fetchUserStats();
  final results = await Future.wait([userListFuture, userStatsFuture]);
  final users = results[0] as List<dynamic>;
  final stats = results[1] as Map<String, dynamic>;
  // 处理数据
}
  1. 顺序请求 可以使用 async - await 实现顺序请求。例如,先获取用户列表,然后根据用户列表中的某个用户 ID 获取该用户的详细信息:
Future<void> fetchUserDetails() async {
  final users = await fetchUsers();
  if (users.isNotEmpty) {
    final userId = users[0]['id'];
    final userDetails = await fetchUserDetailsById(userId);
    // 处理用户详细信息
  }
}

网络请求与 UI 更新的安全考虑

  1. HTTPS 连接 在进行网络请求时,始终使用 HTTPS 连接以确保数据传输的安全性。大多数现代服务器都支持 HTTPS。在 Flutter 中,http 库和 Dio 库默认支持 HTTPS。
  2. 数据加密 对于敏感数据,如用户密码、银行卡信息等,除了使用 HTTPS 外,还可以在客户端对数据进行加密后再发送。例如,可以使用 encrypt 库进行数据加密:
import 'package:encrypt/encrypt.dart';

void encryptAndSendData(String data) {
  final key = Key.fromLength(32);
  final iv = IV.fromLength(16);
  final encrypter = Encrypter(AES(key));
  final encrypted = encrypter.encrypt(data, iv: iv);
  // 发送加密后的数据
}
  1. 防止 CSRF 攻击 跨站请求伪造(CSRF)攻击是一种常见的网络攻击方式。在服务器端,可以通过设置 CSRF 令牌来防止此类攻击。在 Flutter 客户端,确保在每次请求中正确携带 CSRF 令牌。例如,在使用 http 库时,可以在请求头中添加令牌:
Future<dynamic> sendRequestWithCSRFToken() async {
  final response = await http.post(
    Uri.parse('https://example.com/api/action'),
    headers: <String, String>{
      'Content - Type': 'application/json; charset=UTF - 8',
      'X - CSRF - Token': 'your_csrf_token'
    },
    body: jsonEncode({'data': 'example'}),
  );
  if (response.statusCode == 200) {
    return jsonDecode(response.body);
  } else {
    throw Exception('Failed to send request');
  }
}

通过以上全面的介绍,我们深入了解了 Flutter 中网络请求与 UI 更新的协同处理,包括基础的网络请求方法、UI 更新机制、两者的协同方式、性能优化、高级技巧以及安全考虑等方面。这些知识将有助于开发者构建出高效、稳定且安全的 Flutter 应用程序。