Flutter+OpenHarmony智能家居开发Day6|设备搜索筛选+分组全流程
本文是Flutter+OpenHarmony智能家居开发系列的Day6详解,聚焦**设备搜索筛选+分组管理**核心功能,解决“设备多查找难”的问题,从“工具类封装→业务层状态管理→UI层交互开发→鸿蒙跨终端适配”全流程拆解,兼顾功能、性能和用户体验。
Flutter+OpenHarmony智能家居开发Day6|设备搜索筛选+分组全流程详解(多终端适配+性能优化+万行细节版)
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
大家好~ 经过Day5的开发,我们已经实现了设备的远程控制核心功能——用户能开关设备、调节空调温度/灯光亮度,体验上也通过乐观更新解决了网络延迟的问题。但新的问题随之而来:如果用户家里有几十台智能设备(客厅空调、卧室灯光、阳台窗帘、厨房热水器……),在长长的列表里翻找某一台设备,体验会非常差,甚至会让用户失去使用APP的耐心。
这就是Day6要解决的核心问题:设备搜索筛选+分组管理,把“被动找设备”变成“主动找设备”,让用户能通过搜索、筛选、分组快速定位目标设备,这也是智能家居APP从“能用”到“易用”的关键升级。不同于Day5的控制逻辑,Day6的开发重点在“数据过滤+UI交互+性能优化”——既要保证搜索/筛选的响应速度,又要适配OpenHarmony多终端(手机/平板/DAYU200)的布局,还要解决“搜索卡顿、筛选状态不同步、分组记忆失效”等高频问题。
本文会延续Day5的“逐步骤拆解+全场景踩坑解决”风格,不堆砌冗余代码,只保留核心关键代码,重点讲解“为什么这么设计、可能出什么问题、怎么优化”,从需求分析、逻辑设计、UI开发到鸿蒙适配,每一个细节都讲透,同时补充大量工程化优化细节(比如搜索防抖、筛选状态持久化、大屏分组布局),让你的搜索筛选功能不仅能运行,还能兼顾体验和性能。
无论你是刚接触Flutter+OpenHarmony的新手,还是有一定经验的开发者,都能从本文中获取可直接落地的逻辑思路、高频问题解决方案,以及贴合鸿蒙跨终端特性的优化技巧。话不多说,我们正式开启Day6的开发之旅!
一、Day6核心目标(明确方向,不走弯路)
在正式开发前,先明确Day6的核心目标,所有开发工作围绕这些目标展开,确保不偏离“提升设备查找效率”的核心诉求:
- 实现模糊搜索功能:支持按设备名称模糊匹配,输入关键词实时过滤设备列表,解决“设备多找起来慢”的问题;
- 实现多维度筛选:支持按设备类型(空调/灯光/窗帘)、在线状态(在线/离线)筛选,支持多条件组合筛选;
- 实现设备分组:支持按房间(客厅/卧室/厨房)分组展示设备,分组状态可记忆,下次打开APP保留用户的分组选择;
- 性能优化:解决搜索/筛选时的卡顿问题(如输入时列表频繁重建、大数据量下过滤缓慢),保证筛选响应时间<100ms;
- OpenHarmony专属适配:针对手机/平板/DAYU200开发板做布局适配(如平板端筛选栏横向排列、开发板分组卡片放大),解决大屏布局混乱、触控不友好的问题;
- 体验优化:添加搜索防抖、筛选状态提示、空结果页面,避免用户误操作,提升交互体验;
- 工程化规范:复用Day5的分层架构,搜索/筛选逻辑封装在业务层,UI层仅负责交互展示,保证代码可复用、可扩展;
- 全场景测试:验证不同设备数量(10/50/100台)、不同筛选条件组合、多终端下的功能稳定性和性能。
二、前置准备(衔接前文,避免脱节)
Day6的开发基于Day5已完成的设备控制功能,在开始开发前,先确认前置环境和已有代码是否就绪,避免出现衔接问题:
2.1 已有架构确认(重点衔接)
回顾Day3-Day5已完成的核心架构,确保所有依赖和基础组件都能支撑Day6的开发:
- 数据层:已完成BaseDevice及子类模型(含type/online等字段),封装了DeviceRepository仓库,支持设备列表获取和控制;
- 业务层:已封装DeviceProvider,管理设备列表、加载状态、控制状态,支持状态通知和更新;
- UI层:已完成DeviceListPage、DeviceCard等组件,支持多终端布局适配;
- 工具类:已封装防抖工具(Debounce)、全局异常处理器,可直接复用;
- 鸿蒙适配:已配置网络权限、多终端触控适配基础,可在此基础上扩展。
2.2 依赖检查(无新增核心依赖)
Day6无需新增核心依赖,沿用Day5的pubspec.yaml配置,仅需确认已有依赖是否正常:
dependencies:
flutter:
sdk: flutter
dio: ^5.4.0 # 网络请求
provider: ^6.1.1 # 状态管理
logger: ^1.1.0 # 日志
device_info_plus: ^9.1.0 # 设备信息(判断终端类型)
shared_preferences: ^2.2.2 # 新增:分组状态持久化(仅需添加这1个依赖)
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
【安装命令】:
flutter pub get
【可能出现的问题】:shared_preferences安装失败
【根因】:版本与Flutter for OpenHarmony不兼容
【解决方案】:指定兼容版本(如2.2.2),执行flutter clean清除缓存后重新安装;若仍失败,替换为鸿蒙原生存储API(ohos_preferences)。
2.3 数据模型扩展(核心前置,仅关键代码)
为支持“按房间分组”,需在BaseDevice模型中新增room字段(房间名称),仅修改核心部分:
文件路径:lib/data/models/device_model.dart
enum DeviceTypeEnum { airConditioner, light, curtain, other }
enum DeviceStatusEnum { on, off }
// 基础设备模型(仅展示新增/修改部分)
class BaseDevice {
final String id;
final String name;
final DeviceTypeEnum type;
final DeviceStatusEnum status;
final bool online;
final String room; // 新增:房间字段(如"客厅"/"卧室"/"厨房")
// 其他原有字段省略...
BaseDevice({
required this.id,
required this.name,
required this.type,
required this.status,
required this.online,
required this.room, // 新增:必传参数
// 其他参数省略...
});
// fromJson方法新增room解析(仅关键代码)
factory BaseDevice.fromJson(Map<String, dynamic> json) {
return BaseDevice(
id: json['id'] ?? '',
name: json['name'] ?? '',
type: _deviceTypeFromJson(json['type'] ?? ''),
status: json['status'] == 'on' ? DeviceStatusEnum.on : DeviceStatusEnum.off,
online: json['online'] ?? false,
room: json['room'] ?? '未分组', // 新增:默认值"未分组"
// 其他参数省略...
);
}
}
// 子类(AirConditionerDevice/LightDevice等)无需修改,继承BaseDevice即可
【核心说明】:
- 新增
room字段用于分组,默认值设为“未分组”,避免空值导致的UI异常; - 后端返回数据需包含
room字段,若后端暂未支持,可在前端模拟(如给不同设备手动赋值)。
2.4 后端接口准备(核心前提)
Day6需要新增“搜索/筛选”接口(也可前端本地过滤,推荐混合方案:少量设备本地过滤,大量设备后端过滤):
方案1:前端本地过滤(适合设备数<100台)
无需新增接口,直接基于Day4获取的设备列表做本地过滤,优点是响应快、无网络依赖,缺点是大数据量下性能略降。
方案2:后端过滤(适合设备数≥100台)
新增接口规范(仅关键信息):
- 接口地址:/device/list
- 请求方式:GET
- 请求参数:
参数名 类型 说明 keyword String 搜索关键词(模糊匹配名称) type String 设备类型(airConditioner/light等) online Boolean 在线状态(true/false) room String 房间名称 page Int 分页页码(复用Day4分页逻辑) - 响应体:和Day4的设备列表接口一致,返回过滤后的设备列表。
【开发建议】:先实现前端本地过滤,后续根据设备数量扩展后端过滤,降低开发复杂度。
2.5 开发环境确认(复用Day5)
确认Flutter、DevEco Studio、测试设备(手机/平板/DAYU200)均就绪,开启Flutter DevTools便于性能监控(重点监控搜索时的列表重建耗时)。
三、逐步骤详细开发(核心部分,代码少+细节多)
Day6的开发分为4个核心模块,按“数据层→业务层→UI层→OpenHarmony适配”的顺序开发,每个模块重点讲解逻辑设计、问题解决,仅保留关键代码:
模块1:数据层扩展——搜索筛选工具类(基础核心)
先封装通用的搜索筛选工具类,统一处理过滤逻辑,避免在业务层/UI层重复写过滤代码,提升复用性:
关键代码:搜索筛选工具类
文件路径:lib/core/utils/device_filter_util.dart
import 'package:smart_home_flutter/data/models/device_model.dart';
class DeviceFilterUtil {
// 核心:多条件组合过滤(仅关键代码)
static List<BaseDevice> filterDevices({
required List<BaseDevice> allDevices, // 原始设备列表
String? keyword, // 搜索关键词
DeviceTypeEnum? type, // 设备类型
bool? online, // 在线状态
String? room, // 房间分组
}) {
List<BaseDevice> filtered = List.from(allDevices); // 复制列表,避免修改原数据
// 步骤1:关键词过滤(模糊匹配名称,忽略大小写)
if (keyword != null && keyword.isNotEmpty) {
filtered = filtered.where((device) =>
device.name.toLowerCase().contains(keyword.toLowerCase())
).toList();
}
// 步骤2:设备类型过滤
if (type != null) {
filtered = filtered.where((device) => device.type == type).toList();
}
// 步骤3:在线状态过滤
if (online != null) {
filtered = filtered.where((device) => device.online == online).toList();
}
// 步骤4:房间分组过滤
if (room != null && room.isNotEmpty && room != '全部') {
filtered = filtered.where((device) => device.room == room).toList();
}
return filtered;
}
// 辅助:获取所有房间列表(用于分组选择)
static List<String> getRoomList(List<BaseDevice> allDevices) {
final rooms = allDevices.map((d) => d.room).toSet().toList();
rooms.insert(0, '全部'); // 新增“全部”选项
return rooms;
}
// 辅助:获取所有设备类型列表(用于类型筛选)
static List<DeviceTypeEnum> getTypeList(List<BaseDevice> allDevices) {
return allDevices.map((d) => d.type).toSet().toList();
}
}
【核心逻辑详解】:
- 组合过滤:按“关键词→类型→在线状态→房间”的顺序过滤,顺序不影响结果,但按“过滤范围从大到小”排列可提升性能(先过滤掉更多数据,后续过滤更少);
- 数据隔离:使用
List.from(allDevices)复制原始列表,避免过滤时修改原数据导致的状态错乱(新手高频踩坑点); - 大小写忽略:关键词过滤时统一转小写,避免“客厅空调”和“客厅空调”匹配失败的问题;
- 空值兼容:所有过滤条件都做空值判断,未传条件则跳过该过滤,支持“只搜关键词”“只筛选在线状态”等灵活组合。
【高频问题&解决方案】:
| 问题场景 | 根因 | 解决方案 |
|---|---|---|
| 过滤后列表为空,无任何提示 | 未处理空结果场景,用户以为APP故障 | 在UI层添加空结果页面(如“未找到匹配的设备”),后续UI层会详细讲 |
| 分组过滤时,“未分组”设备不显示 | 过滤条件判断room != '',但“未分组”是字符串不是空值 |
调整判断逻辑为room != null && room.isNotEmpty && room != '全部',兼容“未分组”场景 |
| 类型过滤时,枚举值匹配失败 | 后端返回的type字符串与前端枚举映射错误 | 检查_deviceTypeFromJson方法,确保“airConditioner”对应DeviceTypeEnum.airConditioner |
模块2:业务层封装——搜索筛选状态管理(核心灵魂)
业务层是搜索筛选的核心,需要管理搜索关键词、筛选条件、分组状态,处理过滤逻辑、状态持久化,同时通知UI层刷新。
步骤1:扩展DeviceProvider(仅关键代码+详细逻辑)
文件路径:lib/domain/providers/device_provider.dart
import 'package:flutter/foundation.dart';
import 'package:logger/logger.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:smart_home_flutter/core/utils/device_filter_util.dart';
import 'package:smart_home_flutter/data/models/device_model.dart';
// 其他导入省略...
final Logger _logger = Logger();
class DeviceProvider with ChangeNotifier {
// 原有状态(Day4-Day5,省略)
List<BaseDevice> _deviceList = []; // 原始设备列表
LoadStatus _loadStatus = LoadStatus.initial;
// ...其他原有状态
// 新增:搜索筛选核心状态
String _searchKeyword = ''; // 搜索关键词
DeviceTypeEnum? _selectedType; // 选中的设备类型
bool? _selectedOnline; // 选中的在线状态(true=在线,false=离线,null=全部)
String _selectedRoom = '全部'; // 选中的房间分组(默认“全部”)
List<BaseDevice> _filteredDeviceList = []; // 过滤后的设备列表
final Debounce _searchDebounce = Debounce(delay: const Duration(milliseconds: 300)); // 搜索防抖
// 对外暴露的只读状态
String get searchKeyword => _searchKeyword;
DeviceTypeEnum? get selectedType => _selectedType;
bool? get selectedOnline => _selectedOnline;
String get selectedRoom => _selectedRoom;
List<BaseDevice> get filteredDeviceList => _filteredDeviceList;
// 构造函数(复用原有,新增初始化分组状态)
DeviceProvider({required DeviceRepository deviceRepository})
: _deviceRepository = deviceRepository {
_initSelectedRoom(); // 初始化分组状态(从本地读取)
}
// 核心:初始化分组状态(持久化)
Future<void> _initSelectedRoom() async {
try {
final prefs = await SharedPreferences.getInstance();
_selectedRoom = prefs.getString('selected_room') ?? '全部';
_logger.d('初始化分组状态:$_selectedRoom');
} catch (e) {
_logger.e('读取分组状态失败:$e');
_selectedRoom = '全部'; // 失败时默认“全部”
}
}
// 核心:保存分组状态(持久化)
Future<void> _saveSelectedRoom(String room) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('selected_room', room);
_logger.d('保存分组状态:$room');
} catch (e) {
_logger.e('保存分组状态失败:$e');
}
}
// 核心:更新搜索关键词(带防抖)
void updateSearchKeyword(String keyword) {
_searchKeyword = keyword;
// 防抖:输入停止300ms后再过滤,避免频繁重建列表
_searchDebounce.run(() {
_filterDevices(); // 执行过滤
notifyListeners(); // 通知UI刷新
});
}
// 核心:更新筛选条件(类型/在线状态/房间)
void updateFilterCondition({
DeviceTypeEnum? type,
bool? online,
String? room,
}) {
// 更新条件(非空则覆盖,空则保留原有)
if (type != null) _selectedType = type;
if (online != null) _selectedOnline = online;
if (room != null) {
_selectedRoom = room;
_saveSelectedRoom(room); // 保存分组状态
}
_filterDevices(); // 立即过滤(筛选条件无需防抖)
notifyListeners();
}
// 核心:重置所有筛选条件
void resetFilterCondition() {
_searchKeyword = '';
_selectedType = null;
_selectedOnline = null;
_selectedRoom = '全部';
_saveSelectedRoom('全部'); // 重置分组状态
_filterDevices();
notifyListeners();
}
// 核心:执行过滤逻辑(内部方法)
void _filterDevices() {
if (_deviceList.isEmpty) {
_filteredDeviceList = [];
return;
}
// 调用工具类执行组合过滤
_filteredDeviceList = DeviceFilterUtil.filterDevices(
allDevices: _deviceList,
keyword: _searchKeyword,
type: _selectedType,
online: _selectedOnline,
room: _selectedRoom,
);
_logger.d('过滤完成:原始数量=${_deviceList.length},过滤后=${_filteredDeviceList.length}');
}
// 重写原有方法:获取设备列表后立即执行过滤
Future<void> fetchDeviceList() async {
// 原有逻辑(省略):发起请求→更新_deviceList
// 新增:获取列表后执行过滤,保证filteredDeviceList有数据
_filterDevices();
}
// 其他原有方法(控制相关)省略...
}
【核心逻辑详解(重点!)】:
- 状态设计:
- 区分
_deviceList(原始列表)和_filteredDeviceList(过滤后列表):原始列表保存所有设备,过滤后列表用于UI展示,避免过滤后无法恢复原始数据(新手最容易犯的错误:直接修改原始列表,导致筛选重置后数据丢失); - 筛选条件拆分为独立状态:关键词、类型、在线状态、房间,支持单独修改,灵活组合;
- 区分
- 防抖处理:
- 搜索关键词添加300ms防抖:用户输入时(如“客厅空调”),每输入一个字都会触发更新,防抖可避免1次输入触发5次过滤,提升性能(实测:100台设备时,防抖可减少80%的过滤次数);
- 筛选条件(类型/在线/房间)无需防抖:用户点击筛选按钮是一次性操作,无频繁触发问题;
- 状态持久化:
- 分组状态(selectedRoom)通过SharedPreferences保存,APP重启后保留用户的分组选择,提升体验;
- 持久化操作封装在
_initSelectedRoom和_saveSelectedRoom中,异常时默认“全部”,避免崩溃;
- 逻辑联动:
- 获取设备列表后立即执行过滤,保证UI层一开始就能看到过滤后的列表(而非空列表);
- 所有条件更新后都调用
_filterDevices,保证过滤后的列表实时同步。
【高频问题&解决方案(篇幅重点!)】:
| 问题场景 | 根因 | 解决方案 |
|---|---|---|
| 输入关键词后,列表延迟300ms刷新,用户以为没反应 | 防抖导致延迟,无加载反馈 | 在搜索框右侧添加加载动画(如CircularProgressIndicator),防抖期间显示,提示用户“正在搜索” |
| 切换分组后,控制设备状态,分组列表未刷新 | 控制设备后仅更新_deviceList,未触发_filterDevices | 在toggleDeviceStatus方法的finally块中调用_filterDevices(),保证控制后过滤列表同步更新 |
| 大量设备(100台)时,过滤卡顿,UI掉帧 | 过滤逻辑在主线程执行,阻塞UI渲染 | 优化:1. 提前将设备名称转小写缓存;2. 使用compute(Isolate)执行过滤(代码示例):_filteredDeviceList = await compute(DeviceFilterUtil.filterDevices, params); |
| 筛选状态重置后,搜索框内容未清空 | resetFilterCondition仅重置状态,未清空搜索框UI | 重置方法中同时清空_searchKeyword,并确保UI层绑定searchKeyword状态 |
| 鸿蒙开发板上,SharedPreferences存储失败 | 鸿蒙沙盒路径限制,SharedPreferences无法写入 | 替换为鸿蒙原生存储API(ohos_preferences),核心逻辑不变,仅更换存储方式 |
模块3:UI层开发——搜索筛选交互组件(用户体验核心)
UI层的核心是“让筛选操作直观、顺手”,同时适配OpenHarmony多终端布局,解决大屏交互问题。
步骤1:搜索框组件(仅关键代码+适配细节)
文件路径:lib/presentation/widgets/search/device_search_bar.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:smart_home_flutter/domain/providers/device_provider.dart';
class DeviceSearchBar extends StatelessWidget {
final bool isTablet; // 是否平板/开发板
const DeviceSearchBar({super.key, required this.isTablet});
Widget build(BuildContext context) {
final provider = Provider.of<DeviceProvider>(context);
final double inputWidth = isTablet ? 400 : double.infinity; // 多终端宽度适配
return SizedBox(
width: inputWidth,
child: TextField(
controller: TextEditingController(text: provider.searchKeyword),
decoration: InputDecoration(
hintText: '搜索设备名称(如客厅空调)',
hintStyle: TextStyle(fontSize: isTablet ? 16 : 14), // 大屏文字放大
prefixIcon: const Icon(Icons.search),
suffixIcon: provider.searchKeyword.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () => provider.updateSearchKeyword(''), // 清空关键词
)
: (provider.searchKeyword.isEmpty && provider.isControlling
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: null),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.grey[100],
// 开发板适配:增大输入框高度,提升触控体验
contentPadding: EdgeInsets.symmetric(
horizontal: 16,
vertical: isTablet ? 16 : 12,
),
),
style: TextStyle(fontSize: isTablet ? 16 : 14),
// 开发板适配:增大输入框光标尺寸
cursorWidth: isTablet ? 3 : 2,
cursorHeight: isTablet ? 24 : 20,
onChanged: (value) => provider.updateSearchKeyword(value),
),
);
}
}
【适配细节详解(重点!)】:
- 多终端尺寸:
- 手机端:搜索框占满屏幕宽度;
- 平板/开发板:固定宽度400dp,避免搜索框过宽导致视觉失衡;
- 输入框高度:开发板端从12dp增至16dp,触控区域更大,减少误触;
- 交互反馈:
- 清空按钮:关键词非空时显示,一键清空,比重新输入更便捷;
- 加载动画:防抖期间显示,解决“输入后无反应”的用户疑惑;
- 状态绑定:使用
TextEditingController(text: provider.searchKeyword)绑定关键词状态,确保筛选重置后搜索框内容同步清空。
步骤2:筛选栏组件(核心逻辑+大屏适配)
文件路径:lib/presentation/widgets/filter/device_filter_bar.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:smart_home_flutter/domain/providers/device_provider.dart';
import 'package:smart_home_flutter/data/models/device_model.dart';
import 'package:smart_home_flutter/core/utils/device_filter_util.dart';
class DeviceFilterBar extends StatelessWidget {
final bool isTablet; // 是否平板/开发板
const DeviceFilterBar({super.key, required this.isTablet});
Widget build(BuildContext context) {
final provider = Provider.of<DeviceProvider>(context);
final roomList = DeviceFilterUtil.getRoomList(provider.deviceList);
final typeList = DeviceFilterUtil.getTypeList(provider.deviceList);
// 大屏/小屏布局适配:平板端横向排列,手机端纵向折叠
return isTablet
? _buildHorizontalFilterBar(context, provider, roomList, typeList)
: _buildVerticalFilterBar(context, provider, roomList, typeList);
}
// 平板/开发板:横向筛选栏(关键代码)
Widget _buildHorizontalFilterBar(
BuildContext context,
DeviceProvider provider,
List<String> roomList,
List<DeviceTypeEnum> typeList,
) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 设备类型筛选
_buildTypeFilter(context, provider, typeList),
const SizedBox(width: 16),
// 在线状态筛选
_buildOnlineFilter(context, provider),
const SizedBox(width: 16),
// 房间分组筛选
_buildRoomFilter(context, provider, roomList),
const SizedBox(width: 16),
// 重置按钮
ElevatedButton(
onPressed: () => provider.resetFilterCondition(),
style: ElevatedButton.styleFrom(
minimumSize: const Size(80, 40), // 大屏按钮放大
),
child: const Text('重置筛选'),
),
],
),
);
}
// 手机端:纵向折叠筛选栏(核心逻辑同横向,省略代码)
Widget _buildVerticalFilterBar(...) {
// 逻辑:用ExpansionTile折叠筛选条件,节省屏幕空间,省略冗余代码
}
// 设备类型筛选(关键代码)
Widget _buildTypeFilter(
BuildContext context,
DeviceProvider provider,
List<DeviceTypeEnum> typeList,
) {
return DropdownButton<DeviceTypeEnum>(
value: provider.selectedType,
hint: const Text('设备类型'),
items: typeList
.map((type) => DropdownMenuItem(
value: type,
child: Text(
type == DeviceTypeEnum.airConditioner ? '空调' :
type == DeviceTypeEnum.light ? '灯光' :
type == DeviceTypeEnum.curtain ? '窗帘' : '其他',
style: const TextStyle(fontSize: 16), // 大屏文字放大
),
))
.toList(),
onChanged: (value) => provider.updateFilterCondition(type: value),
// 开发板适配:增大下拉按钮触控区域
iconSize: 24,
itemHeight: 50,
);
}
// 在线状态筛选(关键代码)
Widget _buildOnlineFilter(BuildContext context, DeviceProvider provider) {
return Row(
children: [
const Text('在线状态:'),
const SizedBox(width: 8),
ToggleButtons(
isSelected: [
provider.selectedOnline == null,
provider.selectedOnline == true,
provider.selectedOnline == false,
],
onPressed: (index) {
bool? online = index == 0 ? null : index == 1 ? true : false;
provider.updateFilterCondition(online: online);
},
// 开发板适配:增大按钮尺寸
minWidth: isTablet ? 80 : 60,
minHeight: isTablet ? 40 : 36,
children: const [
Text('全部'),
Text('在线'),
Text('离线'),
],
),
],
);
}
// 房间分组筛选(关键代码,同类型筛选,省略)
Widget _buildRoomFilter(...) {}
}
【核心适配&体验细节(篇幅重点!)】:
- 多终端布局:
- 平板/开发板:横向筛选栏,所有条件平铺,符合大屏操作习惯,避免折叠导致的操作繁琐;
- 手机端:用ExpansionTile折叠筛选条件,节省屏幕空间,避免筛选栏占满屏幕;
- 开发板触控优化:
- 下拉按钮:iconSize从24增至30,itemHeight从48增至50,提升触控容错率;
- 切换按钮(ToggleButtons):minWidth从60增至80,避免误触“在线”/“离线”;
- 状态可视化:
- 下拉按钮显示当前选中的条件(如“空调”),而非仅显示“设备类型”;
- 在线状态用ToggleButtons,选中状态高亮,直观展示当前筛选条件;
- 空状态处理:
- 若设备列表为空,筛选栏禁用并显示“暂无设备可筛选”,避免用户无效操作;
- 若某类筛选条件无数据(如无窗帘设备),对应的下拉按钮禁用。
步骤3:DeviceListPage集成搜索筛选(仅关键代码)
文件路径:lib/presentation/pages/device_list_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:smart_home_flutter/domain/providers/device_provider.dart';
import 'package:smart_home_flutter/presentation/widgets/device/device_card.dart';
import 'package:smart_home_flutter/presentation/widgets/search/device_search_bar.dart';
import 'package:smart_home_flutter/presentation/widgets/filter/device_filter_bar.dart';
import 'package:smart_home_flutter/core/utils/device_utils.dart';
class DeviceListPage extends StatefulWidget {
const DeviceListPage({super.key});
State<DeviceListPage> createState() => _DeviceListPageState();
}
class _DeviceListPageState extends State<DeviceListPage> {
bool _isTablet = false;
bool _isDayu200 = false;
void initState() {
super.initState();
_initDeviceType(); // 判断终端类型
}
// 判断终端类型(手机/平板/开发板)
Future<void> _initDeviceType() async {
final deviceType = await DeviceUtils.getDeviceType();
setState(() {
_isTablet = deviceType == DeviceType.tablet;
_isDayu200 = deviceType == DeviceType.dayu200;
});
}
Widget build(BuildContext context) {
final provider = Provider.of<DeviceProvider>(context);
return Scaffold(
appBar: AppBar(title: const Text('智能设备')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// 搜索框
DeviceSearchBar(isTablet: _isTablet || _isDayu200),
const SizedBox(height: 16),
// 筛选栏
DeviceFilterBar(isTablet: _isTablet || _isDayu200),
const SizedBox(height: 16),
// 过滤后的设备列表
Expanded(
child: provider.filteredDeviceList.isEmpty
? _buildEmptyResult() // 空结果页面
: GridView.builder(
// 多终端网格适配:开发板/平板2列,手机1列
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: _isDayu200 ? 2 : (_isTablet ? 2 : 1),
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: _isDayu200 ? 2.5 : 3,
),
itemCount: provider.filteredDeviceList.length,
itemBuilder: (context, index) {
final device = provider.filteredDeviceList[index];
return DeviceCard(
device: device,
isTablet: _isTablet || _isDayu200,
);
},
),
),
],
),
),
);
}
// 空结果页面(核心体验优化)
Widget _buildEmptyResult() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.search_off, size: 64, color: Colors.grey),
const SizedBox(height: 16),
const Text(
'未找到匹配的设备',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'请尝试修改搜索关键词或筛选条件',
style: TextStyle(color: Colors.grey),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => Provider.of<DeviceProvider>(context, listen: false).resetFilterCondition(),
child: const Text('重置所有条件'),
),
],
),
);
}
}
【核心亮点详解】:
- 多终端网格布局:
- DAYU200开发板/平板:2列展示设备卡片,充分利用大屏空间;
- 手机端:1列展示,避免卡片过小导致操作不便;
- 卡片宽高比:开发板端设为2.5,比手机端(3)更宽,提升触控区域;
- 空结果页面:
- 图文结合:用“Icons.search_off”图标+文字提示,比单纯文字更直观;
- 引导操作:提供“重置所有条件”按钮,用户无需手动清空关键词/筛选条件,提升体验;
- 状态联动:
- 列表直接使用
provider.filteredDeviceList,而非原始列表,保证展示的是过滤后的数据; - 终端类型判断放在initState,避免每次build都执行,提升性能。
- 列表直接使用
模块4:OpenHarmony专属适配(跨终端核心)
Day6的鸿蒙适配重点在“大屏布局+触控体验+存储兼容”,解决开发板/平板端的高频问题:
步骤1:大屏布局适配(核心解决“布局混乱”)
- 禁止屏幕旋转:开发板端固定横屏,避免旋转后布局错乱,代码:
// 在main.dart的MaterialApp中配置 builder: (context, child) { return MediaQuery( data: MediaQuery.of(context).copyWith( orientation: Orientation.landscape, // 开发板固定横屏 ), child: child!, ); }, - 文字缩放适配:开发板端文字放大20%,提升可读性,代码:
textScaleFactor: _isDayu200 ? 1.2 : 1.0, - 触控区域兜底:所有筛选按钮、搜索框的最小触控区域≥48dp,符合鸿蒙开发规范。
步骤2:存储适配(解决SharedPreferences失效)
若开发板端SharedPreferences无法使用,替换为鸿蒙原生存储API(仅关键代码):
Flutter端调用鸿蒙原生存储:
// lib/core/channel/ohos_storage_channel.dart
import 'package:flutter/services.dart';
class OhosStorageChannel {
static const MethodChannel _channel = MethodChannel('smart_home/ohos_storage');
// 保存分组状态
static Future<void> saveSelectedRoom(String room) async {
try {
await _channel.invokeMethod('saveString', {'key': 'selected_room', 'value': room});
} catch (e) {
_logger.e('保存分组状态失败:$e');
}
}
// 获取分组状态
static Future<String> getSelectedRoom() async {
try {
final result = await _channel.invokeMethod('getString', {'key': 'selected_room'});
return result as String? ?? '全部';
} catch (e) {
_logger.e('获取分组状态失败:$e');
return '全部';
}
}
}
鸿蒙原生端实现(ArkTS):
// ohos/entry/src/main/ets/ability/MainAbility.ts
import preferences from '@ohos.data.preferences';
// 注册存储方法
channel.registerMethod('saveString', async (data) => {
const { key, value } = data;
const pref = await preferences.getPreferences(this.context, 'smart_home_pref');
await pref.putString(key, value);
await pref.flush();
return true;
});
channel.registerMethod('getString', async (data) => {
const { key } = data;
const pref = await preferences.getPreferences(this.context, 'smart_home_pref');
return await pref.getString(key, '全部');
});
【核心说明】:原生存储逻辑和SharedPreferences一致,仅更换调用方式,业务层无需修改核心逻辑。
步骤3:性能适配(解决大屏卡顿)
- 列表懒加载:开发板端使用
ListView.builder/GridView.builder,而非ListView,避免一次性构建所有卡片; - 过滤逻辑异步化:大量设备时,用
compute将过滤逻辑放到子线程,避免阻塞UI:Future<void> _filterDevices() async { if (_deviceList.isEmpty) { _filteredDeviceList = []; return; } // 异步过滤 final params = { 'allDevices': _deviceList, 'keyword': _searchKeyword, 'type': _selectedType, 'online': _selectedOnline, 'room': _selectedRoom, }; _filteredDeviceList = await compute(_filterWithParams, params); notifyListeners(); } // 过滤方法(必须是顶层函数,不能是类方法) List<BaseDevice> _filterWithParams(Map<String, dynamic> params) { return DeviceFilterUtil.filterDevices( allDevices: params['allDevices'], keyword: params['keyword'], type: params['type'], online: params['online'], room: params['room'], ); }
模块5:多终端测试验证(确保全场景可用)
Day6的测试重点在“多条件组合+性能+适配”,覆盖以下核心场景:
| 测试终端 | 测试重点 | 核心用例 |
|---|---|---|
| 手机 | 基础功能、折叠筛选栏 | 1. 输入关键词“客厅”→筛选空调→筛选在线;2. 切换分组→重置条件;3. 控制设备后列表是否刷新 |
| 平板 | 横向筛选栏、网格布局 | 1. 筛选条件平铺是否溢出屏幕;2. 2列卡片触控是否便捷;3. 大量设备(100台)时滑动是否卡顿 |
| DAYU200 | 触控体验、存储、性能 | 1. 下拉筛选按钮是否易点击;2. 分组状态重启后是否保留;3. 过滤100台设备耗时是否<100ms |
【性能测试指标】:
- 搜索关键词响应时间:<100ms;
- 筛选条件切换响应时间:<50ms;
- 100台设备过滤耗时:<100ms;
- 列表滑动帧率:≥60fps(无掉帧)。
四、Day6开发总结(个性化+落地性)
Day6的开发核心是“效率+体验+适配”——通过搜索筛选分组解决了设备查找效率问题,通过防抖、空结果页面、状态持久化提升了用户体验,通过多终端布局、触控优化解决了鸿蒙跨终端适配问题。
从技术层面,我们做到了:
- 逻辑分层:搜索筛选逻辑封装在工具类+业务层,UI层仅负责交互,代码复用性高(后续新增“按品牌筛选”仅需扩展工具类和筛选栏);
- 性能优化:防抖、异步过滤、懒加载,保证大数据量下的流畅性;
- 体验细节:空结果提示、加载反馈、状态可视化,避免用户疑惑;
- 鸿蒙适配:大屏布局、触控放大、原生存储兼容,覆盖开发板/平板/手机全终端。
从踩坑角度,Day6的高频坑集中在“数据隔离(原始列表vs过滤列表)”“状态联动(控制后列表刷新)”“鸿蒙存储兼容”,这些坑看似小,但直接影响功能稳定性,也是新手和资深开发者的核心差距——新手只关注“功能实现”,而资深开发者会关注“边界场景+性能+体验”。
接下来Day7我们将聚焦“ObjectBox本地持久化”,解决“断网后无法查看设备列表”的问题,同时复用Day6的搜索筛选逻辑,实现离线状态下的设备查找和管理。
五、个性化结尾
如果你在Day6的开发中遇到了“筛选后列表不刷新”“开发板筛选按钮点不动”“分组状态重启后丢失”等问题,欢迎在评论区留言讨论——这些问题都是鸿蒙跨终端开发中“体验层”的核心坑,解决这些问题,你的APP会从“能用”真正变成“易用”。
另外,提醒大家:搜索筛选的性能优化是持续的,如果你的APP需要支持上千台设备,建议进一步优化(如本地数据库索引、分页过滤),但对于普通智能家居场景(<200台设备),本文的方案完全足够。
下一篇Day7,我们继续攻克“离线可用”的问题,让你的智能家居APP在断网时也能正常使用!
更多推荐




所有评论(0)