Flutter+开源鸿蒙实战|智联邻里Day7 图片选择(image_picker)+网络图片缓存(cached_network_image)+GetX状态管理彻底封装

欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net

前置说明

Day7 继续全面深化第三方库实战,新增2个高频企业级第三方库:image_picker(图片选择)、cached_network_image(网络图片缓存),同时彻底用 GetX 状态管理替代所有 setState,重构闲置发布、列表渲染逻辑,全程适配鸿蒙手机/平板/DAYU200开发板,详细讲解每个第三方库的集成、用法、适配细节,贴合毕设、比赛规范,代码可直接复用。

<!-- Schema.org 结构化数据 -->
<script type="application/ld+json">
{
  "@context":"https://schema.org",
  "@type":"BlogPosting",
  "headline":"Flutter+开源鸿蒙实战 智联邻里Day7 image_picker图片选择+cached_network_image缓存+GetX状态管理封装",
  "author":{"@type":"Person","name":"鸿蒙跨端开发者"},
  "publisher":{"@type":"Organization","name":"CSDN开源鸿蒙跨平台社区"},
  "datePublished":"2026-05-06",
  "description":"智联邻里Day7 引入第三方库:image_picker相册/相机选择图片、cached_network_image网络图片缓存,彻底封装GetX状态管理,完善闲置发布带图功能,鸿蒙多端适配",
  "keywords":"Flutter,开源鸿蒙,OpenHarmony,智联邻里Day7,image_picker,cached_network_image,GetX状态管理,图片选择,图片缓存"
}
</script>

一、前言

哈喽小伙伴们,智联邻里Day7正式上线!

Day6 我们完成了核心第三方库的接入:用 GetX 重构了项目架构和路由、用 flutter_easy_refresh 升级了下拉刷新、用 fluttertoast 简化了弹窗、用 connectivity_plus 实现了网络监听,项目已经具备企业级APP的基础框架。

今天 Day7,我们聚焦 “图片相关功能+GetX状态管理深化”,继续引入2个高频第三方库,同时彻底抛弃繁琐的 setState,让项目架构更清晰、性能更优,具体核心目标:

  1. 引入 image_picker 第三方库:实现鸿蒙设备相册选择、相机拍摄图片,完善闲置发布“带图上传”功能(模拟上传,贴合真实业务);
  2. 引入 cached_network_image 第三方库:实现网络图片缓存,优化闲置列表、详情页图片加载速度,解决鸿蒙弱网环境下图片加载卡顿问题;
  3. 彻底封装 GetX 状态管理:新建闲置数据控制器(IdleController),统一管理闲置数据的查询、新增、删除,替代所有页面的 setState,实现局部刷新;
  4. 完善鸿蒙多端适配:优化图片选择弹窗、图片展示布局,适配平板/开发板的图片尺寸,解决图片拉伸、触控不灵敏问题;
  5. 整合所有第三方库:将 Day6-7 引入的库与原有库联动,形成完整的“状态管理+图片处理+网络监听+路由跳转”生态,让项目更规范、更易维护。
    在这里插入图片描述

全程依旧遵循“第三方库为主、原生为辅”,每一个库都会详细讲解:为什么用、怎么集成、核心代码、适配坑点,新手既能快速抄代码,也能理解背后的业务逻辑和技术选型,完美适配毕设、课程设计、项目结题的需求。

二、Day7 新增第三方库说明

本次 Day7 新增2个图片相关的第三方库,加上之前已有的,项目第三方库生态完全成型,覆盖企业级APP常用能力:

第三方库 作用 核心优势 适配鸿蒙说明
image_picker 相册选择图片、相机拍摄图片 支持多图选择、图片压缩、尺寸裁剪,调用系统原生相册/相机,适配鸿蒙所有设备 无需额外配置,直接调用,支持鸿蒙手机相机权限适配
cached_network_image 网络图片缓存、占位图、错误图处理 缓存图片到本地,下次加载无需重新请求,优化弱网体验,支持自定义占位样式 自动适配鸿蒙本地存储,缓存路径符合鸿蒙系统规范

原有保留库(继续深化使用):

  • getx:重点用于状态管理封装,替代 setState,统一数据管理
  • flutter_easy_refresh:下拉刷新/上拉加载继续保留,联动 GetX 状态刷新
  • fluttertoast:弹窗提示继续使用,新增图片选择相关提示
  • connectivity_plus:网络监听继续启用,图片选择前先判断网络
  • flutter_screenutil:屏幕适配,确保图片尺寸在多设备上协调
  • dio:模拟图片上传接口,贴合真实业务流程
  • shared_preferences:本地缓存图片路径(模拟上传后的地址)

三、版块1:pubspec.yaml 新增依赖库(图片相关)

打开 pubspec.yaml,在原有依赖基础上,新增2个图片相关第三方库,粘贴完整依赖(可直接复制使用):

dependencies:
  flutter:
    sdk: flutter
  flutter_screenutil: ^5.9.0
  dio: ^5.4.0
  shared_preferences: ^2.2.2

  # Day6 引入的第三方库
  getx: ^4.6.55
  flutter_easy_refresh: ^3.4.0
  fluttertoast: ^8.2.2
  connectivity_plus: ^5.0.1

  # Day7 新增图片相关第三方库
  image_picker: ^1.1.1
  cached_network_image: ^3.3.0

终端执行安装命令,确保所有库正常引入:

flutter pub get

关键补充:鸿蒙设备权限配置(必做)

image_picker 需要调用鸿蒙设备的相册、相机权限,否则无法正常使用,需在项目中添加权限配置:

  1. 打开 android/app/src/main/AndroidManifest.xml,添加以下权限(鸿蒙Android兼容模式必备):
<!-- 相册权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- 相机权限 -->
<uses-permission android:name="android.permission.CAMERA"/>
  1. 鸿蒙原生模式(可选,适配DAYU200开发板):打开 ohos/main_pages.json,添加权限声明:
"abilities": [
  {
    "name": ".MainAbility",
    "permissions": [
      {
        "name": "ohos.permission.READ_MEDIA",
        "reason": "需要访问相册选择图片",
        "usedScene": {
          "abilities": [".MainAbility"],
          "when": "always"
        }
      },
      {
        "name": "ohos.permission.CAMERA",
        "reason": "需要使用相机拍摄图片",
        "usedScene": {
          "abilities": [".MainAbility"],
          "when": "always"
        }
      }
    ]
  }
]

四、版块2:集成 image_picker 实现图片选择(第三方库实战)

4.1 为什么用 image_picker?

原生实现图片选择需要单独适配相册、相机,代码繁琐,且无法统一处理图片压缩、裁剪;image_picker 第三方库封装了所有逻辑,一行代码即可调用系统相册/相机,支持多图选择、图片压缩,完美适配鸿蒙手机、平板、DAYU200开发板,是企业级APP图片选择的首选库。

4.2 封装图片选择工具类(全局复用)

新建 lib/utils/image_util.dart,封装图片选择、压缩、预览相关方法,方便全局调用,核心代码(附带详细注释):

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:zhilian_linli/utils/net_util.dart';

// 图片选择工具类(基于image_picker第三方库)
class ImageUtil {
  // 初始化image_picker实例
  static final ImagePicker _picker = ImagePicker();

  // 选择图片(支持相册/相机)
  static Future<XFile?> pickImage({required ImageSource source}) async {
    // 先判断网络(模拟图片上传,无网络提示)
    bool hasNet = await NetUtil.isNetworkAvailable();
    if (!hasNet) return null;

    try {
      // 调用image_picker选择图片,压缩图片尺寸(避免图片过大)
      final XFile? image = await _picker.pickImage(
        source: source,
        imageQuality: 80, // 图片质量(0-100),压缩后减少内存占用
        maxWidth: 800, // 最大宽度,适配鸿蒙多设备
        maxHeight: 800, // 最大高度
      );

      if (image != null) {
        // 选择成功,返回图片文件
        return image;
      } else {
        // 取消选择
        Fluttertoast.showToast(msg: "已取消图片选择");
        return null;
      }
    } catch (e) {
      // 异常处理(权限不足、设备不支持等)
      Fluttertoast.showToast(msg: "图片选择失败,请检查权限");
      return null;
    }
  }

  // 预览图片(点击图片放大查看)
  static void previewImage(BuildContext context, String imageUrl) {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (ctx) => Scaffold(
          backgroundColor: Colors.black,
          body: Center(
            child: GestureDetector(
              onTap: () => Navigator.pop(ctx),
              child: Image.network(imageUrl),
            ),
          ),
        ),
      ),
    );
  }
}

4.3 改造闲置发布表单(新增图片选择功能)

打开 idle_publish_page.dart,新增图片选择入口,实现“选择图片→预览图片→模拟上传”的流程,核心代码修改(基于GetX,抛弃setState):

import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:zhilian_linli/utils/image_util.dart';
import 'package:zhilian_linli/controller/idle_controller.dart'; // 后续新建的GetX控制器

class IdlePublishPage extends StatelessWidget {
  // 绑定GetX控制器(全局状态管理)
  final IdleController idleController = Get.find<IdleController>();
  // 定义选中的图片(XFile类型,image_picker返回值)
  XFile? selectedImage;

  IdlePublishPage({super.key});

  
  Widget build(BuildContext context) {
    final isLargeScreen = MediaQuery.of(context).size.width >= 600;
    return Scaffold(
      appBar: AppBar(title: const Text("发布闲置物品")),
      body: Padding(
        padding: EdgeInsets.all(16.w),
        child: Column(
          children: [
            // 新增:图片选择区域(基于image_picker)
            GestureDetector(
              onTap: () => _showImagePicker(context),
              child: Container(
                width: double.infinity,
                height: isLargeScreen ? 200.h : 150.h,
                decoration: BoxDecoration(
                  border: Border.all(color: Colors.grey[200]),
                  borderRadius: BorderRadius.circular(8.r),
                  color: Colors.grey[50],
                ),
                alignment: Alignment.center,
                child: Obx(() {
                  // 用GetX Obx实现局部刷新(只刷新图片区域)
                  return idleController.selectedImageUrl.value.isEmpty
                      ? Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Icon(Icons.add_photo_alternate, size: isLargeScreen ? 40.sp : 32.sp, color: Colors.grey),
                            SizedBox(height: 8.h),
                            Text("点击选择图片(可选)", style: TextStyle(fontSize: 13.sp, color: Colors.grey)),
                          ],
                        )
                      : // 预览已选择的图片
                        CachedNetworkImage(
                            imageUrl: idleController.selectedImageUrl.value,
                            fit: BoxFit.cover,
                            width: double.infinity,
                            height: double.infinity,
                            // 占位图(cached_network_image特性)
                            placeholder: (context, url) => const Center(child: CircularProgressIndicator()),
                            // 错误图(cached_network_image特性)
                            errorWidget: (context, url, error) => const Icon(Icons.error, color: Colors.red),
                          );
                }),
              ),
            ),
            SizedBox(height: 16.h),

            // 原有输入框(标题、描述、联系方式)不变,改用GetX控制器管理输入
            CustomInput(
              hintText: "请输入闲置物品标题",
              controller: idleController.titleController,
            ),
            SizedBox(height: 16.h),
            CustomInput(
              hintText: "请输入闲置物品描述",
              controller: idleController.descController,
              maxLines: 3,
            ),
            SizedBox(height: 16.h),
            CustomInput(
              hintText: "请输入联系方式",
              controller: idleController.contactController,
            ),
            SizedBox(height: 30.h),

            // 发布按钮(调用GetX控制器的发布方法)
            CustomButton(
              text: "发布闲置",
              onTap: () => idleController.publishIdle(),
            ),
          ],
        ),
      ),
    );
  }

  // 弹出图片选择弹窗(相册/相机)
  void _showImagePicker(BuildContext context) {
    showModalBottomSheet(
      context: context,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.r)),
      builder: (ctx) {
        return SafeArea(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              ListTile(
                leading: const Icon(Icons.photo_library),
                title: const Text("从相册选择"),
                onTap: () async {
                  // 调用ImageUtil工具类,选择相册图片
                  XFile? image = await ImageUtil.pickImage(source: ImageSource.gallery);
                  if (image != null) {
                    // 模拟图片上传,获取图片URL(真实项目替换为后端接口)
                    String imageUrl = await _mockUploadImage(image);
                    // 用GetX更新选中图片URL(局部刷新)
                    Get.find<IdleController>().selectedImageUrl.value = imageUrl;
                  }
                  Navigator.pop(ctx);
                },
              ),
              ListTile(
                leading: const Icon(Icons.camera_alt),
                title: const Text("相机拍摄"),
                onTap: () async {
                  // 调用ImageUtil工具类,相机拍摄图片
                  XFile? image = await ImageUtil.pickImage(source: ImageSource.camera);
                  if (image != null) {
                    String imageUrl = await _mockUploadImage(image);
                    Get.find<IdleController>().selectedImageUrl.value = imageUrl;
                  }
                  Navigator.pop(ctx);
                },
              ),
              ListTile(
                leading: const Icon(Icons.cancel),
                title: const Text("取消"),
                onTap: () => Navigator.pop(ctx),
              ),
            ],
          ),
        );
      },
    );
  }

  // 模拟图片上传(调用dio第三方库,模拟接口请求)
  Future<String> _mockUploadImage(XFile image) async {
    // 模拟接口延迟
    await Future.delayed(const Duration(milliseconds: 800));
    // 模拟上传成功,返回图片URL(真实项目替换为后端返回的地址)
    return "https://xxx/mock/image/${DateTime.now().millisecondsSinceEpoch}.jpg";
  }
}

4.4 鸿蒙适配细节(重点)

  1. 权限适配:前面已配置相册、相机权限,鸿蒙设备首次调用时会弹出权限申请弹窗,用户允许后才能正常使用,避免权限不足导致崩溃;
  2. 尺寸适配:图片选择区域的高度根据设备尺寸动态调整,大屏(平板/开发板)放大图片区域,确保触控区域充足;
  3. 兼容性适配:image_picker 自动适配鸿蒙Android兼容模式和原生模式,DAYU200开发板可正常调用相机、相册,无需额外修改代码;
  4. 弱网适配:图片选择前先调用 connectivity_plus 判断网络,无网络时弹出Toast提示,避免用户选择图片后无法上传。

五、版块3:集成 cached_network_image 实现图片缓存(第三方库优化)

5.1 为什么用 cached_network_image?

原生 Image.network 每次加载图片都需要重新请求网络,鸿蒙弱网环境下加载缓慢、容易卡顿,且会消耗更多流量;cached_network_image 第三方库会将图片缓存到本地,下次加载时直接从本地读取,无需重新请求,同时支持占位图、错误图、淡入动画,极大提升用户体验,是商业APP图片展示的必备库。

5.2 改造闲置列表和详情页(图片缓存+预览)

5.2.1 改造邻里互助列表(neighbor_page.dart)

cached_network_image 替换原生图片组件,实现列表图片缓存,核心代码修改(基于GetX):

import 'package:get/get.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:zhilian_linli/controller/idle_controller.dart';
import 'package:zhilian_linli/utils/image_util.dart';

class NeighborPage extends StatelessWidget {
  // 绑定GetX控制器
  final IdleController idleController = Get.find<IdleController>();

  NeighborPage({super.key});

  
  Widget build(BuildContext context) {
    final isLargeScreen = MediaQuery.of(context).size.width >= 600;
    return Scaffold(
      appBar: AppBar(title: const Text("邻里互助·闲置共享")),
      body: Padding(
        padding: EdgeInsets.all(16.w),
        child: Column(
          children: [
            // 搜索栏(不变)
            CustomInput(hintText: "搜索闲置物品(如家电、书籍)"),
            SizedBox(height: 16.h),

            // 闲置列表(用GetX Obx监听数据变化,局部刷新)
            Expanded(
              child: Obx(() {
                return EasyRefresh(
                  controller: idleController.refreshController,
                  onRefresh: () async => idleController.getIdleFromCache(),
                  onLoad: () async => idleController.loadMoreIdle(), // 上拉加载(新增)
                  header: const ClassicHeader(),
                  child: isLargeScreen
                      ? GridView.count(
                          crossAxisCount: 2,
                          crossAxisSpacing: 16.w,
                          mainAxisSpacing: 16.h,
                          childAspectRatio: 1.5,
                          children: idleController.idleList.map((idle) {
                            return _buildIdleItem(idle);
                          }).toList(),
                        )
                      : ListView.builder(
                          itemCount: idleController.idleList.length,
                          itemBuilder: (ctx, index) {
                            return _buildIdleItem(idleController.idleList[index]);
                          },
                        ),
                );
              }),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => Get.toNamed(Routes.idlePublish),
        backgroundColor: const Color(0xFF2E8B57),
        child: const Icon(Icons.add),
      ),
    );
  }

  // 封装闲置卡片(新增图片展示,用cached_network_image)
  Widget _buildIdleItem(IdleModel idle) {
    final isLargeScreen = MediaQuery.of(context).size.width >= 600;
    return GestureDetector(
      onTap: () => Get.toNamed(Routes.idleDetail, arguments: idle),
      child: Container(
        padding: EdgeInsets.all(12.w),
        decoration: BoxDecoration(
          border: Border.all(color: Colors.grey[200]),
          borderRadius: BorderRadius.circular(8.r),
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 新增:闲置图片(cached_network_image缓存)
            if (idle.imageUrl.isNotEmpty)
              GestureDetector(
                onTap: () => ImageUtil.previewImage(context, idle.imageUrl), // 预览图片
                child: Container(
                  width: double.infinity,
                  height: isLargeScreen ? 80.h : 60.h,
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(6.r),
                    color: Colors.grey[100],
                  ),
                  child: CachedNetworkImage(
                    imageUrl: idle.imageUrl,
                    fit: BoxFit.cover,
                    placeholder: (context, url) => const Center(child: CircularProgressIndicator()),
                    errorWidget: (context, url, error) => const Icon(Icons.error, color: Colors.red),
                  ),
                ),
              ),
            if (idle.imageUrl.isNotEmpty) SizedBox(height: 8.h),

            // 原有标题、描述、时间不变
            Text(
              idle.title,
              style: TextStyle(fontSize: 15.sp, fontWeight: FontWeight.bold),
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
            ),
            SizedBox(height: 8.h),
            Text(
              idle.description,
              style: TextStyle(fontSize: 13.sp, color: Colors.grey[600]),
              maxLines: 2,
              overflow: TextOverflow.ellipsis,
            ),
            SizedBox(height: 8.h),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(idle.time, style: TextStyle(fontSize: 11.sp, color: Colors.grey)),
                Text("联系我", style: TextStyle(fontSize: 11.sp, color: const Color(0xFF2E8B57))),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
5.2.2 改造闲置详情页(idle_detail_page.dart)

完善详情页图片展示,支持大图预览,核心代码修改:

import 'package:cached_network_image/cached_network_image.dart';
import 'package:zhilian_linli/utils/image_util.dart';

class IdleDetailPage extends StatelessWidget {
  const IdleDetailPage({super.key});

  
  Widget build(BuildContext context) {
    final IdleModel idle = Get.arguments;
    final isLargeScreen = MediaQuery.of(context).size.width >= 600;

    return Scaffold(
      appBar: AppBar(
        title: const Text("闲置详情"),
        leading: IconButton(
          icon: const Icon(Icons.arrow_back),
          onPressed: () => Get.back(),
        ),
        actions: [
          IconButton(
            icon: const Icon(Icons.delete, color: Colors.red),
            onPressed: () => Get.find<IdleController>().deleteIdle(idle.id),
          ),
        ],
      ),
      body: Padding(
        padding: EdgeInsets.all(isLargeScreen ? 24.w : 16.w),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 新增:详情页图片(大图,支持预览)
            if (idle.imageUrl.isNotEmpty)
              GestureDetector(
                onTap: () => ImageUtil.previewImage(context, idle.imageUrl),
                child: Container(
                  width: double.infinity,
                  height: isLargeScreen ? 300.h : 200.h,
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(12.r),
                    color: Colors.grey[100],
                  ),
                  child: CachedNetworkImage(
                    imageUrl: idle.imageUrl,
                    fit: BoxFit.cover,
                    placeholder: (context, url) => const Center(child: CircularProgressIndicator()),
                    errorWidget: (context, url, error) => const Icon(Icons.error, color: Colors.red, size: 40),
                  ),
                ),
              ),
            if (idle.imageUrl.isNotEmpty) SizedBox(height: 16.h),

            // 原有标题、时间、描述、联系方式不变
            Text(
              idle.title,
              style: TextStyle(fontSize: isLargeScreen ? 18.sp : 16.sp, fontWeight: FontWeight.bold),
            ),
            SizedBox(height: 16.h),
            Text(
              "发布时间:${idle.time}",
              style: TextStyle(fontSize: isLargeScreen ? 14.sp : 12.sp, color: Colors.grey),
            ),
            SizedBox(height: 20.h),
            Text(
              "物品描述:",
              style: TextStyle(fontSize: isLargeScreen ? 15.sp : 14.sp, fontWeight: FontWeight.w500),
            ),
            SizedBox(height: 8.h),
            Text(
              idle.description,
              style: TextStyle(fontSize: isLargeScreen ? 14.sp : 13.sp, color: Colors.grey[700], height: 1.5),
            ),
            SizedBox(height: 20.h),
            Container(
              padding: EdgeInsets.all(12.w),
              decoration: BoxDecoration(
                color: Colors.grey[100],
                borderRadius: BorderRadius.circular(8.r),
              ),
              child: Row(
                children: [
                  Icon(Icons.contact_phone, color: const Color(0xFF2E8B57), size: isLargeScreen ? 20.sp : 18.sp),
                  SizedBox(width: 8.w),
                  Text(
                    "联系方式:${idle.contact}",
                    style: TextStyle(fontSize: isLargeScreen ? 14.sp : 13.sp, fontWeight: FontWeight.w500),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

5.3 关键细节讲解(图片缓存+适配)

  1. 缓存逻辑:cached_network_image 会自动将图片缓存到鸿蒙设备本地,默认缓存时间较长,下次打开APP时,图片会直接从本地加载,无需重新请求,极大优化弱网体验;
  2. 占位图/错误图:通过 placeholdererrorWidget 属性,实现图片加载中的占位动画和加载失败的错误提示,提升用户体验,避免页面空白;
  3. 图片预览:结合 ImageUtil.previewImage 方法,实现“点击图片放大预览”,贴合真实APP用法,适配鸿蒙多设备的触控操作;
  4. 鸿蒙适配:缓存路径符合鸿蒙系统规范,不会出现缓存文件无法读取的问题;平板/开发板上,图片尺寸自动适配屏幕比例,避免拉伸、变形。

六、版块4:彻底封装 GetX 状态管理(替代所有 setState)

6.1 为什么要封装 GetX 控制器?

前面 Day6 我们引入了 GetX,但只是用了路由功能;今天我们彻底封装 GetX 状态管理,将所有闲置数据的查询、新增、删除、刷新逻辑,统一放到 IdleController 中,替代所有页面的 setState,实现:

  1. 数据全局共享,多个页面(列表页、详情页、发布页)可共用同一套数据;
  2. 局部刷新,只刷新需要更新的组件,避免整页重建,提升性能;
  3. 代码解耦,页面只负责展示,业务逻辑全部放到控制器中,便于维护和扩展;
  4. 无需 context,任意位置可调用控制器方法,简化代码。

6.2 新建 GetX 控制器(IdleController)

新建 lib/controller/idle_controller.dart,封装所有闲置相关的业务逻辑,核心代码(附带详细注释):

import 'package:get/get.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:flutter_easy_refresh/flutter_easy_refresh.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:zhilian_linli/models/idle_model.dart';
import 'package:zhilian_linli/utils/net_util.dart';
import 'package:zhilian_linli/routes/routes.dart';
import 'dart:convert';

// GetX控制器,继承GetxController,管理闲置数据
class IdleController extends GetxController {
  // 1. 状态管理(Rx变量,支持响应式刷新)
  final RxList<IdleModel> idleList = <IdleModel>[].obs; // 闲置列表
  final RxString selectedImageUrl = "".obs; // 选中的图片URL(发布页用)
  final RxBool isLoading = false.obs; // 加载状态

  // 2. 控制器(刷新、输入框)
  final EasyRefreshController refreshController = EasyRefreshController();
  final TextEditingController titleController = TextEditingController();
  final TextEditingController descController = TextEditingController();
  final TextEditingController contactController = TextEditingController();

  // 3. 初始化:页面启动时读取缓存数据
  
  void onInit() {
    super.onInit();
    getIdleFromCache(); // 读取本地缓存中的闲置数据
  }

  // 4. 读取本地缓存中的闲置数据
  Future<void> getIdleFromCache() async {
    isLoading.value = true; // 显示加载状态
    try {
      SharedPreferences prefs = await SharedPreferences.getInstance();
      List<String> idleStrList = prefs.getStringList("idle_list") ?? [];
      // 转换为IdleModel列表
      List<IdleModel> data = idleStrList.map((str) {
        Map<String, dynamic> map = json.decode(str);
        return IdleModel(
          id: map["id"],
          title: map["title"],
          description: map["description"],
          contact: map["contact"],
          time: map["time"],
          imageUrl: map["imageUrl"] ?? "", // 新增图片URL字段
        );
      }).toList();
      // 更新闲置列表(响应式,自动刷新页面)
      idleList.value = data;
      Fluttertoast.showToast(msg: "刷新成功");
    } catch (e) {
      Fluttertoast.showToast(msg: "数据加载失败");
    } finally {
      isLoading.value = false; // 隐藏加载状态
      refreshController.finishRefresh(); // 结束刷新动画
    }
  }

  // 5. 上拉加载更多(预留扩展,模拟加载更多数据)
  Future<void> loadMoreIdle() async {
    try {
      await Future.delayed(const Duration(milliseconds: 800));
      // 模拟加载更多数据(真实项目替换为接口请求)
      SharedPreferences prefs = await SharedPreferences.getInstance();
      List<String> idleStrList = prefs.getStringList("idle_list") ?? [];
      List<IdleModel> moreData = idleStrList.take(2).map((str) {
        Map<String, dynamic> map = json.decode(str);
        return IdleModel(
          id: DateTime.now().millisecondsSinceEpoch.toString() + map["id"],
          title: "更多-" + map["title"],
          description: map["description"],
          contact: map["contact"],
          time: map["time"],
          imageUrl: map["imageUrl"] ?? "",
        );
      }).toList();
      // 追加数据到列表
      idleList.addAll(moreData);
      refreshController.finishLoad();
    } catch (e) {
      refreshController.finishLoad(IndicatorResult.fail);
      Fluttertoast.showToast(msg: "加载更多失败");
    }
  }

  // 6. 发布闲置物品(整合表单输入、图片URL、缓存存储)
  Future<void> publishIdle() async {
    // 表单校验
    if (titleController.text.isEmpty) {
      Fluttertoast.showToast(msg: "请输入闲置物品标题");
      return;
    }
    if (descController.text.isEmpty) {
      Fluttertoast.showToast(msg: "请输入闲置物品描述");
      return;
    }
    if (contactController.text.isEmpty) {
      Fluttertoast.showToast(msg: "请输入联系方式");
      return;
    }

    // 检查网络
    bool hasNet = await NetUtil.isNetworkAvailable();
    if (!hasNet) return;

    // 创建闲置对象
    IdleModel newIdle = IdleModel(
      id: DateTime.now().millisecondsSinceEpoch.toString(),
      title: titleController.text,
      description: descController.text,
      contact: contactController.text,
      time: "2026-05-06",
      imageUrl: selectedImageUrl.value, // 关联选中的图片URL
    );

    // 存储到本地缓存
    try {
      SharedPreferences prefs = await SharedPreferences.getInstance();
      List<String> idleStrList = prefs.getStringList("idle_list") ?? [];
      // 转换为字符串,存储到缓存
      String idleStr = json.encode({
        "id": newIdle.id,
        "title": newIdle.title,
        "description": newIdle.description,
        "contact": newIdle.contact,
        "time": newIdle.time,
        "imageUrl": newIdle.imageUrl,
      });
      idleStrList.add(idleStr);
      await prefs.setStringList("idle_list", idleStrList);

      // 更新列表(响应式刷新)
      idleList.add(newIdle);
      Fluttertoast.showToast(msg: "闲置发布成功!");

      // 重置表单和图片
      titleController.clear();
      descController.clear();
      contactController.clear();
      selectedImageUrl.value = "";

      // 跳转回邻里互助列表页
      Get.back();
    } catch (e) {
      Fluttertoast.showToast(msg: "发布失败,请重试");
    }
  }

  // 7. 删除闲置物品
  Future<void> deleteIdle(String id) async {
    // 弹出确认弹窗(GetX无context弹窗)
    Get.dialog(
      AlertDialog(
        title: const Text("确认删除"),
        content: const Text("确定要删除这条闲置信息吗?删除后无法恢复哦~"),
        actions: [
          TextButton(
            onPressed: () => Get.back(),
            child: const Text("取消", color: Colors.grey),
          ),
          TextButton(
            onPressed: () async {
              // 关闭弹窗
              Get.back();
              // 删除缓存中的数据
              SharedPreferences prefs = await SharedPreferences.getInstance();
              List<String> idleStrList = prefs.getStringList("idle_list") ?? [];
              List<String> newIdleStrList = idleStrList.where((str) {
                Map<String, dynamic> map = json.decode(str);
                return map["id"] != id;
              }).toList();
              await prefs.setStringList("idle_list", newIdleStrList);

              // 更新列表(响应式刷新)
              idleList.removeWhere((item) => item.id == id);
              Fluttertoast.showToast(msg: "闲置信息删除成功!");

              // 跳转回列表页
              Get.back();
            },
            child: const Text("确认删除", color: Colors.red),
          ),
        ],
      ),
    );
  }

  // 8. 销毁控制器,释放资源(避免内存泄漏)
  
  void onClose() {
    super.onClose();
    refreshController.dispose();
    titleController.dispose();
    descController.dispose();
    contactController.dispose();
  }
}

6.3 全局注册 GetX 控制器

打开 main.dart,在APP启动时注册 IdleController,确保全局可调用:

import 'package:get/get.dart';
import 'package:zhilian_linli/controller/idle_controller.dart';

void main() {
  // 全局注册GetX控制器(懒加载,使用时才初始化)
  Get.lazyPut(() => IdleController());
  runApp(const MyApp());
}

6.4 页面绑定控制器(彻底抛弃 setState)

所有涉及闲置数据的页面(列表页、详情页、发布页),均通过 Get.find<IdleController>() 绑定控制器,用 Obx 实现局部刷新,彻底删除所有 setState 代码,具体修改已在前面的版块中完成,核心要点:

  1. 列表页:用 Obx 监听 idleList 变化,数据更新时自动刷新列表;
  2. 发布页:调用控制器的 publishIdle 方法,无需自己处理缓存和列表更新;
  3. 详情页:调用控制器的 deleteIdle 方法,删除后自动刷新列表;
  4. 所有输入框、图片URL,均由控制器管理,实现数据全局共享。

6.5 关键细节讲解(GetX 封装)

  1. 响应式变量:用 RxListRxStringRxBool 定义响应式变量,当变量值变化时,Obx 包裹的组件会自动局部刷新,比 setState 更高效;
  2. 生命周期:onInit 初始化时读取缓存数据,onClose 销毁控制器释放资源,符合Flutter开发规范,避免内存泄漏;
  3. 无context调用:弹窗、路由跳转、控制器调用,均无需context,代码更简洁,适配鸿蒙多设备时更稳定;
  4. 业务解耦:页面只负责UI展示,所有业务逻辑(缓存操作、表单校验、数据更新)都放到控制器中,后续修改逻辑时,无需改动页面代码,便于维护。

七、版块5:鸿蒙多端适配补充(图片+GetX 适配)

7.1 图片相关适配(重点)

  1. 图片尺寸适配:列表页、详情页的图片高度,根据设备尺寸动态调整,大屏(平板/开发板)放大图片,确保显示协调,避免拉伸;
  2. 图片触控适配:图片预览区域的触控区域放大,适配老年人操作,同时支持双击放大、手势缩放(后续可扩展);
  3. 相机/相册适配:image_picker 自动适配鸿蒙设备的相机分辨率、相册格式,DAYU200开发板可正常调用相机,无需额外配置;
  4. 缓存适配:cached_network_image 的缓存路径适配鸿蒙原生存储规范,避免缓存文件被系统清理,确保缓存持久化。
    在这里插入图片描述

7.2 GetX 适配说明

  1. 性能适配:GetX 局部刷新机制,在鸿蒙开发板上能有效减少页面卡顿,提升运行流畅度,比原生 setState 性能更优;
  2. 路由适配:GetX 路由跳转无需context,在鸿蒙多设备上跳转更稳定,不会出现context丢失导致的崩溃;
  3. 弹窗适配:GetX 自带的 Get.dialog 弹窗,自动适配鸿蒙多设备尺寸,大屏弹窗更宽,小屏弹窗更紧凑,无需手动调整。

7.3 今日适配常见坑(新手必看)

  1. 坑1:图片选择失败,提示权限不足 → 解决方案:检查鸿蒙设备权限配置,确保已添加相册、相机权限,首次调用时允许权限;
  2. 坑2:图片缓存不生效,每次都重新加载 → 解决方案:检查 cached_network_image 版本是否正确,确保图片URL是唯一的(避免URL重复导致缓存错乱);
  3. 坑3:GetX 控制器调用失败,提示“找不到控制器” → 解决方案:确保在 main.dart 中全局注册了 IdleController,且调用时使用 Get.find<IdleController>()
  4. 坑4:页面不刷新,Obx 无反应 → 解决方案:确保变量是 Rx 响应式变量,且 Obx 包裹的组件是依赖该变量的;
  5. 坑5:鸿蒙开发板图片拉伸 → 解决方案:设置 CachedNetworkImagefit: BoxFit.cover,同时动态调整图片容器的宽高比。

八、版块6:今日效果测试(第三方库联动测试)

今天的测试重点是“第三方库联动+功能闭环+鸿蒙适配”,测试步骤详细拆解,新手可照着操作:

  1. 测试图片选择功能:

    • 鸿蒙手机端:点击发布页“选择图片”,弹出弹窗,选择相册/相机,确认能正常选择图片,预览图片正常;
    • 鸿蒙平板端:确认图片选择弹窗布局协调,图片预览区域放大,触控灵敏;
    • DAYU200开发板:确认能正常调用相机/相册,图片选择后能正常显示。
  2. 测试图片缓存功能:

    • 首次加载闲置列表,图片显示占位动画,加载完成后正常显示;
    • 关闭APP重新打开,列表图片无需重新加载,直接显示(缓存生效);
    • 弱网环境下,图片加载流畅,无卡顿、空白问题。
  3. 测试GetX状态管理:

    • 发布闲置(带图/不带图),确认发布成功后,列表自动刷新,显示新发布的闲置;
    • 进入详情页删除闲置,确认删除成功后,列表自动刷新,该条数据消失;
    • 下拉刷新列表,确认数据能正常刷新,上拉加载更多能正常追加数据;
    • 所有操作无 setState,页面局部刷新,无整页重建卡顿。
  4. 测试第三方库联动:

    • 图片选择前,网络断开时,弹出Toast提示(connectivity_plus 作用);
    • 发布、删除、刷新操作,均弹出Toast提示(fluttertoast 作用);
    • 路由跳转、弹窗调用,均无需context(GetX 作用);
    • 下拉刷新样式自定义,动画流畅(flutter_easy_refresh 作用)。
  5. 鸿蒙多端适配测试:

    • 检查所有页面的图片布局、弹窗尺寸、文字大小,确保多设备显示协调;
    • 测试开发板的触控灵敏度,确保图片选择、按钮点击、下拉刷新正常;
    • 测试弱网环境下的图片加载和缓存效果,确保适配鸿蒙弱网场景。

九、Day7 开发总结(第三方库深化实战)

Day7 我们聚焦“图片功能+GetX状态管理”,全面深化第三方库实战,完成了项目架构的升级,核心成果总结如下:

  1. 新增2个图片相关第三方库:image_picker 实现相册/相机选择图片,cached_network_image 实现网络图片缓存,解决鸿蒙弱网环境下图片加载卡顿问题;
  2. 彻底封装 GetX 状态管理:新建 IdleController,统一管理闲置数据的查询、新增、删除、刷新,替代所有 setState,实现局部刷新,提升性能、代码解耦;
  3. 完善闲置发布带图功能:实现“选择图片→预览图片→模拟上传→缓存存储→列表展示”的完整流程,贴合真实业务场景;
  4. 优化闲置列表和详情页:新增图片展示、预览功能,用 cached_network_image 优化加载体验,适配鸿蒙多设备;
  5. 第三方库联动:整合 GetX、fluttertoast、connectivity_plus、flutter_easy_refresh 等库,形成完整的生态,让项目更具企业级规范;
  6. 完善鸿蒙多端适配:针对图片选择、图片缓存、GetX 状态管理的适配细节,解决常见坑点,确保多设备运行流畅。

核心提醒:Day7 是项目架构升级的关键一天,GetX 状态管理、图片选择与缓存,都是毕设、比赛中高频出现的考点,也是企业级APP的必备能力;后续我们会继续引入更多第三方库,完善政务服务、个人中心等模块,同时优化项目性能和UI细节。
在这里插入图片描述

十、下期内容预告(Day8,第三方库持续深化)

Day8 继续基于第三方库开发,重点完善政务服务模块和个人中心,同时引入Lottie动画提升UI质感:

  • 引入 lottie 第三方库:实现精美动画(页面加载、按钮点击、空状态),提升APP视觉体验;
  • 完善政务服务页面:新增社保查询、居住证办理功能,用 dio 模拟接口请求,GetX 管理政务数据;
  • 完善我的页面:新增个人信息编辑、登录状态模拟(用 shared_preferences 缓存登录状态);
  • 引入 url_launcher 第三方库:实现联系方式一键拨号、跳转微信,贴合民生需求;
  • 统一项目UI风格,优化鸿蒙多端适配细节,让项目更美观、更规范。
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐