Flutter+开源鸿蒙实战|智联邻里Day7 图片选择(image_picker)+网络图片缓存(cached_network_image)+GetX状态管理彻底封装
本文介绍了如何在Flutter+开源鸿蒙项目中实现图片选择与缓存功能,主要包含以下内容: 新增两个核心第三方库: image_picker:实现相册/相机图片选择功能 cached_network_image:提供网络图片缓存能力 技术实现要点: 封装图片选择工具类,支持图片压缩和多设备适配 使用GetX状态管理替代setState,实现数据统一管理 添加鸿蒙设备权限配置,确保功能正常使用 优化网
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,让项目架构更清晰、性能更优,具体核心目标:
- 引入
image_picker第三方库:实现鸿蒙设备相册选择、相机拍摄图片,完善闲置发布“带图上传”功能(模拟上传,贴合真实业务); - 引入
cached_network_image第三方库:实现网络图片缓存,优化闲置列表、详情页图片加载速度,解决鸿蒙弱网环境下图片加载卡顿问题; - 彻底封装 GetX 状态管理:新建闲置数据控制器(IdleController),统一管理闲置数据的查询、新增、删除,替代所有页面的
setState,实现局部刷新; - 完善鸿蒙多端适配:优化图片选择弹窗、图片展示布局,适配平板/开发板的图片尺寸,解决图片拉伸、触控不灵敏问题;
- 整合所有第三方库:将 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 需要调用鸿蒙设备的相册、相机权限,否则无法正常使用,需在项目中添加权限配置:
- 打开
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"/>
- 鸿蒙原生模式(可选,适配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 鸿蒙适配细节(重点)
- 权限适配:前面已配置相册、相机权限,鸿蒙设备首次调用时会弹出权限申请弹窗,用户允许后才能正常使用,避免权限不足导致崩溃;
- 尺寸适配:图片选择区域的高度根据设备尺寸动态调整,大屏(平板/开发板)放大图片区域,确保触控区域充足;
- 兼容性适配:
image_picker自动适配鸿蒙Android兼容模式和原生模式,DAYU200开发板可正常调用相机、相册,无需额外修改代码; - 弱网适配:图片选择前先调用
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 关键细节讲解(图片缓存+适配)
- 缓存逻辑:
cached_network_image会自动将图片缓存到鸿蒙设备本地,默认缓存时间较长,下次打开APP时,图片会直接从本地加载,无需重新请求,极大优化弱网体验; - 占位图/错误图:通过
placeholder和errorWidget属性,实现图片加载中的占位动画和加载失败的错误提示,提升用户体验,避免页面空白; - 图片预览:结合
ImageUtil.previewImage方法,实现“点击图片放大预览”,贴合真实APP用法,适配鸿蒙多设备的触控操作; - 鸿蒙适配:缓存路径符合鸿蒙系统规范,不会出现缓存文件无法读取的问题;平板/开发板上,图片尺寸自动适配屏幕比例,避免拉伸、变形。
六、版块4:彻底封装 GetX 状态管理(替代所有 setState)
6.1 为什么要封装 GetX 控制器?
前面 Day6 我们引入了 GetX,但只是用了路由功能;今天我们彻底封装 GetX 状态管理,将所有闲置数据的查询、新增、删除、刷新逻辑,统一放到 IdleController 中,替代所有页面的 setState,实现:
- 数据全局共享,多个页面(列表页、详情页、发布页)可共用同一套数据;
- 局部刷新,只刷新需要更新的组件,避免整页重建,提升性能;
- 代码解耦,页面只负责展示,业务逻辑全部放到控制器中,便于维护和扩展;
- 无需 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 代码,具体修改已在前面的版块中完成,核心要点:
- 列表页:用
Obx监听idleList变化,数据更新时自动刷新列表; - 发布页:调用控制器的
publishIdle方法,无需自己处理缓存和列表更新; - 详情页:调用控制器的
deleteIdle方法,删除后自动刷新列表; - 所有输入框、图片URL,均由控制器管理,实现数据全局共享。
6.5 关键细节讲解(GetX 封装)
- 响应式变量:用
RxList、RxString、RxBool定义响应式变量,当变量值变化时,Obx包裹的组件会自动局部刷新,比setState更高效; - 生命周期:
onInit初始化时读取缓存数据,onClose销毁控制器释放资源,符合Flutter开发规范,避免内存泄漏; - 无context调用:弹窗、路由跳转、控制器调用,均无需context,代码更简洁,适配鸿蒙多设备时更稳定;
- 业务解耦:页面只负责UI展示,所有业务逻辑(缓存操作、表单校验、数据更新)都放到控制器中,后续修改逻辑时,无需改动页面代码,便于维护。
七、版块5:鸿蒙多端适配补充(图片+GetX 适配)
7.1 图片相关适配(重点)
- 图片尺寸适配:列表页、详情页的图片高度,根据设备尺寸动态调整,大屏(平板/开发板)放大图片,确保显示协调,避免拉伸;
- 图片触控适配:图片预览区域的触控区域放大,适配老年人操作,同时支持双击放大、手势缩放(后续可扩展);
- 相机/相册适配:
image_picker自动适配鸿蒙设备的相机分辨率、相册格式,DAYU200开发板可正常调用相机,无需额外配置; - 缓存适配:
cached_network_image的缓存路径适配鸿蒙原生存储规范,避免缓存文件被系统清理,确保缓存持久化。
7.2 GetX 适配说明
- 性能适配:GetX 局部刷新机制,在鸿蒙开发板上能有效减少页面卡顿,提升运行流畅度,比原生
setState性能更优; - 路由适配:GetX 路由跳转无需context,在鸿蒙多设备上跳转更稳定,不会出现context丢失导致的崩溃;
- 弹窗适配:GetX 自带的
Get.dialog弹窗,自动适配鸿蒙多设备尺寸,大屏弹窗更宽,小屏弹窗更紧凑,无需手动调整。
7.3 今日适配常见坑(新手必看)
- 坑1:图片选择失败,提示权限不足 → 解决方案:检查鸿蒙设备权限配置,确保已添加相册、相机权限,首次调用时允许权限;
- 坑2:图片缓存不生效,每次都重新加载 → 解决方案:检查
cached_network_image版本是否正确,确保图片URL是唯一的(避免URL重复导致缓存错乱); - 坑3:GetX 控制器调用失败,提示“找不到控制器” → 解决方案:确保在
main.dart中全局注册了IdleController,且调用时使用Get.find<IdleController>(); - 坑4:页面不刷新,
Obx无反应 → 解决方案:确保变量是Rx响应式变量,且Obx包裹的组件是依赖该变量的; - 坑5:鸿蒙开发板图片拉伸 → 解决方案:设置
CachedNetworkImage的fit: BoxFit.cover,同时动态调整图片容器的宽高比。
八、版块6:今日效果测试(第三方库联动测试)
今天的测试重点是“第三方库联动+功能闭环+鸿蒙适配”,测试步骤详细拆解,新手可照着操作:
-
测试图片选择功能:
- 鸿蒙手机端:点击发布页“选择图片”,弹出弹窗,选择相册/相机,确认能正常选择图片,预览图片正常;
- 鸿蒙平板端:确认图片选择弹窗布局协调,图片预览区域放大,触控灵敏;
- DAYU200开发板:确认能正常调用相机/相册,图片选择后能正常显示。
-
测试图片缓存功能:
- 首次加载闲置列表,图片显示占位动画,加载完成后正常显示;
- 关闭APP重新打开,列表图片无需重新加载,直接显示(缓存生效);
- 弱网环境下,图片加载流畅,无卡顿、空白问题。
-
测试GetX状态管理:
- 发布闲置(带图/不带图),确认发布成功后,列表自动刷新,显示新发布的闲置;
- 进入详情页删除闲置,确认删除成功后,列表自动刷新,该条数据消失;
- 下拉刷新列表,确认数据能正常刷新,上拉加载更多能正常追加数据;
- 所有操作无
setState,页面局部刷新,无整页重建卡顿。
-
测试第三方库联动:
- 图片选择前,网络断开时,弹出Toast提示(connectivity_plus 作用);
- 发布、删除、刷新操作,均弹出Toast提示(fluttertoast 作用);
- 路由跳转、弹窗调用,均无需context(GetX 作用);
- 下拉刷新样式自定义,动画流畅(flutter_easy_refresh 作用)。
-
鸿蒙多端适配测试:
- 检查所有页面的图片布局、弹窗尺寸、文字大小,确保多设备显示协调;
- 测试开发板的触控灵敏度,确保图片选择、按钮点击、下拉刷新正常;
- 测试弱网环境下的图片加载和缓存效果,确保适配鸿蒙弱网场景。
九、Day7 开发总结(第三方库深化实战)
Day7 我们聚焦“图片功能+GetX状态管理”,全面深化第三方库实战,完成了项目架构的升级,核心成果总结如下:
- 新增2个图片相关第三方库:
image_picker实现相册/相机选择图片,cached_network_image实现网络图片缓存,解决鸿蒙弱网环境下图片加载卡顿问题; - 彻底封装 GetX 状态管理:新建
IdleController,统一管理闲置数据的查询、新增、删除、刷新,替代所有setState,实现局部刷新,提升性能、代码解耦; - 完善闲置发布带图功能:实现“选择图片→预览图片→模拟上传→缓存存储→列表展示”的完整流程,贴合真实业务场景;
- 优化闲置列表和详情页:新增图片展示、预览功能,用
cached_network_image优化加载体验,适配鸿蒙多设备; - 第三方库联动:整合 GetX、fluttertoast、connectivity_plus、flutter_easy_refresh 等库,形成完整的生态,让项目更具企业级规范;
- 完善鸿蒙多端适配:针对图片选择、图片缓存、GetX 状态管理的适配细节,解决常见坑点,确保多设备运行流畅。
核心提醒:Day7 是项目架构升级的关键一天,GetX 状态管理、图片选择与缓存,都是毕设、比赛中高频出现的考点,也是企业级APP的必备能力;后续我们会继续引入更多第三方库,完善政务服务、个人中心等模块,同时优化项目性能和UI细节。
十、下期内容预告(Day8,第三方库持续深化)
Day8 继续基于第三方库开发,重点完善政务服务模块和个人中心,同时引入Lottie动画提升UI质感:
- 引入
lottie第三方库:实现精美动画(页面加载、按钮点击、空状态),提升APP视觉体验; - 完善政务服务页面:新增社保查询、居住证办理功能,用 dio 模拟接口请求,GetX 管理政务数据;
- 完善我的页面:新增个人信息编辑、登录状态模拟(用 shared_preferences 缓存登录状态);
- 引入
url_launcher第三方库:实现联系方式一键拨号、跳转微信,贴合民生需求; - 统一项目UI风格,优化鸿蒙多端适配细节,让项目更美观、更规范。
更多推荐



所有评论(0)