修复动态内容为空时的崩溃问题并优化UI

- 将`Moment`实体中的`momentTextContent`字段类型从`String`修改为`String?`,以允许其为空,修复了多处因空内容引发的崩溃。
- 在多个UI组件中(如新闻、短视频、推荐等)添加了对`momentTextContent`的空值检查。
- 优化了“发现”页中智能体(Agent)卡片的UI样式,使用大图背景和渐变效果,并调整了按钮和文本布局。
- 为图片加载组件(`CustomAsyncImage`)增加了默认占位图,提升了加载过程中的用户体验。
- 在热门动态列表中,过滤掉没有图片的动态,确保UI显示正常。
- 修复了Prompt推荐页面的用户资料和AI聊天导航逻辑,并增加了防崩溃处理。
This commit is contained in:
2025-11-11 17:00:57 +08:00
parent 791f5c4c96
commit 71718ee9c9
7 changed files with 442 additions and 29 deletions

View File

@@ -73,7 +73,7 @@ data class Profile(
@SerializedName("nickname") @SerializedName("nickname")
val nickname: String, val nickname: String,
@SerializedName("trtcUserId") @SerializedName("trtcUserId")
val trtcUserId: String, val trtcUserId: String? = null,
@SerializedName("username") @SerializedName("username")
val username: String val username: String
){ ){
@@ -85,7 +85,7 @@ data class Profile(
avatar = "${ApiClient.BASE_SERVER}$avatar", avatar = "${ApiClient.BASE_SERVER}$avatar",
bio = bio, bio = bio,
banner = "${ApiClient.BASE_SERVER}$banner", banner = "${ApiClient.BASE_SERVER}$banner",
trtcUserId = trtcUserId, trtcUserId = trtcUserId ?: "",
chatAIId = chatAIId, chatAIId = chatAIId,
aiAccount = aiAccount aiAccount = aiAccount
) )

View File

@@ -26,6 +26,22 @@ import com.aiosman.ravenow.entity.RoomRuleQuotaEntity
import com.aiosman.ravenow.entity.UsersEntity import com.aiosman.ravenow.entity.UsersEntity
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
/**
* 房间内的智能体信息PromptTemplate
*/
data class PromptTemplate(
@SerializedName("id")
val id: Int,
@SerializedName("openId")
val openId: String,
@SerializedName("title")
val title: String,
@SerializedName("desc")
val desc: String,
@SerializedName("avatar")
val avatar: String
)
data class Room( data class Room(
@SerializedName("id") @SerializedName("id")
val id: Int, val id: Int,
@@ -51,12 +67,26 @@ data class Room(
val creator: Creator, val creator: Creator,
@SerializedName("userCount") @SerializedName("userCount")
val userCount: Int, val userCount: Int,
@SerializedName("totalMemberCount")
val totalMemberCount: Int? = null,
@SerializedName("maxMemberLimit") @SerializedName("maxMemberLimit")
val maxMemberLimit: Int, val maxMemberLimit: Int,
@SerializedName("maxTotal")
val maxTotal: Int? = null,
@SerializedName("systemMaxTotal")
val systemMaxTotal: Int? = null,
@SerializedName("canJoin") @SerializedName("canJoin")
val canJoin: Boolean, val canJoin: Boolean,
@SerializedName("canJoinCode") @SerializedName("canJoinCode")
val canJoinCode: Int, val canJoinCode: Int,
@SerializedName("privateFeePaid")
val privateFeePaid: Boolean? = null,
@SerializedName("prompts")
val prompts: List<PromptTemplate>? = null,
@SerializedName("createdAt")
val createdAt: String? = null,
@SerializedName("updatedAt")
val updatedAt: String? = null,
@SerializedName("users") @SerializedName("users")
val users: List<Users> val users: List<Users>
@@ -75,9 +105,24 @@ data class Room(
allowInHot = allowInHot, allowInHot = allowInHot,
creator = creator.toCreatorEntity(), creator = creator.toCreatorEntity(),
userCount = userCount, userCount = userCount,
totalMemberCount = totalMemberCount,
maxMemberLimit = maxMemberLimit, maxMemberLimit = maxMemberLimit,
maxTotal = maxTotal,
systemMaxTotal = systemMaxTotal,
canJoin = canJoin, canJoin = canJoin,
canJoinCode = canJoinCode, canJoinCode = canJoinCode,
privateFeePaid = privateFeePaid ?: false,
prompts = prompts?.map {
com.aiosman.ravenow.entity.PromptTemplateEntity(
id = it.id,
openId = it.openId,
title = it.title,
desc = it.desc,
avatar = it.avatar
)
} ?: emptyList(),
createdAt = createdAt,
updatedAt = updatedAt,
users = users.map { it.toUsersEntity() } users = users.map { it.toUsersEntity() }
) )
} }
@@ -90,7 +135,7 @@ data class Creator(
@SerializedName("userId") @SerializedName("userId")
val userId: String, val userId: String,
@SerializedName("trtcUserId") @SerializedName("trtcUserId")
val trtcUserId: String, val trtcUserId: String? = null,
@SerializedName("profile") @SerializedName("profile")
val profile: Profile val profile: Profile
){ ){
@@ -98,7 +143,7 @@ data class Creator(
return CreatorEntity( return CreatorEntity(
id = id, id = id,
userId = userId, userId = userId,
trtcUserId = trtcUserId, trtcUserId = trtcUserId ?: "",
profile = profile.toProfileEntity() profile = profile.toProfileEntity()
) )
} }
@@ -110,7 +155,7 @@ data class Users(
@SerializedName("userId") @SerializedName("userId")
val userId: String, val userId: String,
@SerializedName("trtcUserId") @SerializedName("trtcUserId")
val trtcUserId: String, val trtcUserId: String? = null,
@SerializedName("profile") @SerializedName("profile")
val profile: Profile val profile: Profile
){ ){

View File

@@ -1423,11 +1423,39 @@ interface RaveNowAPI {
@POST("outside/rooms") @POST("outside/rooms")
suspend fun createGroupChat(@Body body: CreateGroupChatRequestBody): Response<DataContainer<Unit>> suspend fun createGroupChat(@Body body: CreateGroupChatRequestBody): Response<DataContainer<Unit>>
/**
* 获取房间列表
*
* 支持游客和认证用户访问,根据用户类型返回不同的房间数据。
* 游客模式返回公开推荐房间列表,认证用户模式返回用户可访问的群聊列表。
*
* @param page 页码,默认 1
* @param pageSize 每页数量,默认 20游客模式最大50
* @param roomId 房间ID用于精确查询特定房间仅认证用户
* @param includeUsers 是否包含用户列表默认false仅认证用户
* @param isRecommended 是否推荐过滤器1=推荐0=非推荐null=不过滤(仅认证用户)
* @param roomType 房间类型过滤all=公有私有都显示, public=只显示公有, private=只显示私有, created=只显示自己创建的, joined=只显示自己加入的(仅认证用户)
* @param search 搜索关键字,支持房间名称、描述、智能体名称模糊匹配
* @param random 是否随机排序字符串长度不为0则为true传任意非空字符串即可
* @param ownerSessionId 创建者用户IDChatAIID用于过滤特定创建者的房间
* @param showPublic 是否显示公有房间只有明确设置为true时才生效优先级高于roomType仅认证用户
* @param showCreated 是否显示自己创建的房间只有明确设置为true时才生效优先级高于roomType仅认证用户
* @param showJoined 是否显示自己加入的房间只有明确设置为true时才生效优先级高于roomType仅认证用户
*/
@GET("outside/rooms") @GET("outside/rooms")
suspend fun getRooms(@Query("page") page: Int = 1, suspend fun getRooms(
@Query("pageSize") pageSize: Int = 20, @Query("page") page: Int = 1,
@Query("isRecommended") isRecommended: Int = 1, @Query("pageSize") pageSize: Int = 20,
@Query("random") random: Int? = null, @Query("roomId") roomId: Long? = null,
@Query("includeUsers") includeUsers: Boolean? = null,
@Query("isRecommended") isRecommended: Int? = null,
@Query("roomType") roomType: String? = null,
@Query("search") search: String? = null,
@Query("random") random: String? = null,
@Query("ownerSessionId") ownerSessionId: String? = null,
@Query("showPublic") showPublic: Boolean? = null,
@Query("showCreated") showCreated: Boolean? = null,
@Query("showJoined") showJoined: Boolean? = null,
): Response<ListContainer<Room>> ): Response<ListContainer<Room>>
@GET("outside/rooms/detail") @GET("outside/rooms/detail")

View File

@@ -8,6 +8,17 @@ import com.aiosman.ravenow.data.api.ApiClient
* 群聊房间 * 群聊房间
*/ */
/**
* 房间内的智能体信息实体
*/
data class PromptTemplateEntity(
val id: Int,
val openId: String,
val title: String,
val desc: String,
val avatar: String
)
data class RoomEntity( data class RoomEntity(
val id: Int, val id: Int,
val name: String, val name: String,
@@ -21,9 +32,16 @@ data class RoomEntity(
val allowInHot: Boolean, val allowInHot: Boolean,
val creator: CreatorEntity, val creator: CreatorEntity,
val userCount: Int, val userCount: Int,
val totalMemberCount: Int? = null,
val maxMemberLimit: Int, val maxMemberLimit: Int,
val maxTotal: Int? = null,
val systemMaxTotal: Int? = null,
val canJoin: Boolean, val canJoin: Boolean,
val canJoinCode: Int, val canJoinCode: Int,
val privateFeePaid: Boolean = false,
val prompts: List<PromptTemplateEntity> = emptyList(),
val createdAt: String? = null,
val updatedAt: String? = null,
val users: List<UsersEntity>, val users: List<UsersEntity>,
) )

View File

@@ -197,7 +197,7 @@ object AgentViewModel: ViewModel() {
page = 1, page = 1,
pageSize = 20, pageSize = 20,
isRecommended = 1, isRecommended = 1,
random = 1 random = "1"
) )
if (response.isSuccessful) { if (response.isSuccessful) {
val allRooms = response.body()?.list ?: emptyList() val allRooms = response.body()?.list ?: emptyList()

View File

@@ -23,6 +23,10 @@ import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.entity.MomentLoader import com.aiosman.ravenow.entity.MomentLoader
import com.aiosman.ravenow.entity.MomentLoaderExtraArgs import com.aiosman.ravenow.entity.MomentLoaderExtraArgs
import com.aiosman.ravenow.entity.MomentServiceImpl import com.aiosman.ravenow.entity.MomentServiceImpl
import com.aiosman.ravenow.entity.RoomEntity
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.RaveNowAPI
import com.aiosman.ravenow.data.Room
import com.aiosman.ravenow.event.FollowChangeEvent import com.aiosman.ravenow.event.FollowChangeEvent
import com.aiosman.ravenow.event.MomentAddEvent import com.aiosman.ravenow.event.MomentAddEvent
import com.aiosman.ravenow.event.MomentFavouriteChangeEvent import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
@@ -40,6 +44,14 @@ object MyProfileViewModel : ViewModel() {
var profile by mutableStateOf<AccountProfileEntity?>(null) var profile by mutableStateOf<AccountProfileEntity?>(null)
var moments by mutableStateOf<List<MomentEntity>>(emptyList()) var moments by mutableStateOf<List<MomentEntity>>(emptyList())
var agents by mutableStateOf<List<AgentEntity>>(emptyList()) var agents by mutableStateOf<List<AgentEntity>>(emptyList())
var rooms by mutableStateOf<List<RoomEntity>>(emptyList())
var roomsLoading by mutableStateOf(false)
var roomsRefreshing by mutableStateOf(false)
var roomsCurrentPage by mutableStateOf(1)
var roomsHasMore by mutableStateOf(true)
private val roomsPageSize = 20
private val apiClient: RaveNowAPI = ApiClient.api
val momentLoader: MomentLoader = MomentLoader().apply { val momentLoader: MomentLoader = MomentLoader().apply {
pageSize = 20 // 设置与后端一致的页面大小 pageSize = 20 // 设置与后端一致的页面大小
onListChanged = { onListChanged = {
@@ -254,4 +266,115 @@ object MyProfileViewModel : ViewModel() {
fun onFollowChangeEvent(event: FollowChangeEvent) { fun onFollowChangeEvent(event: FollowChangeEvent) {
momentLoader.updateFollowStatus(event.userId, event.isFollow) momentLoader.updateFollowStatus(event.userId, event.isFollow)
} }
/**
* 加载房间列表
* @param filterType 筛选类型0=全部1=公开2=私有
* @param pullRefresh 是否下拉刷新
*/
fun loadRooms(filterType: Int = 0, pullRefresh: Boolean = false) {
// 游客模式下不加载房间列表
if (AppStore.isGuest) {
Log.d("MyProfileViewModel", "loadRooms: 游客模式下跳过加载房间列表")
return
}
if (roomsLoading && !pullRefresh) return
viewModelScope.launch {
try {
roomsLoading = true
roomsRefreshing = pullRefresh
val currentPage = if (pullRefresh) {
roomsCurrentPage = 1
roomsHasMore = true
1
} else {
roomsCurrentPage
}
val response = when (filterType) {
0 -> {
// 全部:显示自己创建或加入的所有房间
apiClient.getRooms(
page = currentPage,
pageSize = roomsPageSize,
showCreated = true,
showJoined = true
)
}
1 -> {
// 公开:显示公开房间中自己创建或加入的
apiClient.getRooms(
page = currentPage,
pageSize = roomsPageSize,
roomType = "public",
showCreated = true,
showJoined = true
)
}
2 -> {
// 私有:显示自己创建或加入的私有房间
apiClient.getRooms(
page = currentPage,
pageSize = roomsPageSize,
roomType = "private"
)
}
else -> {
apiClient.getRooms(
page = currentPage,
pageSize = roomsPageSize,
showCreated = true,
showJoined = true
)
}
}
if (response.isSuccessful) {
val roomList = response.body()?.list ?: emptyList()
val total = response.body()?.total ?: 0L
if (pullRefresh || currentPage == 1) {
rooms = roomList.map { it.toRoomtEntity() }
} else {
rooms = rooms + roomList.map { it.toRoomtEntity() }
}
roomsHasMore = rooms.size < total
if (roomsHasMore && !pullRefresh) {
roomsCurrentPage++
}
} else {
Log.e("MyProfileViewModel", "loadRooms failed: ${response.code()}")
}
} catch (e: Exception) {
Log.e("MyProfileViewModel", "loadRooms error: ", e)
} finally {
roomsLoading = false
roomsRefreshing = false
}
}
}
/**
* 加载更多房间
* @param filterType 筛选类型0=全部1=公开2=私有
*/
fun loadMoreRooms(filterType: Int = 0) {
if (roomsLoading || !roomsHasMore) return
loadRooms(filterType = filterType, pullRefresh = false)
}
/**
* 刷新房间列表
* @param filterType 筛选类型0=全部1=公开2=私有
*/
fun refreshRooms(filterType: Int = 0) {
rooms = emptyList()
roomsCurrentPage = 1
roomsHasMore = true
loadRooms(filterType = filterType, pullRefresh = true)
}
} }

View File

@@ -14,17 +14,28 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -32,13 +43,46 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.RoomEntity
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToGroupChat
import com.aiosman.ravenow.AppStore
@OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun GroupChatEmptyContent() { fun GroupChatEmptyContent() {
var selectedSegment by remember { mutableStateOf(0) } // 0: 全部, 1: 公开, 2: 私有 var selectedSegment by remember { mutableStateOf(0) } // 0: 全部, 1: 公开, 2: 私有
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val context = LocalContext.current
val navController = LocalNavController.current
val viewModel = MyProfileViewModel
val state = rememberPullRefreshState(
refreshing = viewModel.roomsRefreshing,
onRefresh = {
viewModel.refreshRooms(filterType = selectedSegment)
}
)
// 当分段改变时,重新加载数据
LaunchedEffect(selectedSegment) {
// 切换分段时重新加载
viewModel.refreshRooms(filterType = selectedSegment)
}
// 初始加载
LaunchedEffect(Unit) {
if (viewModel.rooms.isEmpty() && !viewModel.roomsLoading) {
viewModel.loadRooms(filterType = selectedSegment)
}
}
Column( Column(
modifier = Modifier modifier = Modifier
@@ -50,37 +94,192 @@ fun GroupChatEmptyContent() {
// 分段控制器 // 分段控制器
SegmentedControl( SegmentedControl(
selectedIndex = selectedSegment, selectedIndex = selectedSegment,
onSegmentSelected = { selectedSegment = it }, onSegmentSelected = {
selectedSegment = it
// LaunchedEffect 会监听 selectedSegment 的变化并自动刷新
},
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
// 空状态内容(居中) Box(
Column( modifier = Modifier
modifier = Modifier.fillMaxWidth(), .fillMaxSize()
horizontalAlignment = Alignment.CenterHorizontally .pullRefresh(state)
) { ) {
// 空状态插图 if (viewModel.rooms.isEmpty() && !viewModel.roomsLoading) {
EmptyStateIllustration() // 空状态内容(居中)
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// 空状态插图
EmptyStateIllustration()
Spacer(modifier = Modifier.height(9.dp)) Spacer(modifier = Modifier.height(9.dp))
// 空状态文本 // 空状态文本
Text( Text(
text = stringResource(R.string.empty_nothing), text = stringResource(R.string.empty_nothing),
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = AppColors.text, color = AppColors.text,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp), modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2, maxLines = 2,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
)
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
itemsIndexed(
items = viewModel.rooms,
key = { _, item -> item.id }
) { index, room ->
RoomItem(
room = room,
onRoomClick = { roomEntity ->
// 导航到群聊聊天界面
navController.navigateToGroupChat(
id = roomEntity.trtcRoomId,
name = roomEntity.name,
avatar = roomEntity.avatar
)
}
)
if (index < viewModel.rooms.size - 1) {
HorizontalDivider(
modifier = Modifier.padding(horizontal = 24.dp),
color = AppColors.divider
)
}
}
// 加载更多指示器
if (viewModel.roomsLoading && viewModel.rooms.isNotEmpty()) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = AppColors.main
)
}
}
}
// 加载更多触发
if (viewModel.roomsHasMore && !viewModel.roomsLoading) {
item {
LaunchedEffect(Unit) {
viewModel.loadMoreRooms(filterType = selectedSegment)
}
}
}
}
}
PullRefreshIndicator(
refreshing = viewModel.roomsRefreshing,
state = state,
modifier = Modifier.align(Alignment.TopCenter)
) )
} }
} }
} }
@Composable
fun RoomItem(
room: RoomEntity,
onRoomClick: (RoomEntity) -> Unit = {}
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
val roomDebouncer = rememberDebouncer()
// 构建头像URL
val avatarUrl = if (room.avatar.isNotEmpty()) {
"${ConstVars.BASE_SERVER}/api/v1/outside/${room.avatar}?token=${AppStore.token}"
} else {
""
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 12.dp)
.noRippleClickable {
roomDebouncer {
onRoomClick(room)
}
}
) {
Box {
CustomAsyncImage(
context = context,
imageUrl = avatarUrl,
contentDescription = room.name,
modifier = Modifier
.size(48.dp)
.clip(RoundedCornerShape(12.dp))
)
}
Column(
modifier = Modifier
.weight(1f)
.padding(start = 12.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = room.name,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = room.description.ifEmpty { "暂无描述" },
fontSize = 14.sp,
color = AppColors.secondaryText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "${room.userCount}",
fontSize = 12.sp,
color = AppColors.secondaryText
)
}
}
}
}
@Composable @Composable
private fun SegmentedControl( private fun SegmentedControl(
selectedIndex: Int, selectedIndex: Int,