用户体验优化:商品局部刷新策略与实现

一、需求描述

在快速变动的电商类APP中需要刷新商品信息尤其是数据,以保证商品信息的准确性。在保证数据刷新的同时,因用户的阅览习惯,在刷新的数据时还需要保证浏览位置不变动。因此,我们决定讨论如何能更用户更友好的进行局部刷新。

让我们分析先来这一类商品的共同点与要求:

  • 商品一般为快速消费: 变动速度快.商品信息进度往往在几分钟内变动数次甚至下架。
  • 商品信息变化大:及时性要求高,需要对用户做出快速反馈
  • 一般收到受到客观限制:商品打折时间,库存等物理或活动上的限制
  • 刷新不能过于频繁,要权衡服务器压力
淘抢购界面(场景例图)

因此,我们需要平衡刷新的时效性与流量限制,抉择刷新策略和机制。

一、刷新机制的策略讨论

经过一些方案与谈论,我们有了初始的两种策略。
基于两种游标的刷新策略:


刷新策略
1.页标游标刷新 (左)

使用页标与游标刷新:以当前页面为页标为记录,接入上拉与下拉加载功能。用以保证上拉与下拉后的数据为最新。

优势:能保证数据始终最新,能获得最新的商品信息。时效性及时。
劣势:处理逻辑复杂,需要加上拉加载功能并和上拉刷新兼容,工作量巨大。需要处理因数据页码变动产生的商品重复。本屏刷新策略与上下屏不一致,逻辑判断情况多。

2.唯一标识刷新 (右)

使用唯一标识刷新:保持已有的原数据与商品列表,通过唯一标识,只抓取商品的最新数据。进行时间,库存的刷新。(新数据的同步和抓取交于另一个定时器,数据刷新只负责取最新数据)

优势:最小化原则,职能单一,逻辑实现简单。要求简单,单个或多个键值唯一确定一个商品数据即可。最大的优势是用户体验好,感知度较低。既能实时刷新数据,又不会影响使用的直观性。因数据不会变动,位置回滚操作也不需要额外的工作。
劣势:不能即时的获取新上架的商品信息。如果商品为组合类商品,不容易通过唯一标示查找不能使用。请求次数较零散与频繁。且若该商品已下架需要通过标识告诉用户结果,以免误认为发生bug。

对比了以上两种方案,我们最终选择了处理逻辑统一,用户使用不太突兀的方案(2),并对初始方案进行了优化,以减少请求量,平衡服务器压力。因刷新场景的常见性,我们做成了SDK的形式希望在能多个项目中使用。

二、缓存策略

触发流程:

  • 刷新机制: 用户滑动停止触发
    • 获取刷新id数组与位置(IndexPath)
    • 过滤数据,返回需要刷新的数组id
    • 通过代理返回id数组及位置等信息
  • -> 控制器发送请求
    • -> 刷新数据更新时间

方案文案如下:

缓存策略

三、系统优化

1.处理用户拖动误触发,进行函数防抖

    // 请求,是否对过滤数据
    private func pullShopItemsData(filter: Bool = false){
        // 0.5秒函数防抖, 合并请求再发送
        self.timer?.invalidate()
        self.timer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(refreshOperaction(timer:)), userInfo: ["refreshRightNow":filter], repeats: false)
    }

2.预测用户浏览轨迹,进行页面上下半屏预刷新

    if (maxRow != -1 && minRow != listData.count+1) {
        // 上下两屏幕
        NSInteger rest = (10 - refreshIdList.count) > 0 ?(10 - refreshIdList.count):0;
        NSInteger beforeNum = floor(rest/2.0);
        // 前半屏
        NSInteger startNum = (minRow - beforeNum>0?minRow - beforeNum:0);
        for (NSInteger i = startNum; i<minRow; i++) {
            NSIndexPath *path = [NSIndexPath indexPathForRow:i inSection:4];
            ShopItemsDataSourceModel *model =  [[ShopItemsDataSourceModel alloc] initWithSourceData:listData[i] indexPath:path];
            [refreshIdList addObject:model];
        }
        // 后半屏
        NSInteger afterNum = rest - beforeNum;
        NSInteger endNum = maxRow+afterNum < listData.count-1 ? maxRow+afterNum : listData.count-1;
        
        for (NSInteger i = maxRow+1; i<=endNum; i++) {
            NSIndexPath *path = [NSIndexPath indexPathForRow:i inSection:4];
            ShopItemsDataSourceModel *model =  [[ShopItemsDataSourceModel alloc] initWithSourceData:listData[i] indexPath:path];
            [refreshIdList addObject:model];
        }
    
    }
    return refreshIdList;

3.减少请求数方差,使用旧数据填充

保证及时性,发送请求前进行最小请求量合并,进行节流。

    // 请求数不足,填充老数据
    private func itemsWithFillOldData(originalRefresItems: RefreshItemsInfoStruct, toNum minNum: Int) -> RefreshItemsInfoStruct {
        var filledItems: RefreshItemsInfoStruct = originalRefresItems
        let minRequestNum = minNum - originalRefresItems.count;
        let sortedArray = self.refreshInfoDict
            .filter({return !originalRefresItems.keys.contains($0.key)}) // 除去已确定要刷新元素
            .sorted(by: {$0.value.lastRefreshStamp < $1.value.lastRefreshStamp}) // 根据刷新时间排序
        
        let maxIndex = minRequestNum < (sortedArray.count)
            ? minRequestNum - 1
            : (sortedArray.count) - 1
        
        if ((sortedArray.count) > 0) {
            // 切割数组
            let sortedArray = sortedArray[0...maxIndex]
            for infoTuple in sortedArray {
                filledItems.updateValue(infoTuple.value, forKey: infoTuple.key)
            }
        }
        return filledItems;
    }

4.定时器周期性刷新

四、SDK 类图

刷新机制类图.png

使用代理模式, 传入想要刷新的cell信息,代理者只负责判断是否数据过期,返回需要刷新的列表,具体刷新操作由使用者去做。

五、接入说明

1.初始化代理,遵循 ShopItemsRefreshProxyProtocol 协议
// 初始化商品刷新代理
self.proxy = [[ShopItemsRefreshProxy alloc] initWithRefreshMaster:self];
2.实现两个 数据来源 与 请求操作 两个代理方法
// 数据来源
- (NSArray<ShopItemsPrepareInfoModel *> *)shopItemsPrepareToRefresh{
    NSMutableArray<ShopItemsPrepareInfoModel *> *refreshIdList = [NSMutableArray array];
    refreshIdList = //视野内你能见到的cell
    return refreshIdList; // 传入所有你想刷新的cell,是否该刷新由代理决定
}
 
// 数据操作
- (void)shopItemsRefreshOperation:(ShopItemsRefreshProxy *)refreshProxy
                   itemsToRefresh:(NSDictionary<NSString *,ShopItemsInfoModel *> *)itemsToRefresh
                  allItemsInfoDic:(NSDictionary<NSString *,ShopItemsInfoModel *> *)allItemsInfoDic {
    __weak typeof(self) weakSelf = self;
     
    [self showHomeLoadView]; // 刷新动画
// 发送请求服务
    [refreshItemsService refreshHomeCellWithRefreshIdDic:itemsToRefresh
                                          completeHandle:^(NSDictionary<NSString *,id> * _Nullable respond, NSError * _Nullable error) {
        // ...请求成功,刷新数据源
        [refreshProxy updateRefreshTimeWithItems:refreshItems];// 刷新数据的跟新时间
        [self hideHomeLoadView];// 停止刷新动画
    }
}

3.进行刷新

// 在拖拽结束方法进行调用
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
    if (self.proxy) {
        [self.proxy pullIntelligent]; // 进行刷新
    }
}
注: 商品刷新SDK 现因仅在我们的应用中使用,暂未集成到cocoapods。如果有类似的使用需求,在评论区@我即可。我会后续集成进去。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • ORA-00001: 违反唯一约束条件 (.) 错误说明:当在唯一索引所对应的列上键入重复值时,会触发此异常。 O...
    我想起个好名字阅读 5,429评论 0 9
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 173,122评论 25 708
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,883评论 2 59
  • 占位,待更新 大年30的晚上,太忙碌了,都没有及时更新打卡,占个位置还占出来一个坑啊。 大年30晚上我们在家里吃好...
    巧巧姐阅读 154评论 0 0
  • Android 5.0中新增了ripple类型,即波纹效果这里要注意,波纹效果只在5.0以上的设备生效,要实现此种...
    DevWang阅读 8,596评论 7 52