学习目标:

这里和大家一起来学习针对鸿蒙系统如何使用Flutter框架进行服务器数据的网络请求并渲染到页面中。


Flutter网络请求库 Dio

Dio 是 Flutter 中一个强大的网络请求库,支持 RESTful API、文件上传/下载、拦截器、请求取消等功能。相比原生 http 包,Dio 提供了更简洁的 API 和更丰富的特性。

1. 添加依赖

~/pubspec.yaml 项目根目录下添加dio依赖

  dio: ^5.4.0

在这里插入图片描述

2. 创建dio服务类

项目很多地方都需要用到网络请求,故创建了一个全局单例的服务类,专门处理数据请求的。

import 'package:dio/dio.dart';
import 'dart:convert';

/// API 服务类
/// 用于处理网络请求和数据获取
class ApiService {
  // 单例模式
  static final ApiService _instance = ApiService._internal();
  factory ApiService() => _instance;
  ApiService._internal();

  // Dio 实例
  late final Dio _dio;

  // 静态资源接口地址
  static const String baseUrl = 'https://hanfucn.com';
  static const String mockDataPath = '/assets/mockData-DfdWDX7N.js';

  /// 初始化 Dio
  void init() {
    _dio = Dio(BaseOptions(
      baseUrl: baseUrl,
      connectTimeout: const Duration(seconds: 10),
      receiveTimeout: const Duration(seconds: 10),
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json, text/javascript, */*',
      },
    ));

    // 添加拦截器(可选)
    _dio.interceptors.add(LogInterceptor(
      requestBody: true,
      responseBody: true,
      error: true,
    ));
  }

  /// 获取 Dio 实例
  Dio get dio => _dio;
 }

数据接口这里从一个远程js文件中获取数据,除了获取数据外,还多了一步解析数据, 额外需要一个方法来处理从 JavaScript 文件中提取 JSON 数据

/// 获取 Mock 数据
  /// 从 JavaScript 文件中提取 JSON 数据
  /// 
  /// 返回解析后的 JSON 数据(Map 或 List)
  /// 如果请求失败,抛出 DioException
  Future<dynamic> getMockData() async {
    try {
      final response = await _dio.get(mockDataPath);

      if (response.statusCode == 200) {
        final data = response.data;
        
        // 如果返回的是字符串(JavaScript 文件)
        if (data is String) {
          // 尝试提取 JSON 数据
          // 方法1: 如果文件包含 JSON 对象,尝试直接解析
          try {
            // 查找 JSON 对象(可能是 export default {...} 或 module.exports = {...})
            final jsonMatch = RegExp(r'\{[\s\S]*\}').firstMatch(data);
            if (jsonMatch != null) {
              final jsonString = jsonMatch.group(0);
              return json.decode(jsonString!);
            }
            
            // 方法2: 如果包含 JSON 数组
            final arrayMatch = RegExp(r'\[[\s\S]*\]').firstMatch(data);
            if (arrayMatch != null) {
              final jsonString = arrayMatch.group(0);
              return json.decode(jsonString!);
            }
            
            // 方法3: 尝试直接解析整个字符串
            return json.decode(data);
          } catch (e) {
            // 如果解析失败,返回原始字符串
            return data;
          }
        }
        
        // 如果已经是 Map 或 List,直接返回
        return data;
      } else {
        throw DioException(
          requestOptions: response.requestOptions,
          response: response,
          type: DioExceptionType.badResponse,
          message: '请求失败,状态码: ${response.statusCode}',
        );
      }
    } on DioException catch (e) {
      // 处理网络错误
      throw _handleError(e);
    } catch (e) {
      // 处理其他错误
      throw Exception('获取数据失败: $e');
    }
  }

扩展GET和POST通用接口

/// 通用 GET 请求
  Future<dynamic> get(
    String path, {
    Map<String, dynamic>? queryParameters,
    Options? options,
  }) async {
    try {
      final response = await _dio.get(
        path,
        queryParameters: queryParameters,
        options: options,
      );
      return response.data;
    } on DioException catch (e) {
      throw _handleError(e);
    }
  }
/// 通用 POST 请求
  Future<dynamic> post(
    String path, {
    dynamic data,
    Map<String, dynamic>? queryParameters,
    Options? options,
  }) async {
    try {
      final response = await _dio.post(
        path,
        data: data,
        queryParameters: queryParameters,
        options: options,
      );
      return response.data;
    } on DioException catch (e) {
      throw _handleError(e);
    }
  }

另外还有一步比较重要的就是错误处理,针对服务端返回的错误信息,进行页面本地化状态显示

/// 错误处理
  DioException _handleError(DioException error) {
    String message = '请求失败';

    // 根据 HTTP 状态码进行错误匹配
    final statusCode = error.response?.statusCode;

    if (statusCode != null) {
      if (statusCode >= 400 && statusCode < 500) {
        // 客户端错误
        message = '请求错误: $statusCode';
      } else if (statusCode >= 500 && statusCode < 600) {
        // 服务端错误
        message = '服务器错误: $statusCode';
      }
    } else {
      // 如果没有状态码则根据 Dio 的异常类型判断
      switch (error.type) {
        case DioExceptionType.connectionTimeout:
        case DioExceptionType.sendTimeout:
        case DioExceptionType.receiveTimeout:
          message = '连接超时,请检查网络';
          break;
        case DioExceptionType.cancel:
          message = '请求已取消';
          break;
        case DioExceptionType.unknown:
          message = '网络连接失败,请检查网络设置';
          break;
        default:
          message = error.message ?? '未知错误';
      }
    }
    
    return DioException(
      requestOptions: error.requestOptions,
      response: error.response,
      type: error.type,
      message: message,
    );
  }

不同的statusCode对应不同的提示,需要给用户看得懂的提示,这里简单定义了几种状态码,后续可以新增。如token过期等等。

数据请求处理

在这里插入图片描述
这是我要的整个数据链,从js中获取到的数据提取图片组成新的数组,我只需要图片,然后页面中显示这些不同的图片,类似抖音图片上下滑动,如果是自己写接口,就后端直接按需要的格式返回数据格式就行。

页面

图片加载这里用到另外一个框架,cached_network_image
所以需要再pubspec.yaml 中新增依赖
在这里插入图片描述

页面大概的代码结构
在这里插入图片描述

使用了flutter的动态组件StatefulWidget ,首先拆解下这个类似抖音的页面有哪些东西

  1. 有状态的、动态的widget,类似控制器 build
  2. 要用到请求服务类去获取数据并解析,_loadData
  3. 页面构建 _buildBody
  4. 单个图片视图构建 _buildImagePage
  5. 然后数据中图片在视图的渲染
  • 页面初始化过程:
1. HomePage Widget 被创建
   ↓
2. _HomePageState 被创建
   ↓
3. initState() 被调用
   ├─→ super.initState()
   ├─→ _initializeApi()          // 初始化 API 服务
   │   └─→ _apiService.init()     // 配置 Dio 实例
   └─→ _loadData()               // 开始异步加载数据
  • 页面数据流如下:
_loadData() 执行流程:
   ↓
1. setState() - 设置加载状态
   ├─→ _isLoading = true
   └─→ _errorMessage = null2. 触发 build() 重建(显示加载指示器)
   ↓
3. 异步执行数据获取:
   ├─→ _apiService.getMockData()           // 网络请求获取 JS 数据
   ├─→ DataParser.parseWorksAndImages()     // 解析数据并提取图片
   └─→ 获取 allImages 数组
   ↓
4. setState() - 更新数据状态
   ├─→ _images = allImages
   └─→ _isLoading = false5. 触发 build() 重建(显示图片列表)
  • 渲染过程
build() 被调用
   ↓
_buildBody() 被调用
   ↓
根据状态判断:
   ├─→ 如果 _isLoading == true
   │   └─→ 返回 CircularProgressIndicator(加载指示器)
   │
   ├─→ 如果 _errorMessage != null
   │   └─→ 返回错误提示界面(带重试按钮)
   │
   ├─→ 如果 _images.isEmpty
   │   └─→ 返回"暂无数据"提示
   │
   └─→ 如果数据正常
       └─→ 返回 PageView.builder
           ├─→ itemBuilder 被调用(为每个图片创建页面)
           │   └─→ _buildImagePage(imageUrl, index) 被调用
           │       ├─→ 创建 Stack 布局
           │       ├─→ 添加 CachedNetworkImage(图片组件)
           │       ├─→ 添加渐变遮罩(底部)
           │       └─→ 添加页码指示器(右上角)
           │
           └─→ onPageChanged 回调(当页面切换时)
               └─→ setState() 更新 _currentIndex

这样就构成了一个看起来效果非常好的页面了,然后页面上下滚动是依靠了Column组件的能力,这是一个垂直排列,当内容超出容器高度后,直接上下滚动的这么一个组件,与之对应的是水平滚动组件Row,更详细的组件介绍可以到https://flutter.dev 上学习。

编译和运行
  • 拉取依赖
    编译前需要拉取下依赖,执行以下命令
    flutter pub get

在这里插入图片描述

在这里插入图片描述

  • 配置调试签名
    这里有个报错,就是第一次需要通过DevEco Studio打开ohos工程后配置调试签名(File -> Project Structure -> Signing Configs 勾选Automatically generate signature),按照描述进行操作。
    在这里插入图片描述
    勾选Automatically generate signature后,点击Apply 然后ok。

这里配置遇到问题,就是勾选了好多次, DevEco Studio 的配置没有写入build-profile.json5,依然在/ohos/build-profile.json5 中看不到签名配置,不清楚是不是IED的问题还是,之前配置都ok的, 后来想了个办法将其他项目中的build-profile.json5 内容拷贝过来才正常了。

运行正常,App也如愿启动成功,但也遇到了新的问题,启动后App一直图片不显示,无法加载,最终试着把CachedNetworkImage 改成 Image.network 后也不行,后来还是直接使用dio下载图片才解决了图片显示的问题,
参考network_image_widget.dart

Future<void> _loadImage() async {
    try {
      print('开始下载图片: ${widget.imageUrl}');
      
      final dio = Dio();
      final response = await dio.get<Uint8List>(
        widget.imageUrl,
        options: Options(
          responseType: ResponseType.bytes,
          headers: {
            'User-Agent': 'Mozilla/5.0 (Linux; HarmonyOS) AppleWebKit/537.36',
            'Accept': 'image/webp,image/apng,image/*,*/*;q=0.8',
          },
          receiveTimeout: const Duration(seconds: 30),
          sendTimeout: const Duration(seconds: 30),
        ),
      );

      if (response.data != null) {
        print('图片下载成功: ${widget.imageUrl}, 大小: ${response.data!.length} bytes');
        setState(() {
          _imageData = response.data;
          _isLoading = false;
        });
      } else {
        throw Exception('图片数据为空');
      }
    } catch (e) {
      print('图片下载失败: ${widget.imageUrl}, 错误: $e');
      setState(() {
        _error = e.toString();
        _isLoading = false;
      });
    }
  }

总算是达到预期效果了,可上下滑动翻页,每页图片全屏展示了。
在这里插入图片描述

项目中已隐藏接口域名,需要下载使用建议更换成其他数据接口,修改下数据解析代码才能使用。

结束语

本文主要是边学习边整理思路,感谢阅读本帖,如对贴中内容有意见和建议的,欢迎与我联系交流,也欢迎加入开源鸿蒙跨平台社区:
https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐