HarmonyOS开发:社交动态发布与展示
HarmonyOS开发:社交动态发布与展示
📌 核心要点:社交动态流是社交App的核心,图文/视频动态发布、点赞/评论/转发交互、动态列表性能优化三大难题,懒加载和虚拟列表是性能关键。
背景与动机
刷朋友圈、刷微博、刷小红书——你每天花多少时间在"刷动态"上?
社交动态流看起来就是一列卡片,每个卡片有图片、文字、点赞按钮。但真要做起来,坑一个接一个。图片多了列表卡顿怎么办?视频自动播放怎么控制?点赞要实时反馈还是等服务端确认?评论列表怎么分页?转发动态怎么展示原动态?动态列表滑到1000条时内存爆了怎么办?
社交动态的难点不在UI,在性能和交互。动态列表可能无限长,每条动态可能有9张图、几十条评论、几百个赞。你用ForEach渲染1000条动态试试?内存直接飙到2GB,App闪退。
更别提交互细节:点赞动画要丝滑、评论要实时更新、图片要懒加载、视频要自动播放但只播可见区域——每个细节都影响用户体验。
这篇文章把社交动态的发布、展示、交互、性能优化全拆开讲。
核心原理
社交动态流的核心是虚拟列表+懒加载+增量更新。只渲染可见区域的动态,图片按需加载,交互操作乐观更新。
A[用户打开动态流] --> B[加载首页动态列表]
B --> C[虚拟列表渲染可见区域]
C --> D{用户操作}
D -->|下拉刷新| E[请求最新动态]
E --> F[新动态插入列表头部]
D -->|上滑加载| G[请求更多历史动态]
G --> H[旧动态追加到列表尾部]
D -->|点击点赞| I[乐观更新UI]
I --> J[发送点赞请求]
J --> K{服务端结果}
K -->|成功| L[保持UI不变]
K -->|失败| M[回滚UI状态]
D -->|发布动态| N[上传图片/视频]
N --> O[创建动态]
O --> P[插入列表头部]
D -->|查看图片| Q[全屏图片查看器]
动态数据模型
一条动态包含:发布者信息、内容(文字/图片/视频)、互动数据(点赞/评论/转发)、时间戳。
动态类型决定了渲染方式:
- 纯文字:只渲染文字,最轻量
- 图文:1-9张图片,用九宫格布局
- 视频:视频封面+播放器
- 转发:嵌套展示原动态
性能优化策略
| 策略 | 说明 |
|---|---|
| 虚拟列表 | 只渲染可见区域的动态,不可见的回收 |
| 图片懒加载 | 图片进入可见区域时才加载 |
| 分页加载 | 每次加载20条,上滑触底加载更多 |
| 增量更新 | 点赞/评论只更新对应动态,不刷新整个列表 |
| 图片缓存 | 已加载的图片缓存到内存,滑动回来时直接显示 |
代码实战
基础用法:动态列表展示
先搞定动态列表的核心:数据模型、列表渲染、九宫格图片。
// SocialFeed.ets — 社交动态流
// 动态类型
enum FeedType {
TEXT = 'TEXT', // 纯文字
IMAGE = 'IMAGE', // 图文
VIDEO = 'VIDEO', // 视频
REPOST = 'REPOST', // 转发
}
// 动态数据
interface FeedItem {
id: string
type: FeedType
userId: string
userName: string
avatarUrl: string
content: string // 文字内容
images: string[] // 图片URL列表(最多9张)
videoUrl?: string // 视频URL
videoCoverUrl?: string // 视频封面
likeCount: number // 点赞数
commentCount: number // 评论数
repostCount: number // 转发数
isLiked: boolean // 当前用户是否已点赞
createTime: string
location?: string // 位置
originalFeed?: FeedItem // 转发的原动态
}
@Entry
@Component
struct SocialFeedPage {
@State feedList: FeedItem[] = []
@State isLoading: boolean = false
@State isLoadingMore: boolean = false
@State hasMore: boolean = true
aboutToAppear() {
this.loadFeedList()
}
build() {
Column() {
// 顶部导航
Row() {
Text('动态')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Blank()
Image($r('app.media.ic_camera'))
.width(24)
.height(24)
.fillColor('#333333')
.onClick(() => {
// 跳转发布页
})
}
.width('100%')
.height(48)
.padding({ left: 16, right: 16 })
.backgroundColor(Color.White)
// 动态列表
Refresh({ refreshing: $$this.isLoading }) {
List() {
ForEach(this.feedList, (item: FeedItem) => {
ListItem() {
this.FeedCard(item)
}
}, (item: FeedItem) => item.id)
// 加载更多
ListItem() {
this.LoadMoreFooter()
}
}
.onReachEnd(() => {
if (this.hasMore && !this.isLoadingMore) {
this.loadMoreFeed()
}
})
.scrollBar(BarState.Off)
.cachedCount(3) // 预渲染3个屏幕外的Item
}
.onRefreshing(() => {
this.refreshFeed()
})
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
// ========== 动态卡片 ==========
@Builder
FeedCard(item: FeedItem) {
Column() {
// 用户信息行
Row() {
Image(item.avatarUrl)
.width(40)
.height(40)
.borderRadius(20)
.objectFit(ImageFit.Cover)
Column() {
Text(item.userName)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
if (item.location) {
Text(item.location)
.fontSize(11)
.fontColor('#999999')
.margin({ top: 2 })
}
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 8 })
Blank()
Text(this.formatTime(item.createTime))
.fontSize(12)
.fontColor('#999999')
}
.width('100%')
// 文字内容
if (item.content) {
Text(item.content)
.fontSize(15)
.fontColor('#333333')
.maxLines(6)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 8 })
.width('100%')
}
// 图片九宫格
if (item.type === FeedType.IMAGE && item.images.length > 0) {
this.ImageGrid(item.images)
}
// 视频封面
if (item.type === FeedType.VIDEO && item.videoCoverUrl) {
this.VideoCover(item)
}
// 转发原动态
if (item.type === FeedType.REPOST && item.originalFeed) {
this.RepostContent(item.originalFeed)
}
// 互动栏
this.ActionBar(item)
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.margin({ bottom: 8 })
}
// ========== 九宫格图片 ==========
@Builder
ImageGrid(images: string[]) {
const count = images.length
// 根据图片数量决定布局
if (count === 1) {
// 单张大图
Image(images[0])
.width('100%')
.height(200)
.borderRadius(8)
.objectFit(ImageFit.Cover)
.margin({ top: 8 })
} else if (count <= 3) {
// 横向排列
Row() {
ForEach(images, (url: string, index?: number) => {
Image(url)
.width(count === 2 ? '48%' : '31%')
.aspectRatio(1)
.borderRadius(4)
.objectFit(ImageFit.Cover)
.margin({ right: (index ?? 0) < count - 1 ? 4 : 0 })
}, (url: string, index?: number) => `${url}_${index}`)
}
.margin({ top: 8 })
} else {
// 九宫格
Grid() {
ForEach(images, (url: string) => {
GridItem() {
Image(url)
.width('100%')
.aspectRatio(1)
.borderRadius(2)
.objectFit(ImageFit.Cover)
}
}, (url: string, index?: number) => `${url}_${index}`)
}
.columnsTemplate(count <= 2 ? '1fr 1fr' : '1fr 1fr 1fr')
.rowsGap(4)
.columnsGap(4)
.margin({ top: 8 })
}
}
// ========== 视频封面 ==========
@Builder
VideoCover(item: FeedItem) {
Stack() {
Image(item.videoCoverUrl)
.width('100%')
.height(200)
.borderRadius(8)
.objectFit(ImageFit.Cover)
// 播放按钮
Image($r('app.media.ic_play_circle'))
.width(48)
.height(48)
.fillColor('#80FFFFFF')
}
.width('100%')
.height(200)
.margin({ top: 8 })
.onClick(() => {
// 播放视频
})
}
// ========== 转发内容 ==========
@Builder
RepostContent(original: FeedItem) {
Column() {
Text(`@${original.userName}`)
.fontSize(13)
.fontColor('#1DA1F2')
.fontWeight(FontWeight.Medium)
if (original.content) {
Text(original.content)
.fontSize(13)
.fontColor('#666666')
.maxLines(3)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 4 })
}
if (original.images.length > 0) {
Row() {
ForEach(original.images.slice(0, 3), (url: string) => {
Image(url)
.width(48)
.height(48)
.borderRadius(2)
.objectFit(ImageFit.Cover)
.margin({ right: 4 })
}, (url: string, index?: number) => `${url}_${index}`)
}
.margin({ top: 4 })
}
}
.width('100%')
.padding(12)
.backgroundColor('#F5F5F5')
.borderRadius(8)
.margin({ top: 8 })
.alignItems(HorizontalAlign.Start)
}
// ========== 互动栏 ==========
@Builder
ActionBar(item: FeedItem) {
Row() {
// 点赞
Row() {
Image(item.isLiked ? $r('app.media.ic_liked') : $r('app.media.ic_like'))
.width(20)
.height(20)
.fillColor(item.isLiked ? '#FF4444' : '#999999')
if (item.likeCount > 0) {
Text(this.formatCount(item.likeCount))
.fontSize(12)
.fontColor(item.isLiked ? '#FF4444' : '#999999')
.margin({ left: 4 })
}
}
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.toggleLike(item)
})
// 评论
Row() {
Image($r('app.media.ic_comment'))
.width(20)
.height(20)
.fillColor('#999999')
if (item.commentCount > 0) {
Text(this.formatCount(item.commentCount))
.fontSize(12)
.fontColor('#999999')
.margin({ left: 4 })
}
}
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
// 转发
Row() {
Image($r('app.media.ic_repost'))
.width(20)
.height(20)
.fillColor('#999999')
if (item.repostCount > 0) {
Text(this.formatCount(item.repostCount))
.fontSize(12)
.fontColor('#999999')
.margin({ left: 4 })
}
}
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height(40)
.margin({ top: 8 })
}
// ========== 加载更多底部 ==========
@Builder
LoadMoreFooter() {
Row() {
if (this.isLoadingMore) {
LoadingProgress()
.width(20)
.height(20)
.color('#999999')
Text('加载中...')
.fontSize(13)
.fontColor('#999999')
.margin({ left: 8 })
} else if (!this.hasMore) {
Text('— 没有更多了 —')
.fontSize(13)
.fontColor('#CCCCCC')
}
}
.width('100%')
.height(48)
.justifyContent(FlexAlign.Center)
}
// ========== 点赞(乐观更新) ==========
toggleLike(item: FeedItem) {
// 乐观更新:先改UI,再发请求
const wasLiked = item.isLiked
item.isLiked = !item.isLiked
item.likeCount += wasLiked ? -1 : 1
// 触发UI刷新
this.feedList = [...this.feedList]
// 发送请求
this.sendLikeRequest(item.id, !wasLiked).catch(() => {
// 请求失败,回滚UI
item.isLiked = wasLiked
item.likeCount += wasLiked ? 1 : -1
this.feedList = [...this.feedList]
})
}
async sendLikeRequest(feedId: string, isLike: boolean): Promise<void> {
// 实际项目:const response = await http.post('/api/feed/like', { feedId, isLike })
console.info(`[SocialFeed] 点赞: ${feedId}, ${isLike}`)
}
// ========== 辅助方法 ==========
formatCount(count: number): string {
if (count >= 10000) return `${(count / 10000).toFixed(1)}万`
if (count >= 1000) return `${(count / 1000).toFixed(1)}k`
return `${count}`
}
formatTime(time: string): string {
// 简化实现:实际需要计算时间差
return '3分钟前'
}
async loadFeedList() {
this.feedList = this.getMockFeedList()
}
async refreshFeed() {
const newFeeds = this.getMockFeedList()
this.feedList = newFeeds
this.hasMore = true
}
async loadMoreFeed() {
this.isLoadingMore = true
// 模拟加载更多
setTimeout(() => {
this.feedList = [...this.feedList, ...this.getMockFeedList()]
this.isLoadingMore = false
}, 1000)
}
private getMockFeedList(): FeedItem[] {
return [
{
id: `feed_${Date.now()}_1`, type: FeedType.IMAGE,
userId: 'u1', userName: '设计师小王', avatarUrl: 'https://picsum.photos/80/80?random=1',
content: '今天的新作品,灵感来自大自然的色彩渐变 🎨',
images: ['https://picsum.photos/400/400?random=1', 'https://picsum.photos/400/400?random=2', 'https://picsum.photos/400/400?random=3'],
likeCount: 128, commentCount: 32, repostCount: 15, isLiked: false,
createTime: '2024-12-25 10:30:00', location: '北京·798艺术区'
},
{
id: `feed_${Date.now()}_2`, type: FeedType.TEXT,
userId: 'u2', userName: '产品经理老李', avatarUrl: 'https://picsum.photos/80/80?random=2',
content: '做产品最重要的是什么?不是功能多,不是技术强,而是真正理解用户的需求。很多时候用户说想要一匹更快的马,其实他需要的是一辆车。',
images: [], likeCount: 256, commentCount: 48, repostCount: 67, isLiked: true,
createTime: '2024-12-25 09:15:00'
},
{
id: `feed_${Date.now()}_3`, type: FeedType.VIDEO,
userId: 'u3', userName: '旅行达人小张', avatarUrl: 'https://picsum.photos/80/80?random=3',
content: '冰岛极光,这辈子一定要看一次!',
images: [], videoUrl: '', videoCoverUrl: 'https://picsum.photos/600/300?random=4',
likeCount: 1024, commentCount: 156, repostCount: 89, isLiked: false,
createTime: '2024-12-25 08:00:00', location: '冰岛·雷克雅未克'
},
]
}
}
进阶用法:动态发布
动态发布支持图文和视频,核心是图片/视频选择和上传。
// FeedPublishPage.ets — 动态发布页
import { router } from '@kit.ArkUI'
import { photoAccessHelper } from '@kit.MediaLibraryKit'
import { promptAction } from '@kit.ArkUI'
@Entry
@Component
struct FeedPublishPage {
@State content: string = ''
@State selectedImages: string[] = []
@State maxImageCount: number = 9
@State isPublishing: boolean = false
@State location: string = ''
build() {
Column() {
// 顶部操作栏
Row() {
Text('取消')
.fontSize(16)
.fontColor('#666666')
.onClick(() => { router.back() })
Blank()
Button('发布')
.fontSize(14)
.fontColor(Color.White)
.backgroundColor(this.canPublish() ? '#1DA1F2' : '#CCCCCC')
.borderRadius(16)
.width(64)
.height(32)
.enabled(this.canPublish())
.onClick(() => {
this.publishFeed()
})
}
.width('100%')
.height(48)
.padding({ left: 16, right: 16 })
.backgroundColor(Color.White)
// 内容输入区
TextArea({
placeholder: '分享你的想法...'
})
.width('100%')
.height(160)
.fontSize(16)
.fontColor('#333333')
.padding(16)
.backgroundColor(Color.White)
.onChange((value: string) => {
this.content = value
})
// 图片预览区
if (this.selectedImages.length > 0) {
this.ImagePreviewGrid()
}
Divider().color('#F0F0F0')
// 底部工具栏
Row() {
// 图片选择
Image($r('app.media.ic_image'))
.width(28)
.height(28)
.fillColor('#666666')
.onClick(() => {
this.pickImages()
})
// 视频选择
Image($r('app.media.ic_video'))
.width(28)
.height(28)
.fillColor('#666666')
.margin({ left: 24 })
// 位置
Image($r('app.media.ic_location'))
.width(28)
.height(28)
.fillColor('#666666')
.margin({ left: 24 })
.onClick(() => {
this.location = '北京·朝阳区'
})
Blank()
// 字数统计
Text(`${this.content.length}/2000`)
.fontSize(12)
.fontColor(this.content.length > 2000 ? '#FF4444' : '#999999')
}
.width('100%')
.height(48)
.padding({ left: 16, right: 16 })
.backgroundColor(Color.White)
}
.width('100%')
.height('100%')
.backgroundColor(Color.White)
}
// ========== 图片预览九宫格 ==========
@Builder
ImagePreviewGrid() {
Grid() {
ForEach(this.selectedImages, (url: string, index?: number) => {
GridItem() {
Stack() {
Image(url)
.width('100%')
.aspectRatio(1)
.borderRadius(4)
.objectFit(ImageFit.Cover)
// 删除按钮
Image($r('app.media.ic_close'))
.width(20)
.height(20)
.fillColor(Color.White)
.backgroundColor('#80000000')
.borderRadius(10)
.position({ x: '75%', y: 0 })
.onClick(() => {
const idx = index ?? 0
this.selectedImages.splice(idx, 1)
this.selectedImages = [...this.selectedImages]
})
}
}
}, (url: string, index?: number) => `${url}_${index}`)
// 添加图片按钮
if (this.selectedImages.length < this.maxImageCount) {
GridItem() {
Column() {
Image($r('app.media.ic_add'))
.width(28)
.height(28)
.fillColor('#CCCCCC')
Text('添加图片')
.fontSize(11)
.fontColor('#999999')
.margin({ top: 4 })
}
.width('100%')
.aspectRatio(1)
.justifyContent(FlexAlign.Center)
.backgroundColor('#F5F5F5')
.borderRadius(4)
.onClick(() => {
this.pickImages()
})
}
}
}
.columnsTemplate('1fr 1fr 1fr')
.rowsGap(4)
.columnsGap(4)
.width('100%')
.padding(16)
}
// ========== 选择图片 ==========
async pickImages() {
try {
const remaining = this.maxImageCount - this.selectedImages.length
if (remaining <= 0) {
promptAction.showToast({ message: `最多选择${this.maxImageCount}张图片` })
return
}
// 实际项目:调用系统图片选择器
// const picker = new photoAccessHelper.PhotoViewPicker()
// const result = await picker.select({
// MIMEType: photoAccessHelper.PhotoViewMIMEType.IMAGE_TYPE,
// maxSelectNumber: remaining
// })
// const newImages = result.photoUris
// this.selectedImages = [...this.selectedImages, ...newImages]
// 模拟添加图片
const mockImages = [
'https://picsum.photos/400/400?random=10',
'https://picsum.photos/400/400?random=11',
]
this.selectedImages = [...this.selectedImages, ...mockImages.slice(0, remaining)]
} catch (error) {
console.error(`[FeedPublish] 选择图片失败: ${JSON.stringify(error)}`)
}
}
// ========== 发布动态 ==========
async publishFeed() {
if (!this.canPublish()) return
this.isPublishing = true
try {
// 1. 上传图片(如果有)
let imageUrls: string[] = []
if (this.selectedImages.length > 0) {
imageUrls = await this.uploadImages(this.selectedImages)
}
// 2. 创建动态
const feedData = {
content: this.content,
images: imageUrls,
location: this.location,
}
// 实际项目:const response = await http.post('/api/feed/create', feedData)
console.info('[FeedPublish] 动态发布成功')
promptAction.showToast({ message: '发布成功' })
router.back()
} catch (error) {
promptAction.showToast({ message: '发布失败,请重试' })
} finally {
this.isPublishing = false
}
}
async uploadImages(localPaths: string[]): Promise<string[]> {
// 实际项目:上传到云存储,返回CDN URL
return localPaths
}
canPublish(): boolean {
return this.content.trim().length > 0 || this.selectedImages.length > 0
}
}
完整示例:高性能动态流
把懒加载、虚拟列表、增量更新串成完整的高性能动态流。
// HighPerfFeed.ets — 高性能社交动态流
import { router } from '@kit.ArkUI'
@Entry
@Component
struct HighPerfFeed {
@State feedList: FeedItem[] = []
@State isLoading: boolean = false
@State isLoadingMore: boolean = false
@State hasMore: boolean = true
@State currentPage: number = 1
private listScroller: Scroller = new Scroller()
private pageSize: number = 20
aboutToAppear() {
this.loadFeedList(1)
}
build() {
Column() {
// 顶部栏
Row() {
Text('动态')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Blank()
Image($r('app.media.ic_camera'))
.width(24)
.height(24)
.fillColor('#333333')
}
.width('100%')
.height(48)
.padding({ left: 16, right: 16 })
.backgroundColor(Color.White)
Refresh({ refreshing: $$this.isLoading }) {
List({ scroller: this.listScroller }) {
// 使用LazyForEach实现虚拟列表
LazyForEach(new FeedDataSource(this.feedList), (item: FeedItem) => {
ListItem() {
this.FeedCard(item)
}
}, (item: FeedItem) => item.id)
ListItem() {
Row() {
if (this.isLoadingMore) {
LoadingProgress().width(20).height(20).color('#999999')
Text('加载中...').fontSize(13).fontColor('#999999').margin({ left: 8 })
} else if (!this.hasMore) {
Text('— 没有更多了 —').fontSize(13).fontColor('#CCCCCC')
}
}
.width('100%')
.height(48)
.justifyContent(FlexAlign.Center)
}
}
.cachedCount(5) // 预缓存5个Item
.scrollBar(BarState.Off)
.onReachEnd(() => {
if (this.hasMore && !this.isLoadingMore) {
this.loadFeedList(this.currentPage + 1)
}
})
}
.onRefreshing(() => {
this.refreshFeed()
})
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
// ========== 动态卡片 ==========
@Builder
FeedCard(item: FeedItem) {
Column() {
Row() {
Image(item.avatarUrl)
.width(40)
.height(40)
.borderRadius(20)
.objectFit(ImageFit.Cover)
Column() {
Text(item.userName).fontSize(15).fontWeight(FontWeight.Medium).fontColor('#333333')
if (item.location) {
Text(item.location).fontSize(11).fontColor('#999999').margin({ top: 2 })
}
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 8 })
Blank()
Text(this.formatTime(item.createTime)).fontSize(12).fontColor('#999999')
}
if (item.content) {
Text(item.content).fontSize(15).fontColor('#333333').maxLines(6)
.textOverflow({ overflow: TextOverflow.Ellipsis }).margin({ top: 8 }).width('100%')
}
if (item.type === FeedType.IMAGE && item.images.length > 0) {
this.ImageGrid(item.images)
}
// 互动栏
Row() {
Row() {
Image(item.isLiked ? $r('app.media.ic_liked') : $r('app.media.ic_like'))
.width(20).height(20).fillColor(item.isLiked ? '#FF4444' : '#999999')
if (item.likeCount > 0) {
Text(this.formatCount(item.likeCount)).fontSize(12)
.fontColor(item.isLiked ? '#FF4444' : '#999999').margin({ left: 4 })
}
}.layoutWeight(1).justifyContent(FlexAlign.Center)
.onClick(() => { this.toggleLike(item) })
Row() {
Image($r('app.media.ic_comment')).width(20).height(20).fillColor('#999999')
if (item.commentCount > 0) {
Text(this.formatCount(item.commentCount)).fontSize(12).fontColor('#999999').margin({ left: 4 })
}
}.layoutWeight(1).justifyContent(FlexAlign.Center)
Row() {
Image($r('app.media.ic_repost')).width(20).height(20).fillColor('#999999')
if (item.repostCount > 0) {
Text(this.formatCount(item.repostCount)).fontSize(12).fontColor('#999999').margin({ left: 4 })
}
}.layoutWeight(1).justifyContent(FlexAlign.Center)
}
.width('100%').height(40).margin({ top: 8 })
}
.width('100%').padding(16).backgroundColor(Color.White).margin({ bottom: 8 })
}
@Builder
ImageGrid(images: string[]) {
if (images.length === 1) {
Image(images[0]).width('100%').height(200).borderRadius(8).objectFit(ImageFit.Cover).margin({ top: 8 })
} else {
Grid() {
ForEach(images, (url: string) => {
GridItem() {
Image(url).width('100%').aspectRatio(1).borderRadius(2).objectFit(ImageFit.Cover)
}
}, (url: string, index?: number) => `${url}_${index}`)
}
.columnsTemplate('1fr 1fr 1fr').rowsGap(4).columnsGap(4).margin({ top: 8 })
}
}
toggleLike(item: FeedItem) {
const wasLiked = item.isLiked
item.isLiked = !wasLiked
item.likeCount += wasLiked ? -1 : 1
this.feedList = [...this.feedList]
}
formatCount(count: number): string {
if (count >= 10000) return `${(count / 10000).toFixed(1)}万`
return `${count}`
}
formatTime(time: string): string { return '3分钟前' }
async refreshFeed() {
this.currentPage = 1
this.hasMore = true
this.feedList = this.getMockFeedList()
}
async loadFeedList(page: number) {
if (page === 1) {
this.feedList = this.getMockFeedList()
} else {
this.isLoadingMore = true
setTimeout(() => {
this.feedList = [...this.feedList, ...this.getMockFeedList()]
this.isLoadingMore = false
}, 500)
}
this.currentPage = page
}
private getMockFeedList(): FeedItem[] {
return Array.from({ length: 5 }, (_, i) => ({
id: `feed_${Date.now()}_${this.currentPage}_${i}`,
type: i % 3 === 0 ? FeedType.TEXT : FeedType.IMAGE,
userId: `u${i}`, userName: `用户${i + 1}`,
avatarUrl: `https://picsum.photos/80/80?random=${Date.now() + i}`,
content: `这是第${this.currentPage}页第${i + 1}条动态,分享一些有趣的事情...`,
images: i % 3 !== 0 ? [
`https://picsum.photos/400/400?random=${Date.now() + i * 10 + 1}`,
`https://picsum.photos/400/400?random=${Date.now() + i * 10 + 2}`,
`https://picsum.photos/400/400?random=${Date.now() + i * 10 + 3}`,
] : [],
likeCount: Math.round(Math.random() * 500),
commentCount: Math.round(Math.random() * 100),
repostCount: Math.round(Math.random() * 50),
isLiked: Math.random() > 0.5,
createTime: '2024-12-25 10:30:00',
}))
}
}
// LazyForEach数据源
class FeedDataSource implements IDataSource {
private dataList: FeedItem[] = []
private listeners: DataChangeListener[] = []
constructor(data: FeedItem[]) {
this.dataList = data
}
totalCount(): number { return this.dataList.length }
getData(index: number): FeedItem { return this.dataList[index] }
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) this.listeners.push(listener)
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener)
if (pos >= 0) this.listeners.splice(pos, 1)
}
updateData(data: FeedItem[]): void {
this.dataList = data
this.listeners.forEach(l => l.onDataReloaded())
}
}
踩坑与注意事项
坑1:动态列表滑动卡顿
100条动态,每条9张图,总共900张图片同时加载——内存爆了,滑动卡成PPT。
解决方案:
- 用
LazyForEach替代ForEach,只渲染可见区域 - 设置
cachedCount,预缓存3-5个Item - 图片用缩略图,点击后加载原图
- 列表项用
@Reusable装饰器复用
坑2:九宫格图片布局计算
1张图、2张图、4张图、9张图——每种数量的布局都不一样。你用if-else写9种布局?那代码可读性为零。
解决方案:统一用3列Grid,1张图时单独处理(大图),其余都用3列。4张图时第二行只占1格,5张图时第二行占2格——Grid自动处理。
坑3:点赞乐观更新失败回滚
用户点了赞,UI立刻变了,但服务端返回失败——这时候UI要回滚,但用户已经看到了"已点赞"的状态,突然变回去很突兀。
解决方案:加个短暂的延迟,如果200ms内服务端返回成功就不回滚,如果失败再回滚。或者干脆不回滚,失败时弹个Toast提示"操作失败"。
坑4:图片上传顺序错乱
用户选了5张图,上传时3号图先传完,1号图还在传——动态发布后图片顺序不对。
解决方案:按选择顺序上传,用Promise.all等待所有图片上传完成后再发布。或者每张图片上传时记录原始序号,发布时按序号排序。
坑5:动态列表数据更新导致滚动位置跳动
下拉刷新时新数据插入列表头部,当前浏览位置突然跳到顶部。
解决方案:下拉刷新后保持滚动位置。新数据插入头部后,用scroller.scrollToIndex跳回原来的位置。
HarmonyOS 6适配说明
HarmonyOS 6对社交动态相关组件做了以下更新:
-
LazyForEach性能优化:LazyForEach新增了
prefetchCount属性,可以预取数据。用户滑到第15条动态时,第16-20条的数据已经预取好了,不再有加载延迟。 -
Image组件解码优化:Image新增
autoResize属性,自动根据显示尺寸解码图片。九宫格里的缩略图只需要150x150,不再加载4000x3000的原图再缩放,内存占用减少90%。 -
Grid自适应布局:Grid新增了
layoutDirection和adaptiveColumns属性,九宫格布局可以根据屏幕宽度自动调整列数,不用手动计算。 -
视频自动播放控制:Video组件新增了
autoPlayPolicy属性,可以设置为"仅WiFi自动播放"或"仅可见区域自动播放",避免流量偷跑。 -
@Reusable组件复用:新增
@Reusable装饰器,标记的组件在滑出屏幕后不会销毁,而是放入复用池。新动态滑入时从复用池取组件,只更新数据,不需要重新创建。性能提升约60%。
总结
社交动态流的核心是性能。动态列表可能无限长,每条动态可能有9张图,你用ForEach全部渲染,内存和CPU都扛不住。
核心记住三点:
- LazyForEach是必须的,只渲染可见区域,配合cachedCount预缓存
- 点赞用乐观更新,先改UI再发请求,失败时回滚
- 图片用缩略图,列表里显示缩略图,点击后加载原图,内存占用降90%
| 评估维度 | 说明 |
|---|---|
| 学习难度 | ⭐⭐⭐⭐ 九宫格布局和性能优化需要经验 |
| 使用频率 | ⭐⭐⭐⭐⭐ 社交App的核心功能 |
| 重要程度 | ⭐⭐⭐⭐ 动态流卡顿直接导致用户流失 |
动态列表滑到第100条时App闪退了——这不是bug,这是用户卸载的理由。
更多推荐


所有评论(0)