广告数据从网络返回后,如何被插入到视频流 RecyclerView 中

流程图

callback.onSuccess(List<AdItemModel>)


 ┌──────────────────────────────────────────────┐
 │ AdAbsPosStrategy.tryRequest() 的回调内部       │
 │   ① adControlSetReqPos()  标记请求时用户位置   │
 │   ② filterValidAd()       过滤无效广告         │
 │   ③ offerListToQueue()    放入本地队列         │
 │   ④ onRequestSuccess()    触发一次 tryInsert   │
 └─────────────────────┬────────────────────────┘


 ┌──────────────────────────────────────────────┐
 │ AdAbsPosStrategy.tryInsert()                  │
 │   ① getTargetList()         拿到 flowList     │
 │   ② getNextAd()             从队列取广告       │
 │   ③ getInsertTargetPos()    计算目标楼层       │
 │   ④ checkInsertCondition()  校验能否插入       │
 │   ⑤ helper.insertAd()      执行插入           │
 └─────────────────────┬────────────────────────┘


 ┌──────────────────────────────────────────────┐
 │ FlowVideoHelper.insertAd()                   │
 │   ① dodge()                避让冲突位置       │
 │   ② makeAdElement()        包装为 FlowItem    │
 │   ③ list.add(pos, element) 插入 flowList      │
 └─────────────────────┬────────────────────────┘


 ┌──────────────────────────────────────────────┐
 │ adControlNotifyInsert()                       │
 │   → notifyItemRangeInserted(position, 1)      │
 │   → RecyclerView 刷新                         │
 └──────────────────────────────────────────────┘

参与角色

角色文件职责
AdPositionPluginvideo/feedflow/ad/position/AdPositionPlugin.ktRedux 插件,监听滑动,驱动流程
AdPosStrategyManagerlib_ad/.../strategy/AdPosStrategyManager.kt策略管理器,路由到具体策略
AdAbsPosStrategylib_ad/.../strategy/AdAbsPosStrategy.kt策略基类,控制”何时请求""何时插入”
FlowVideoHelpervideo/feedflow/ad/position/FlowVideoHelper.kt执行者,负责包装、避让、插入列表

代码

1. 入口:监听用户滑动

// AdPositionPlugin.kt
 
class AdPositionPlugin {
 
    private val posMgrPortrait by lazy {
        AdPosStrategyManager(helperPortrait).apply {
            setRequestCallback(requestCb)
            setInsertCallback(insertCb)
        }
    }
 
    // helperPortrait 就是 FlowVideoHelper
    // 按照不同的入口区分不同的 Helper
    private val helperPortrait by lazy {
        AdRedux.getHelper(manager, FlowStyle.PORTRAIT)
    }
 
    fun onAttachToManager() {
        // RecyclerView 选中
        positionSelected.observe { data ->
            val mgr = getCurrentPosManager()
            mgr.run {
                onPositionSelected(data.position, data.isUp)
                // 尝试请求
                tryRequest(data.position, data.isUp)
                // 尝试插入
                tryInsert(data.position, data.isUp, ...)
            }
        }
    }
}

每次用户滑动,。选中,tryRequesttryInsert都会被调用。请求是异步的,插入是同步的。队列里有广告就插,没有就跳过。

2. 请求成功、过滤、入队

// AdAbsPosStrategy.kt
 
fun tryRequest(position: Int, isUp: Boolean) {
    if (isRequestProcessing) return
    if (!shouldRequest(position, isUp)) return
 
    isRequestProcessing = true
 
    helper.requestAd(object : IAdRequestCallback<AdItemModel> {
 
        override fun onSuccess(adList: MutableList<AdItemModel>) {
            // 1. 标记请求时的用户位置(后续用于计算楼层间隔)
            adList.forEach { ad ->
                helper.adControlSetReqPos(ad, helper.getListState().currentReqPos)
            }
 
            // 2. 过滤无效广告(去重、下线、空订单等)
            helper.filterValidAd(adList)
 
            // 3. 放入本地队列
            offerListToQueue(adList)
 
            isRequestProcessing = false
 
            // 4. 请求回来后立刻尝试插入一次
            // 内部调用 tryInsert()
            onRequestSuccess(position, isUp)
        }
 
        override fun onFail() {
            isRequestProcessing = false
        }
    })
}

广告不是一回来就直接插入列表,而是先进队列,等tryInsert判断时机合适再插

3. 计算目标楼层、校验、执行插入

// AdAbsPosStrategy.kt
 
fun tryInsert(position: Int, isUp: Boolean, extra: AdInsertExtra? = null) {
    // FlowState.flowList(RecyclerView 数据源)
    val list = helper.getTargetList()
    // 从队列 peek 一条广告
    val ad = helper.getNextAd(...)         
 
    if (ad == null) return                 // 队列空,跳过
 
    // 策略子类各自实现楼层计算:
    //   AdDynamic2PosStrategy           — 动态混排(时间+楼层双维度)
    //   NadIntelligentControlPosStrategy — 智能控制(服务端下发参数)
    //   NadResetSortPosStrategy         — 重排序策略
    // ... 等等等
    val targetPos = getInsertTargetPos(position, list, ad)
 
    // 校验:楼层间隔够不够、方向对不对、有没有重复...
    val ret = checkInsertCondition(isUp, position, targetPos, list, ad)
    if (ret != AdPosInsertCase.Accept) return
 
    // 交给 FlowVideoHelper 执行插楼,然后返回插到的楼层
    val finalPos = helper.insertAd(ad, targetPos, list, extra)
 
    if (finalPos > 0) {
        helper.getListState().getAdQueue<AdItemModel>()?.remove(ad)  // 从队列移除
        helper.getListState().setLastAd(ad)                          // 记录"上一条广告"
        helper.getListState().lastAdPos = finalPos                   // 记录"上一条广告位置"
        insertCb?.onSuccess(finalPos, ad)                            // 通知 Plugin
    }
}

getInsertTargetPos()是抽象方法,不同策略有不同的楼层计算逻辑。checkInsertCondition()做最后的兜底校验,不通过就不插

4. 避让、包装、插入列表

// FlowVideoHelper.kt
 
override fun insertAd(
    ad: AdItemModel,
    targetPos: Int,
    list: MutableList<FlowItem<*>>,   // 就是 FlowState.flowList
    extra: AdInsertExtra?
): Int {
    // ... 省略部分禁止插楼逻辑
    
    // 1.避让:目标位置如果是直播/资讯/敏捷卡/离线缓存/相似集合什么的等等吧
    // 不能放广告的位置,向前或向后偏移
    val finalPos = dodge(list, targetPos, ad, true)
 
    // 2. 空订单(adType == "3")不真正插入,只标记咨询为空订单
    if (ad.adType == EMPTY_ORDER_AD_TYPE) {
        markUgcAsEmptyOrder(ad, list[finalPos])
        return finalPos
    }
 
    // 3. 记录插入触发方式(滑动触发 / 请求完成触发 / 定时触发)
    ad.runTime.insertTrigger = extra?.triggerCase?.getTriggerCase()
 
    // 4. 包装为 FlowItem
    val adElement = makeAdElement(ad)
 
    // 5. 插入列表
    CollectionUtils.add(list, adElement, finalPos)
 
    return finalPos
}

5. 包装:AdItemModel → FlowItem

// 文件: FlowVideoHelper.kt
 
override fun makeAdElement(ad: AdItemModel): FlowItem<*> {
    return FlowItem(
        id = ad.id,
        nid = ad.nid,
        layout = ad.layout,
        data = ad              // 👈 AdItemModel 作为 payload
    ).apply {
        // 反向弱引用,方便后续通过 AdItemModel 找到对应的列表项
        ad.runTime.itemModelWeakRef = WeakReference(this)
    }
}
 
// FlowItem 就是 ItemModel 的别名:
// typealias FlowItem<MODEL> = ItemModel<MODEL>
//
// data class ItemModel<MODEL>(
//     var id: String,
//     var nid: String,
//     var layout: String,
//     val data: MODEL,          // 👈 泛型 payload
//     var commonItemData: CommonItemData? = null
// )

6. 通知 RecyclerView 刷新

// 文件: FlowVideoHelper.kt
 
override fun adControlNotifyInsert(ad: AdItemModel, position: Int) {
    managerRef.get()
        ?.getService(IFlowComponentService::class.java)
        ?.notifyItemRangeInserted(position, 1)
    // RecyclerView.Adapter.notifyItemRangeInserted()
    // 用户滑到该位置时触发 onBindViewHolder → onBindData
}

插入完成。此时FlowState.flowListfinalPos处多了一个FlowItem<AdItemModel>,RecyclerView 感知到数据变化,用户滑到该位置时进入详情渲染流程。

视频流广告 - 完整数据流

从用户滑动 → 网络请求 → 数据解析 → 层层转换 → 插楼 → UI 渲染

全景流程图

用户滑动视频流


 ┌──────────────────────────────────────────────────────────────┐
 │ ① 监听滑动 & 触发请求                                        │
 │    AdPositionPlugin 监听 ViewPager positionSelected           │
 │    → AdPosStrategyManager.tryRequest()                       │
 └──────────────────────────┬───────────────────────────────────┘


 ┌──────────────────────────────────────────────────────────────┐
 │ ② 构建请求参数                                                │
 │    AdListParam (~50个参数, 含广告历史、session、楼层策略等)      │
 │    ↓ .to2DataMap()                                            │
 │    Map<String, String>                                        │
 └──────────────────────────┬───────────────────────────────────┘


 ┌──────────────────────────────────────────────────────────────┐
 │ ③ 发起网络请求                                                │
 │    AdListRepositoryImpl                                       │
 │      └→ AdListApi.requestData(post, callback)                 │
 │           └→ (继承自 CommonApi<AdListBean>)                    │
 │    用 suspendCoroutine 将回调转为协程                           │
 └──────────────────────────┬───────────────────────────────────┘
                            │ Gson 自动反序列化

 ┌──────────────────────────────────────────────────────────────┐
 │ ④ DTO 层:原始数据 (Bean)                                     │
 │    AdListBean                                                 │
 │      ├─ items: List<AdListItemBean>                           │
 │      │    └─ data: AdListItemDataBean  ← 130+ 字段, 大量String │
 │      ├─ eshopItems (电商广告)                                  │
 │      └─ adPolicy: AdPolicyBean (楼层控制策略)                   │
 └──────────────────────────┬───────────────────────────────────┘
                            │ FlowAdItemMapper (批量)
                            │   └→ FlowAdVideoItemMapper (单条)
                            │        └→ 50+ 子 Mapper 协作

 ┌──────────────────────────────────────────────────────────────┐
 │ ⑤ Domain 层:业务模型 (Model)                                 │
 │    List<AdItemModel>  (~90 个强类型字段)                        │
 │    + AdRunTime (可变运行时状态)                                 │
 └──────────────────────────┬───────────────────────────────────┘
                            │ callback.onSuccess(adList)

 ┌──────────────────────────────────────────────────────────────┐
 │ ⑥ 过滤 & 入队                                                │
 │    filterValidAd() → offerListToQueue()                       │
 └──────────────────────────┬───────────────────────────────────┘
                            │ tryInsert()

 ┌──────────────────────────────────────────────────────────────┐
 │ ⑦ 插楼                                                       │
 │    getInsertTargetPos() 计算目标楼层                            │
 │    → dodge() 避让冲突位置                                      │
 │    → makeAdElement() 包装为 FlowItem<AdItemModel>              │
 │    → list.add(finalPos, adElement) 插入 FlowState.flowList     │
 │    → notifyItemRangeInserted() 通知 RecyclerView               │
 └──────────────────────────┬───────────────────────────────────┘
                            │ 用户滑到该位置

 ┌──────────────────────────────────────────────────────────────┐
 │ ⑧ 详情数据准备                                                │
 │    AdVideoItemComponent.onBindData()                          │
 │    → dispatch AdDetailDataAction                              │
 └──────────────────────────┬───────────────────────────────────┘


 ┌──────────────────────────────────────────────────────────────┐
 │ ⑨ Redux Reducer:状态计算                                     │
 │    AdDetailReducer.reduce()                                   │
 │      ├→ AdDetailMapper  → FlowDetailModel → FlowDetailState   │
 │      └→ AdDataMapper    → AdData           → AdDataState       │
 │    + 后处理:cmd宏替换、下载信息注入、实时策略修正                │
 └──────────────────────────┬───────────────────────────────────┘
                            │ NetAction.Success(adData) 广播

 ┌──────────────────────────────────────────────────────────────┐
 │ ⑩ UI 组件消费 (60+ 个 Component)                              │
 │    通过 AdRedux.getAdData(store) 读取 AdData                   │
 │    NadSummaryComponent, AdTailFrameComponent,                 │
 │    AdPlayerComponent, NadSvButtonComponent ...                 │
 └──────────────────────────────────────────────────────────────┘

关键文件索引

步骤文件路径 (相对 ad_business/flowvideo/src/main/java/)
监听滑动AdPositionPlugin.ktvideo/feedflow/ad/position/
策略基类AdAbsPosStrategy.ktlib_ad/.../position/strategy/
请求参数AdListParam.ktflowvideo/ad/repos/
网络请求AdListRepositoryImpl.ktflowvideo/ad/repos/
API 接口AdListApi.ktflowvideo/ad/api/
DTO BeanAdListBean.ktflowvideo/ad/api/
批量 MapperFlowAdItemMapper.ktvideo/feedflow/ad/flow/
单条 MapperFlowAdVideoItemMapper.ktvideo/feedflow/ad/flow/
Domain ModelAdItemModel.ktvideo/feedflow/ad/flow/
插楼执行FlowVideoHelper.ktvideo/feedflow/ad/position/
详情 ReducerAdDetailReducer.ktvideo/feedflow/ad/detail/
AdData MapperAdDataMapper.ktvideo/feedflow/ad/detail/
详情页 ModelAdData.ktvideo/feedflow/ad/detail/
State 容器AdDataState.ktvideo/feedflow/ad/detail/
数据访问器AdRedux.ktvideo/feedflow/ad/
Item 组件AdVideoItemComponent.ktvideo/feedflow/ad/
Mapper 接口Mapper.ktflowvideo/service/lib-component-ext/.../mapper/
列表数据源FlowState.ktflowvideo/flow/list/
适配器FlowListAdapter.ktflowvideo/flow/list/

逐步代码走读

0. 触发点:用户滑动视频,需要请求广告

// 文件: video/feedflow/ad/position/AdPositionPlugin.kt
// AdPositionPlugin 是广告请求的驱动入口
 
class AdPositionPlugin {
 
    // 竖屏和横屏各有一套策略管理器
    private val posMgrPortrait by lazy {
        AdPosStrategyManager(helperPortrait).apply {
            setRequestCallback(requestCb)
            setInsertCallback(insertCb)
        }
    }
 
    // helperPortrait 就是 FlowVideoHelper
    private val helperPortrait by lazy {
        AdRedux.getHelper(manager, FlowStyle.PORTRAIT)
    }
 
    fun onAttachToManager() {
        // 监听 ViewPager 滑动事件
        positionSelected.observe { data ->
            val mgr = getCurrentPosManager()
            mgr.run {
                onPositionSelected(data.position, data.isUp)
                tryRequest(data.position, data.isUp)     // 👈 尝试请求广告
                tryInsert(data.position, data.isUp, ...)  // 👈 尝试插入广告
            }
        }
    }
}

1. 构建请求参数

// 文件: flowvideo/ad/repos/AdListParam.kt
// ~50 个字段,携带广告请求需要的全部上下文
 
val reqParam = AdListParam(
    da = daJsonObject,                   // 广告公参
    pd = "native_video",                 // 入口标识
    fromFullscreen = "0",                // 0=竖屏, 1=横屏
    adRequestFirstFloor = "3",           // 广告首楼楼层
    page = "detail",                     // 页面类型
    historyAdInfos = JSONArray("[...]"),  // 历史广告(去重/频控)
    context = "{nid:xxx, duration:30, progress:15, liked:1}",
    adSession = "1",                     // 广告会话 ID
    refreshState = "7",                  // 刷新状态
    fromClearScreen = "0",               // 是否清屏模式
    // ... 还有 40+ 参数
)
 
// 调用 toJson() → to2DataMap(),最终变成 Map<String, String> 交给网络层
val postParams: Map<String, String> = reqParam.to2DataMap()

2. 发起网络请求

// 文件: flowvideo/ad/repos/AdListRepositoryImpl.kt
// 用 suspendCoroutine 将回调式 API 桥接为协程
 
class AdListRepositoryImpl(private val listApi: AdListApi) : AdListRepository {
    override suspend fun getAdListData(listParam: AdListParam): Result<AdListBean> {
        return suspendCoroutine { cont ->
            listApi.requestData(
                post = listParam.to2DataMap(),
                callback = object : OnDataLoaded<Result<AdListBean>> {
                    override fun onDataLoaded(data: Result<AdListBean>) {
                        cont.resume(data)  // 👈 回调 → 协程,返回 Result<AdListBean>
                    }
                }
            )
        }
    }
}
 
// 其中 AdListApi 继承自 CommonApi<AdListBean>:
//
// 文件: flowvideo/ad/api/AdListApi.kt
// interface AdListApi : CommonApi<AdListBean>
//
// 文件: common/CommonApi.kt
// interface CommonApi<T> {
//     fun requestData(post: Map<String, String>, callback: OnDataLoaded<Result<T>>?)
// }
//
// 底层由 Gson 根据 AdListBean 的结构自动反序列化 JSON

3. DTO 层:Gson 自动解析为 Bean

// 文件: flowvideo/ad/api/AdListBean.kt
// 后端返回的 JSON 被 Gson 解析为三层嵌套结构:
 
// 后端 JSON:
// {
//   "items": [{
//     "id": "7890",
//     "nid": "sv_7890",
//     "layout": "video",
//     "data": {
//       "title": "精彩直播",
//       "image": "https://cdn.baidu.com/poster.jpg",
//       "videoWidth": "720",
//       "videoHeight": "1280",
//       "operate": "{\"button\":{\"text\":\"立即下载\",\"cmd\":\"baidubox://...\"}}", ← JSON字符串!
//       "extra_info": "{\"ext\":\"abc123...\"}",                                     ← JSON字符串!
//       "tail_frame": "{\"image\":\"...\",\"type\":1}",                              ← JSON字符串!
//       "title_cmd": "baiduboxapp://video/detail?id=123",
//       "live_status": "1",                                                          ← String不是Int!
//       ...
//     }
//   }],
//   "ad_policy": { "client_exp_key": "xxx", ... }
// }
 
// Gson 解析后得到:
val adListBean: AdListBean
// adListBean.items[0].id                     = "7890"
// adListBean.items[0].nid                    = "sv_7890"
// adListBean.items[0].data.operate           = "{\"button\":{...}}"   ← 还是 String!
// adListBean.items[0].data.extra_info        = "{\"ext\":\"...\"}"    ← 还是 String!
// adListBean.items[0].data.liveState         = "1"                    ← 还是 String!
//
// 关键特征:后端把嵌套对象序列化为 String 下发,Bean 层只做"原样接收"

4. 处理响应:Bean → Domain(批量转换)

// 文件: video/feedflow/ad/position/FlowVideoHelper.kt
// 在 requestAdData 的回调中处理响应
 
private fun requestAdData(callback: IAdRequestCallback<AdItemModel>, reqParam: AdListParam) {
    GlobalScope.launch(Dispatchers.Main) {
        when (val responseData = repo.getAdListData(reqParam)) {
 
            is Result.Success -> {
                (responseData.data as? AdListBean)?.let { adListBean ->
 
                    // 处理广告策略(楼层控制)
                    adListBean.adPolicy?.let { policy ->
                        getListState().pvState = policy.pvState
                    }
 
                    // 批量转换:Bean → Domain
                    adListBean.items?.let { items ->
                        val adList = FlowAdItemMapper().map(items)
                        //          👆 这一步是核心!往下展开看
                        callback.onSuccess(adList)  // 👈 返回给策略层
                    }
 
                    // 电商软广单独处理
                    adListBean.eshopItems?.let { items ->
                        val adList = FlowAdItemMapper().map(items)
                        // 转发给电商软广服务
                    }
                }
            }
 
            is Result.Failure -> callback.onFail()
        }
    }
}

5. Mapper 层:DTO → Domain(批量)

// 文件: video/feedflow/ad/flow/FlowAdItemMapper.kt
// 遍历列表,逐条调用 FlowAdVideoItemMapper
 
class FlowAdItemMapper : ListMapper<AdListItemBean?, AdItemModel> {
    override fun map(input: List<AdListItemBean?>): MutableList<AdItemModel> {
        val result = mutableListOf<AdItemModel>()
        for (index in input.indices) {
            input[index]?.let { item ->
                item.data?.let {
                    result.add(FlowAdVideoItemMapper().map(item))
                    //         👆 逐条转换,往下展开看
                }
            }
        }
        return result
    }
}

6. Mapper 层:DTO → Domain(单条,最复杂的一步)

// 文件: video/feedflow/ad/flow/FlowAdVideoItemMapper.kt
// ~260 行,调用 50+ 子 Mapper,将 String 黑盒拆解为强类型对象
 
class FlowAdVideoItemMapper : Mapper<AdListItemBean, AdItemModel> {
    override fun map(input: AdListItemBean): AdItemModel {
        return input.data?.let { bean ->
            AdItemModel(
                // ---- 直传字段 ----
                id = input.id,                            // "7890"
                nid = input.nid,                          // "sv_7890"
                layout = input.layout,                    // "video"
                post = bean.image,                        // 封面 url (image → post)
                title = bean.title,                       // "精彩直播"
                videoInfo = bean.videoInfo,                // 原始 JSON 字符串,留给播放器解析
 
                // ---- JSON 字符串 → 子 Mapper → 强类型对象 ----
                operate = FlowAdOperateMapper().map(bean.operate),
                //        "{\"button\":{\"text\":\"立即下载\"}}"  → FeedAdOperate 对象
 
                extraInfo = FlowAdExtraInfoMapper().map(bean.extra_info),
                //          "{\"ext\":\"abc123\"}"  → AdExt 对象
 
                extraData = FlowAdExtraDataMapper().map(bean.extra_data),
                //          String? → ExtData 对象
 
                tailFrame = FlowAdTailFrameMapper().map(bean.tail_frame),
                //          String? → TailFrame 对象
 
                normandy = AdNormandyMapper().map(bean.normandy),
                //         String? → AdNormandyModel 对象
 
                // ... 还有约 40 个子 Mapper 调用
 
                // ---- 类型转换:String → Int / Boolean ----
                liveState = bean.liveState.toIntOrNull() ?: -1,        // "1" → 1
                tailShow = bean.tail_show.toIntOrNull() ?: 0,          // "2" → 2
                canFollowMove = bean.canFollowMove == 1,               //  1  → true
                chargeByAreaOfflineAbSwitch =
                    (bean.chargeByAreaOfflineAbSwitch.toIntOrNull() ?: 0) == 1,  // "1" → true
 
                // ... 其余字段
 
            ).apply {
                // ---- 后处理:建立交叉引用 ----
                extraData?.adDownload?.desc = this.operate?.desc?.text ?: ""
                popover?.adDownload = this.extraData?.adDownload
                operate?.appInfo?.ext = extraInfo?.extraParams
                softAdType = narClientAdvDict?.softAdType ?: softAdType
            }
        } ?: AdItemModel()
    }
}
 
// 转换完成后:
// adItemModel.titleCmd    = "baiduboxapp://..." (String, 直传)
// adItemModel.liveState   = 1                   (Int, 从 "1" 转换)
// adItemModel.operate     = FeedAdOperate(...)   (对象, 从 JSON String 解析)
// adItemModel.extraInfo   = AdExt(...)           (对象, 从 JSON String 解析)

7. 插楼:AdItemModel 进入视频流列表

这一步涉及 4 个角色协作:

角色职责
AdPositionPluginRedux 插件,监听用户滑动,驱动整个流程
AdPosStrategyManager策略管理器,路由到具体策略
AdAbsPosStrategy策略基类,控制”何时请求”和”何时插入”
FlowVideoHelper执行者,负责网络请求、包装、插入列表

7a. 策略判断”要不要请求” → 发起请求 → 结果入队

// 文件: lib_ad/.../strategy/AdAbsPosStrategy.kt
 
fun tryRequest(position: Int, isUp: Boolean) {
    if (isRequestProcessing) return
    if (!shouldRequest(position, isUp)) return
 
    isRequestProcessing = true
 
    // 调用 FlowVideoHelper.requestAd(),传入回调
    helper.requestAd(object : IAdRequestCallback<AdItemModel> {
 
        override fun onSuccess(adList: MutableList<AdItemModel>) {
            // ① 给每条广告标记请求时的用户位置
            adList.forEach { ad ->
                helper.adControlSetReqPos(ad, helper.getListState().currentReqPos)
            }
 
            // ② 过滤无效广告(去重、下线、空订单等)
            helper.filterValidAd(adList)
 
            // ③ 放入本地队列,等待插入
            offerListToQueue(adList)
 
            isRequestProcessing = false
 
            // ④ 请求成功后,立刻尝试一次插入
            onRequestSuccess(position, isUp)
        }
 
        override fun onFail() {
            isRequestProcessing = false
        }
    })
}

7b. 策略判断”插在哪一楼”

// 文件: lib_ad/.../strategy/AdAbsPosStrategy.kt
 
fun tryInsert(position: Int, isUp: Boolean, extra: AdInsertExtra? = null) {
    val list = helper.getTargetList()     // 拿到视频流列表 = FlowState.flowList
    val ad = helper.getNextAd(...)        // 从队列中取出下一条待插入的广告
 
    if (ad == null) return                // 队列为空,跳过
 
    // 各策略子类实现不同的楼层计算逻辑:
    //   AdDynamic2PosStrategy          — 动态混排(时间+楼层双维度)
    //   NadIntelligentControlPosStrategy — 智能控制(服务端下发参数)
    //   NadResetSortPosStrategy        — 重排序策略
    val targetPos = getInsertTargetPos(position, list, ad)
 
    // 插入前校验
    val ret = checkInsertCondition(isUp, position, targetPos, list, ad)
    if (ret != AdPosInsertCase.Accept) return
 
    // 执行插入 👇
    val finalPos = helper.insertAd(ad, targetPos, list, extra)
 
    if (finalPos > 0) {
        helper.getListState().getAdQueue<AdItemModel>()?.remove(ad)
        helper.getListState().setLastAd(ad)
        helper.getListState().lastAdPos = finalPos
        insertCb?.onSuccess(finalPos, ad)
    }
}

7c. 执行插入:避让 + 包装 + 加入列表

// 文件: video/feedflow/ad/position/FlowVideoHelper.kt
 
override fun insertAd(
    ad: AdItemModel,
    targetPos: Int,
    list: MutableList<FlowItem<*>>,
    extra: AdInsertExtra?
): Int {
 
    // ① 避让:如果目标位置不能放广告,向前或向后偏移
    val finalPos = dodge(list, targetPos, ad, true)
 
    // ② 空订单特殊处理
    if (ad.adType == EMPTY_ORDER_AD_TYPE) {
        markUgcAsEmptyOrder(ad, list[finalPos])
        return finalPos
    }
 
    // ③ 记录插入触发方式
    ad.runTime.insertTrigger = extra?.triggerCase?.getTriggerCase()
 
    // ④ 包装:AdItemModel → FlowItem
    val adElement = makeAdElement(ad)
 
    // ⑤ 插入列表
    CollectionUtils.add(list, adElement, finalPos)
 
    return finalPos
}
 
override fun makeAdElement(ad: AdItemModel): FlowItem<*> {
    return FlowItem(
        id = ad.id, nid = ad.nid, layout = ad.layout,
        data = ad   // 👈 AdItemModel 作为 payload 放入
    ).apply {
        ad.runTime.itemModelWeakRef = WeakReference(this)
    }
}
 
// FlowItem 是 ItemModel 的别名:
// typealias FlowItem<MODEL> = ItemModel<MODEL>

7d. 通知 RecyclerView 刷新

// video/feedflow/ad/position/FlowVideoHelper.kt
 
override fun adControlNotifyInsert(ad: AdItemModel, position: Int) {
    managerRef.get()
        ?.getService(IFlowComponentService::class.java)
        ?.notifyItemRangeInserted(position, 1)
    // 👆 最终调用 RecyclerView.Adapter.notifyItemRangeInserted()
    //    视频流列表在 position 处出现了一条新的广告卡片
}
 
// 此时 FlowState.flowList 中在 finalPos 位置多了一个 FlowItem<AdItemModel>
// 当用户滑到这个位置时,RecyclerView 触发 onBindViewHolder
// → AdVideoItemComponent.onBindData()
// → 进入下面的第 8~10 步

8. 用户滑到广告:触发详情页数据准备

// video/feedflow/ad/AdVideoItemComponent.kt
// 当用户滑到广告卡片时,RecyclerView 触发 onBindData
 
override fun onBindData(position: Int, model: ItemModel<*>) {
    hideViewBeforeBindData()          // 先隐藏,防闪烁
    super.onBindData(position, model)
 
    if (!DIFactory.isNetConnected()) {
        itemStore.post(NetAction.Failure(FlowDetailModel::class.java, null))
        return
    }
 
    prepareDetailData(model)          // 👈 准备详情数据
    showViewAfterBindData(100L)       // 100ms 后展示
}
 
private fun prepareDetailData(model: ItemModel<*>) {
    val detailParams = ((model as? FlowItem<*>)?.data as? AdItemModel)?.detailParams
 
    // 第一次:用列表已有的数据,立即渲染
    postDetailData(model)
 
    // 第二次(可选):如果有 detailParams,异步请求更丰富的详情数据
    if (detailParams != null) {
        prepareDetailDataAsync(model, detailParams)
        // → 请求成功后,合并新字段(comment, praise, shareInfo...)
        // → 再次调用 postDetailData(model) 刷新 UI
    }
}

9. Redux Dispatch → Reducer 状态计算

// video/feedflow/ad/AdVideoItemComponent.kt
 
private fun postDetailData(model: ItemModel<*>) {
    // 分发 Action → 触发 Reducer
    itemStore.dispatch(AdDetailDataAction(model))
 
    // Reducer 执行完毕后,取出结果,广播给子组件
    itemStore.getState().select(FlowDetailState::class.java)?.data?.let {
        itemStore.post(NetAction.Success(it))       // 通知通用组件
    }
    itemStore.getState().select(AdDataState::class.java)?.data?.value?.let {
        itemStore.post(NetAction.Success(it))       // 通知广告组件
    }
    itemStore.post(AdCustomizeAction)               // 触发广告定制化
}
// video/feedflow/ad/detail/AdDetailReducer.kt
 
class AdDetailReducer : Reducer<CommonState> {
    override fun reduce(state: CommonState, action: Action): CommonState {
        when (action) {
            is AdDetailDataAction<*> -> {
                ((action.itemModel as? FlowItem<*>)?.data as? AdItemModel)?.let { adItemModel ->
 
                    // Domain → 通用详情模型(作者、评论、点赞等公共字段)
                    val flowDetailModel = AdDetailMapper().map(adItemModel)
 
                    // Domain → 广告专属模型(按钮、尾帧、浮层等广告字段)
                    val adData = AdDataMapper().map(adItemModel)
 
                    // 后处理 ①:下载信息兜底
                    if (adData.extraData?.adDownload != null) {
                        if (TextUtils.isEmpty(adData.extraData.adDownload.appName)) {
                            adData.extraData.adDownload.appName =
                                adData.operate?.appInfo?.appName
                                ?: flowDetailModel.author?.name
                        }
                    }
 
                    // 后处理 ②:cmd 宏替换
                    val ext = adData.extraInfo?.extraParams
                    adData.operate?.button?.cmd = replaceCmdExt(adData.operate?.button?.cmd, ext)
                    // "__AD_EXTRA_PARAM__" → "abc123..."
 
                    // 后处理 ③:cmd 兜底
                    adData.operate?.button?.cmd?.let { cmd ->
                        if (adData.cmd.isBlank()) adData.cmd = cmd
                    }
 
                    // 后处理 ④:标题兜底
                    if (adData.svTitle == null) {
                        adData.svTitle = NadHighLightTextModel().apply {
                            text = flowDetailModel.title
                        }
                    }
 
                    // 后处理 ⑤:实时策略修正(端模型控制组件展现时机)
                    state.select(AdStrategyState::class.java)?.realtimeStrategy?.value?.run {
                        if (commonDelayTime > 0) {
                            adData.enhancement?.getTransition(0)?.run {
                                if (delay <= 0) delay = commonDelayTime
                            }
                            adData.mountTag?.run {
                                if (startDelay <= 0) startDelay = commonDelayTime
                            }
                        }
                        // 按钮策略、浮层策略 ...
                    }
 
                    // 存入 Redux State
                    state.select(FlowDetailState::class.java) ?: let { state.put(FlowDetailState()) }
                    state.select(FlowDetailState::class.java)?.data = flowDetailModel
 
                    state.select(AdDataState::class.java) ?: let { state.put(AdDataState()) }
                    state.select(AdDataState::class.java)?.data?.value = adData
                    // 赋值后触发 LiveData 观察
                }
            }
        }
        return state
    }
}

其中AdDataMapper做了两件事:字段裁剪类型精炼

// 文件: video/feedflow/ad/detail/AdDataMapper.kt
 
class AdDataMapper : Mapper<AdItemModel, AdData> {
    override fun map(input: AdItemModel): AdData {
        return AdData(
            // 类型精炼
            videoWidth = input.videoWidth.toIntOrNull(),   // "720" → 720
            videoHeight = input.videoHeight.toIntOrNull(),  // "1280" → 1280
 
            // 重命名
            poster = input.post,                           // post → poster
 
            // 直传
            operate = input.operate,
            extraInfo = input.extraInfo,
            titleCmd = input.titleCmd,
            liveState = input.liveState,
            // ... 80+ 字段
 
            // 共享运行时状态(引用传递)
            runTime = input.runTime,
        )
    }
}
// 裁剪掉的字段(列表页专用): id, nid, layout, title, adType, floor, status ...

10. UI 组件消费 AdData

// video/feedflow/ad/AdRedux.kt
// 所有组件通过统一方式读取广告数据
 
object AdRedux {
    fun getAdData(store: Store<AbsState>?): AdData? {
        return store?.select<AdDataState>()?.data?.value
    }
}
 
// ---- 示例:按钮组件 ----
val adData = AdRedux.getAdData(store) ?: return
adData.operate?.button?.let { button ->
    operateBtn.text = button.text
    operateBtn.setOnClickListener { SchemeRouter.route(adData.cmd) }
}
 
// ---- 示例:直播状态 ----
val adData = AdRedux.getAdData(store) ?: return
when (adData.liveState) {
    1 -> showLiveTag(adData.liveTagText)
    3 -> showBookingButton()
    4 -> showReplayBadge()
}
 
// ---- 示例:标题点击跳转 ----
val adData = AdRedux.getAdData(store) ?: return
if (!adData.titleCmd.isNullOrEmpty()) {
    titleView.setOnClickListener { SchemeRouter.route(adData.titleCmd) }
}
 
// ---- 示例:尾帧展示 ----
val adData = AdRedux.getAdData(store) ?: return
if (adData.tailShow > 0 && adData.tailFrame != null) {
    showTailFrame(adData.tailFrame)
}

数据经历的 3 次转型

阶段转换执行者核心工作
JSON → BeanGson 自动反序列化AdListBean原样接收,不做深度解析
Bean → AdItemModelFlowAdVideoItemMapper50+ 子 Mapper拆 JSON 字符串 + 类型转换
AdItemModel → AdDataAdDataMapper + AdDetailReducerReducer 后处理裁剪字段 + 精炼类型 + cmd宏替换 + 策略修正

一句话串起来

用户滑动
  → AdPositionPlugin 监听位置变化
  → AdAbsPosStrategy.tryRequest() 判断要不要请求
  → FlowVideoHelper.requestAd() 发网络
  → Gson 解析为 AdListBean (DTO, 全是 String)
  → FlowAdItemMapper → FlowAdVideoItemMapper (50+子Mapper) 转为 List<AdItemModel>
  → filterValidAd() 过滤 → offerListToQueue() 入队
  → tryInsert() → getInsertTargetPos() 计算楼层 → dodge() 避让
  → makeAdElement() 包装为 FlowItem → 插入 FlowState.flowList
  → notifyItemRangeInserted() 通知 RecyclerView
  → 用户滑到该位置 → onBindData()
  → dispatch AdDetailDataAction → AdDetailReducer
  → AdDataMapper 裁剪为 AdData + Reducer 后处理
  → NetAction.Success(adData) 广播
  → 60+ UI Component 消费渲染

更新: 2026-03-04 19:33:51
原文: https://www.yuque.com/dongpozhouzi-mshe3/zhm85g/hwxsuzgegbkgk7h0


相关笔记