广告数据从网络返回后,如何被插入到视频流 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 刷新 │
└──────────────────────────────────────────────┘参与角色
| 角色 | 文件 | 职责 |
|---|---|---|
AdPositionPlugin | video/feedflow/ad/position/AdPositionPlugin.kt | Redux 插件,监听滑动,驱动流程 |
AdPosStrategyManager | lib_ad/.../strategy/AdPosStrategyManager.kt | 策略管理器,路由到具体策略 |
AdAbsPosStrategy | lib_ad/.../strategy/AdAbsPosStrategy.kt | 策略基类,控制”何时请求""何时插入” |
FlowVideoHelper | video/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, ...)
}
}
}
}每次用户滑动,。选中,tryRequest和tryInsert都会被调用。请求是异步的,插入是同步的。队列里有广告就插,没有就跳过。
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.flowList在finalPos处多了一个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.kt | video/feedflow/ad/position/ |
| 策略基类 | AdAbsPosStrategy.kt | lib_ad/.../position/strategy/ |
| 请求参数 | AdListParam.kt | flowvideo/ad/repos/ |
| 网络请求 | AdListRepositoryImpl.kt | flowvideo/ad/repos/ |
| API 接口 | AdListApi.kt | flowvideo/ad/api/ |
| DTO Bean | AdListBean.kt | flowvideo/ad/api/ |
| 批量 Mapper | FlowAdItemMapper.kt | video/feedflow/ad/flow/ |
| 单条 Mapper | FlowAdVideoItemMapper.kt | video/feedflow/ad/flow/ |
| Domain Model | AdItemModel.kt | video/feedflow/ad/flow/ |
| 插楼执行 | FlowVideoHelper.kt | video/feedflow/ad/position/ |
| 详情 Reducer | AdDetailReducer.kt | video/feedflow/ad/detail/ |
| AdData Mapper | AdDataMapper.kt | video/feedflow/ad/detail/ |
| 详情页 Model | AdData.kt | video/feedflow/ad/detail/ |
| State 容器 | AdDataState.kt | video/feedflow/ad/detail/ |
| 数据访问器 | AdRedux.kt | video/feedflow/ad/ |
| Item 组件 | AdVideoItemComponent.kt | video/feedflow/ad/ |
| Mapper 接口 | Mapper.kt | flowvideo/service/lib-component-ext/.../mapper/ |
| 列表数据源 | FlowState.kt | flowvideo/flow/list/ |
| 适配器 | FlowListAdapter.kt | flowvideo/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 的结构自动反序列化 JSON3. 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 个角色协作:
| 角色 | 职责 |
|---|---|
AdPositionPlugin | Redux 插件,监听用户滑动,驱动整个流程 |
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 → Bean | Gson 自动反序列化 | AdListBean | 原样接收,不做深度解析 |
| Bean → AdItemModel | FlowAdVideoItemMapper | 50+ 子 Mapper | 拆 JSON 字符串 + 类型转换 |
| AdItemModel → AdData | AdDataMapper + AdDetailReducer | Reducer 后处理 | 裁剪字段 + 精炼类型 + 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