You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter+Getx联系人获取性能优化:缓存与分批加载咨询

优化Flutter Getx联系人获取性能(1500+联系人场景)

当前使用flutter_contacts 1.1.9+2结合Getx实现联系人获取,但1500+联系人全量加载导致UI卡顿、性能下降,以下从缓存机制和分批加载两个方向给出优化方案。

一、缓存机制:避免重复全量拉取

缓存完全有效,核心思路是首次加载后将联系人数据持久化存储,后续启动优先读取缓存,同时后台静默同步最新联系人,既保证启动速度,又能获取最新数据。

实现步骤

  1. 用shared_preferences做持久化存储(需将Contact对象序列化为JSON,无法直接存储复杂对象)
  2. 记录缓存更新时间,通过FlutterContacts.getContactsLastUpdated()对比系统联系人更新时间,判断是否需要刷新缓存
  3. 启动时先加载缓存展示,后台异步检查并更新缓存,更新完成后刷新UI

缓存实现代码

import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:get/get.dart';

class PhoneBookController extends GetxController{
  RxList<Contact> filteredContacts = <Contact>[].obs;
  RxList<Contact> allContacts = <Contact>[].obs;
  RxBool isLoading = true.obs;
  RxBool isRefreshing = false.obs;
  late SharedPreferences _prefs;
  final String _cacheKey = 'contacts_cache';
  final String _cacheTimeKey = 'contacts_cache_time';

  @override
  void onInit() {
    super.onInit();
    _initPrefs();
    _loadCachedContacts();
  }

  Future<void> _initPrefs() async {
    _prefs = await SharedPreferences.getInstance();
  }

  Future<void> _loadCachedContacts() async {
    try {
      final cachedData = _prefs.getString(_cacheKey);
      final cachedTime = _prefs.getInt(_cacheTimeKey);
      if (cachedData != null && cachedTime != null) {
        // 解析缓存的联系人数据
        final List<dynamic> jsonList = jsonDecode(cachedData);
        final cachedContacts = jsonList.map((json) => Contact.fromJson(json)).toList();
        allContacts.value = cachedContacts;
        filteredContacts.value = cachedContacts;
      }
      // 后台同步最新联系人
      _syncLatestContacts();
    } catch (e) {
      // 缓存加载失败,直接拉取最新数据
      await fetchContacts();
    } finally {
      isLoading.value = false;
    }
  }

  Future<void> _syncLatestContacts() async {
    isRefreshing.value = true;
    try {
      if (await FlutterContacts.requestPermission()) {
        final lastUpdated = await FlutterContacts.getContactsLastUpdated();
        final cachedTime = _prefs.getInt(_cacheTimeKey);
        // 对比更新时间,判断是否需要拉取最新数据
        if (cachedTime == null || lastUpdated.millisecondsSinceEpoch > cachedTime) {
          final contacts = await FlutterContacts.getContacts(withProperties: true, withPhoto: true);
          if (contacts.isNotEmpty) {
            allContacts.value = contacts;
            filteredContacts.value = contacts;
            // 更新缓存
            final jsonList = contacts.map((contact) => contact.toJson()).toList();
            await _prefs.setString(_cacheKey, jsonEncode(jsonList));
            await _prefs.setInt(_cacheTimeKey, lastUpdated.millisecondsSinceEpoch);
          }
        }
      }
    } catch (e) {
      Get.snackbar('同步失败', '无法获取最新联系人');
    } finally {
      isRefreshing.value = false;
    }
  }

  // 保留原有的fetchContacts方法作为主动刷新用
  Future<void> fetchContacts() async {
    try {
      isLoading.value = true;
      if (await FlutterContacts.requestPermission()) {
        final contacts = await FlutterContacts.getContacts(withProperties: true, withPhoto: true);
        if (contacts.isNotEmpty) {
          allContacts.value = contacts;
          filteredContacts.value = contacts;
          // 更新缓存
          final jsonList = contacts.map((contact) => contact.toJson()).toList();
          await _prefs.setString(_cacheKey, jsonEncode(jsonList));
          final lastUpdated = await FlutterContacts.getContactsLastUpdated();
          await _prefs.setInt(_cacheTimeKey, lastUpdated.millisecondsSinceEpoch);
        } else {
          Get.snackbar('无联系人', '未找到任何联系人');
        }
      } else {
        Get.snackbar('权限拒绝', '请授予联系人访问权限');
      }
    } catch (e) {
      Get.snackbar('错误', '获取联系人失败');
    } finally{
      isLoading.value = false;
    }
  }
}

二、分批加载:减少单次加载压力

利用flutter_contacts的offset和limit参数实现分批拉取,结合ListView滚动监听,滚动到底部时自动加载下一批,避免一次性加载大量联系人导致UI阻塞。

实现步骤

  1. 添加分页变量:当前偏移量currentOffset、每页加载数量pageSize、是否还有更多数据hasMore
  2. 实现分批拉取方法fetchBatchContacts,每次拉取指定数量的联系人
  3. 监听ListView滚动事件,触发下一批加载
  4. 处理加载状态,避免重复请求

分批加载实现代码

import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:get/get.dart';

class PhoneBookController extends GetxController{
  RxList<Contact> filteredContacts = <Contact>[].obs;
  RxList<Contact> allContacts = <Contact>[].obs;
  RxBool isLoading = true.obs;
  RxBool isLoadingMore = false.obs;
  RxInt currentOffset = 0.obs;
  final int pageSize = 50; // 每页加载50条,可根据设备性能调整
  RxBool hasMore = true.obs;

  @override
  void onInit() {
    super.onInit();
    fetchBatchContacts();
  }

  Future<void> fetchBatchContacts() async {
    if (isLoadingMore.value || !hasMore.value) return;
    isLoadingMore.value = true;
    try {
      if (await FlutterContacts.requestPermission()) {
        final contacts = await FlutterContacts.getContacts(
          withProperties: true,
          withPhoto: true,
          offset: currentOffset.value,
          limit: pageSize,
        );
        if (contacts.isNotEmpty) {
          allContacts.addAll(contacts);
          filteredContacts.addAll(contacts);
          currentOffset.value += pageSize;
          // 判断是否还有更多数据
          hasMore.value = contacts.length == pageSize;
        } else {
          hasMore.value = false;
        }
      } else {
        Get.snackbar('权限拒绝', '请授予联系人访问权限');
      }
    } catch (e) {
      Get.snackbar('错误', '获取联系人失败');
    } finally{
      isLoading.value = false;
      isLoadingMore.value = false;
    }
  }

  // 重置分页,用于搜索或刷新场景
  void resetPagination() {
    currentOffset.value = 0;
    hasMore.value = true;
    allContacts.clear();
    filteredContacts.clear();
    isLoading.value = true;
    fetchBatchContacts();
  }
}

UI层滚动监听示例

Obx(() {
  return ListView.builder(
    itemCount: controller.allContacts.length + (controller.hasMore.value ? 1 : 0),
    itemBuilder: (context, index) {
      if (index == controller.allContacts.length) {
        // 加载更多提示
        return const Center(child: CircularProgressIndicator());
      }
      final contact = controller.allContacts[index];
      // 渲染联系人项
      return ListTile(
        leading: contact.photo != null ? CircleAvatar(backgroundImage: MemoryImage(contact.photo!)) : const CircleAvatar(child: Icon(Icons.person)),
        title: Text(contact.displayName),
        subtitle: Text(contact.phones.isNotEmpty ? contact.phones.first.number : ''),
      );
    },
    controller: ScrollController()..addListener(() {
      // 滚动到底部时触发加载更多
      final scrollController = ScrollController();
      if (scrollController.position.pixels == scrollController.position.maxScrollExtent) {
        controller.fetchBatchContacts();
      }
    }),
  );
})

三、综合优化建议

  1. 结合缓存与分批加载:首次启动先加载缓存的分批数据,后台同步最新联系人时也用分批方式,进一步降低性能消耗
  2. 延迟加载照片:照片属于大资源,可在列表项滚动到可见区域时再加载,避免一次性加载所有照片导致内存飙升
  3. 优化搜索逻辑:搜索时直接在已加载的联系人中过滤,避免重新拉取数据

内容的提问来源于stack exchange,提问作者Nammar Mahsud

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 06:15:13