Xây dựng Giao diện Mạng xã hội Tương tự WeChat Moments bằng Jetpack Compose

Bài viết này hướng dẫn cách tạo một giao diện người dùng tương tự tính năng "Nhật ký" của WeChat bằng cách sử dụng Jetpack Compose, một bộ công cụ UI hiện đại cho Android.

1. Giao diện Chính (MainScreen)

Đây là thành phần trung tâm điều phối toàn bộ giao diện, bao gồm danh sách các bài đăng, thanh nhập liệu bình luận, cửa sổ xem trước ảnh và các hộp thoại.


package com.example.composecircledemo.ui

import android.Manifest
import android.content.Intent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat.startActivity
import coil.ImageLoader
import com.example.composecircledemo.PlayVideoActivity
import com.example.composecircledemo.ui.model.CircleBean
import com.example.composecircledemo.ui.model.CommentListBean
import com.example.composecircledemo.ui.model.LikeListBean
import com.example.composecircledemo.ui.model.WeatherEvent
import com.example.composecircledemo.utils.Constants
import com.example.composecircledemo.utils.ImageSaveUtil
import com.example.composecircledemo.utils.RxBus
import com.example.composecircledemo.utils.ToastUtil
import com.example.composecircledemo.utils.rememberStoragePermissionLauncher
import io.reactivex.rxjava3.disposables.CompositeDisposable
import kotlinx.coroutines.launch

/**
 * Màn hình chính hiển thị danh sách bài đăng và các thành phần tương tác.
 */
@Composable
fun MainScreen() {
    val context = LocalContext.current
    val scope = rememberCoroutineScope()
    val keyboardController = LocalSoftwareKeyboardController.current
    val lazyListState = rememberLazyListState()
    val compositeDisposable = remember { CompositeDisposable() }

    // Quản lý trạng thái toàn cục
    var circleData by remember { mutableStateOf<List<CircleBean.DataBean>>(emptyList()) }
    var isCommentInputVisible by remember { mutableStateOf(false) }
    var commentInputContent by remember { mutableStateOf("") }
    var currentReplyPostIndex by remember { mutableStateOf(-1) }
    var currentReplyToUserId by remember { mutableStateOf("") }
    var currentReplyToUserName by remember { mutableStateOf("") }
    var isImagePreviewVisible by remember { mutableStateOf(false) }
    var previewImages by remember { mutableStateOf(emptyList<String>()) }
    var previewImageIndex by remember { mutableStateOf(0) }
    var showDeleteConfirmationDialog by remember { mutableStateOf(false) }
    var deletePostIndex by remember { mutableStateOf(-1) }
    var showActionMenuDialog by remember { mutableStateOf(false) } // Trạng thái hộp thoại menu hành động
    var currentActionPostIndex by remember { mutableStateOf(-1) }    // Vị trí bài đăng đang thao tác

    // Launcher yêu cầu quyền lưu trữ
    val storagePermissionLauncher = rememberStoragePermissionLauncher(
        onGranted = {
            scope.launch {
                // Sửa lỗi: tránh crash khi danh sách ảnh xem trước rỗng
                val targetImage = previewImages.getOrNull(previewImageIndex) ?: return@launch
                val success = ImageSaveUtil.saveImageToGallery(
                    context, targetImage, ImageLoader(context)
                )
                ToastUtil.show(context, if (success) "Lưu ảnh thành công" else "Lưu ảnh thất bại")
            }
        },
        onDenied = { ToastUtil.show(context, "Thiếu quyền lưu trữ, không thể lưu ảnh") }
    )

    // 1. Khởi tạo dữ liệu
    LaunchedEffect(Unit) {
        circleData = loadMockCircleData()
    }

    // 2. Sửa lỗi RxBus trùng lặp đăng ký (bản gốc đăng ký trùng lặp & hủy sớm, dẫn đến lỗi nhận sự kiện)
    LaunchedEffect(Unit) {
        val disposable = RxBus.toObservable<WeatherEvent>()
            .subscribe(
                { event ->
                    println("Weather: ${event.cityName} ${event.temperature}℃")
                },
                { error -> error.printStackTrace() }
            )
        compositeDisposable.add(disposable) // Chỉ thêm vào, không hủy sớm
    }

    // 3. Cấu trúc bố cục: Danh sách + Hộp thoại hành động + Thanh nhập liệu bình luận + Xem trước ảnh
    Box(modifier = Modifier.fillMaxSize()) {
        // Danh sách bài đăng
        LazyColumn(state = lazyListState, modifier = Modifier.fillMaxSize()) {
            item { CircleHeader() }
            itemsIndexed(circleData, key = { _, item -> item.id ?: "default_key" }) { index, item ->
                CircleItem(
                    data = item,
                    // Quan trọng: đảm bảo "nút hành động" (như dấu ba chấm ở góc trên bên phải) liên kết với callback này
                    onActionClick = {
                        currentActionPostIndex = index // Ghi lại vị trí bài đăng đang thao tác
                        showActionMenuDialog = true  // Mở hộp thoại
                    },
                    onReplyClick = { comment ->
                        currentReplyPostIndex = index
                        currentReplyToUserId = comment.user_id ?: ""
                        currentReplyToUserName = comment.user_name ?: ""
                        isCommentInputVisible = true
                        commentInputContent = ""
                        keyboardController?.show()
                        scope.launch {
                            lazyListState.scrollToItem(index + 1)
                        }
                    },
                    onDeleteClick = {
                        deletePostIndex = index
                        showDeleteConfirmationDialog = true
                    },
                    onVideoClick = { videoUrl ->
                        val intent = Intent(context, PlayVideoActivity::class.java)
                        intent.putExtra("url", videoUrl)
                        startActivity(context, intent, null)
                    },
                    onImageClick = { images, imgIndex ->
                        previewImages = images
                        previewImageIndex = imgIndex
                        isImagePreviewVisible = true
                    },
                    onCommentLongClick = { comment ->
                        ToastUtil.show(context, "Đã sao chép: ${comment.content ?: ""}")
                    }
                )
            }
        }

        // 4. Mới thêm: Hộp thoại hành động Thích/Bình luận (hiện từ dưới lên, nhấn nút hành động để hiển thị)
        if (showActionMenuDialog && currentActionPostIndex != -1) {
            // Nền hộp thoại (nhấn vào khoảng trống để đóng)
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .background(Color.Black.copy(alpha = 0.3f))
                    .clickable { showActionMenuDialog = false }
            )
            // Nội dung hộp thoại (hiển thị ở dưới, thiết kế bo góc)
            Column(
                modifier = Modifier
                    .align(Alignment.BottomCenter)
                    .fillMaxWidth()
                    .background(Color.White)
                    .padding(16.dp)
            ) {
                // Lấy dữ liệu bài đăng đang thao tác (kiểm tra an toàn)
                val currentItem = circleData.getOrNull(currentActionPostIndex) ?: run {
                    showActionMenuDialog = false
                    return@Column
                }

                // Nút Thích
                Text(
                    text = if (currentItem.is_like == 1) "Bỏ thích" else "Thích",
                    fontSize = 18.sp,
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(vertical = 6.dp)
                        .clickable {
                            // Thực hiện logic Thích/Bỏ thích
                            val updatedLikeList =
                                currentItem.like_list?.toMutableList() ?: mutableListOf()
                            if (currentItem.is_like == 1) {
                                // Bỏ thích: xóa người dùng hiện tại
                                updatedLikeList.removeAll { it.user_id == "current_user_id" }
                            } else {
                                // Thích: thêm người dùng hiện tại
                                updatedLikeList.add(
                                    LikeListBean(
                                        user_id = "current_user_id",
                                        user_name = "Người dùng hiện tại",
                                        circle_id = currentItem.id ?: ""
                                    )
                                )
                            }
                            // Cập nhật dữ liệu danh sách
                            circleData = circleData.toMutableList().apply {
                                this[currentActionPostIndex] = currentItem.copy(
                                    is_like = if (currentItem.is_like == 1) 0 else 1,
                                    like_list = updatedLikeList
                                )
                            }
                            ToastUtil.show(
                                context,
                                if (currentItem.is_like == 1) "Bỏ thích thành công" else "Thích thành công"
                            )
                            showActionMenuDialog = false // Đóng hộp thoại
                        }
                )

                // Dòng phân cách
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(1.dp)
                        .background(Color.LightGray.copy(alpha = 0.5f))
                )

                // Nút Bình luận
                Text(
                    text = "Bình luận",
                    fontSize = 18.sp,
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(vertical = 6.dp)
                        .clickable {
                            // Kích hoạt thanh nhập liệu bình luận
                            currentReplyPostIndex = currentActionPostIndex
                            currentReplyToUserId = ""
                            currentReplyToUserName = ""
                            isCommentInputVisible = true
                            commentInputContent = ""
                            keyboardController?.show()
                            // Cuộn đến bài đăng hiện tại, tránh che khuất
                            scope.launch {
                                lazyListState.scrollToItem(currentActionPostIndex + 1)
                            }
                            showActionMenuDialog = false // Đóng hộp thoại
                        }
                )

                // Dòng phân cách
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(1.dp)
                        .background(Color.LightGray.copy(alpha = 0.5f))
                )

                // Nút Hủy
                Text(
                    text = "Hủy",
                    fontSize = 18.sp,
                    color = Color.Red,
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(vertical = 12.dp)
                        .clickable {
                            showActionMenuDialog = false
                        }
                )
            }
        }

        // 5. Thanh nhập liệu bình luận dưới cùng
        if (isCommentInputVisible && currentReplyPostIndex != -1) { // Sửa lỗi: thêm điều kiện currentReplyPostIndex, tránh NullPointerException
            CommentInputBar(
                modifier = Modifier
                    .align(Alignment.BottomCenter)
                    .fillMaxWidth()
                    .background(Color.White)
                    .padding(8.dp),
                hint = if (currentReplyToUserId.isBlank()) "Nói điều gì đó..." else "Trả lời ${currentReplyToUserName}...",
                content = commentInputContent,
                onContentChange = { commentInputContent = it },
                onSendClick = {
                    if (commentInputContent.isBlank()) {
                        ToastUtil.show(context, "Vui lòng nhập nội dung bình luận")
                        return@CommentInputBar
                    }
                    val newComment = CommentListBean(
                        id = System.currentTimeMillis().toString(),
                        user_id = "current_user_id",
                        user_name = "Người dùng hiện tại",
                        to_user_id = currentReplyToUserId,
                        to_user_name = currentReplyToUserName,
                        circle_id = circleData[currentReplyPostIndex].id ?: "",
                        content = commentInputContent,
                        type = "0",
                        createon = System.currentTimeMillis().toString(),
                        replyUser = CommentListBean.CommentsUser(
                            userId = currentReplyToUserId.takeIf { it.isNotEmpty() } ?: "0",
                            userName = currentReplyToUserName.takeIf { it.isNotEmpty() } ?: ""
                        ),
                        commentsUser = CommentListBean.CommentsUser(
                            userId = "current_user_id",
                            userName = "Người dùng hiện tại"
                        ),
                        is_del = "0",
                        deleteon = ""
                    )
                    // Cập nhật danh sách bình luận (sửa lỗi: đảm bảo tập hợp có thể thay đổi)
                    circleData = circleData.toMutableList().apply {
                        val targetItem = this[currentReplyPostIndex]
                        val newComments =
                            targetItem.comments_list?.toMutableList() ?: mutableListOf()
                        newComments.add(newComment)
                        this[currentReplyPostIndex] = targetItem.copy(comments_list = newComments)
                    }
                    // Đặt lại trạng thái
                    isCommentInputVisible = false
                    commentInputContent = ""
                    keyboardController?.hide()
                    ToastUtil.show(
                        context,
                        if (currentReplyToUserId.isBlank()) "Bình luận thành công" else "Trả lời thành công"
                    )
                },
                onDismiss = {
                    isCommentInputVisible = false
                    keyboardController?.hide()
                }
            )
        }

        // 6. Cửa sổ xem trước ảnh
        if (isImagePreviewVisible && previewImages.isNotEmpty()) { // Sửa lỗi: kiểm tra danh sách xem trước không rỗng
            ImagePreview(
                images = previewImages,
                currentIndex = previewImageIndex,
                onDismiss = { isImagePreviewVisible = false },
                onLongClick = {
                    storagePermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
                }
            )
        }

        // 7. Hộp thoại xác nhận xóa
        if (showDeleteConfirmationDialog && deletePostIndex != -1) {
            AlertDialog(
                title = { Text("Thông báo") },
                text = { Text("Bạn có chắc muốn xóa bài đăng này không?") },
                onDismissRequest = { showDeleteConfirmationDialog = false },
                confirmButton = {
                    Button(onClick = {
                        circleData = circleData.toMutableList().apply { removeAt(deletePostIndex) }
                        showDeleteConfirmationDialog = false
                        deletePostIndex = -1
                        ToastUtil.show(context, "Xóa thành công")
                    }) {
                        Text("Xác nhận")
                    }
                },
                dismissButton = {
                    Button(onClick = { showDeleteConfirmationDialog = false }) {
                        Text("Hủy")
                    }
                }
            )
        }
    }

    // 8. Dọn dẹp tài nguyên (hủy đăng ký RxBus khi trang được hủy)
    DisposableEffect(Unit) {
        onDispose {
            compositeDisposable.dispose()
        }
    }
}

// Giả lập tải dữ liệu bài đăng (thay thế AssetsUtil.getStates cũ)
private fun loadMockCircleData(): List<CircleBean.DataBean> {
    // Hằng số danh sách rỗng thống nhất, tránh tạo lại nhiều lần và xác định rõ kiểu generic
    val emptyStringList: MutableList<String> = mutableListOf<String>()
    val emptyCommentList: MutableList<CommentListBean> = mutableListOf<CommentListBean>()
    val emptyLikeList: MutableList<LikeListBean> = mutableListOf<LikeListBean>()

    return listOf(
        // Bài đăng 1: Bài đăng web (sửa lỗi: bổ sung tham số video, xác định rõ kiểu generic cho danh sách rỗng)
        CircleBean.DataBean(
            id = "190383",
            user_id = "12",
            type = Constants.TYPE_WEB, // "4" (bài đăng web)
            content = "Kiểm tra sửa lỗi nóng",
            files = emptyStringList,
            share_title = "Biên giới của Trường Bạch Sơn Hán và đồng cỏ, rừng Hạo Thủy, dưới chân núi, bao quanh bởi rừng nguyên sinh là vài chục hộ gia đình, nhà tranh, giường đối diện, ống khói đặt sau nhà. Ở cuối làng phía đông có một ngôi nhà riêng, đó là điểm tập kết của thanh niên, trước cửa sổ có một con suối nhỏ chảy qua. Học sinh ăn ở đây, hàng ngày xuất phát từ đây đi làm việc với xã viên. Công việc hoặc là đốn gỗ, vận chuyển gỗ trên núi, hoặc là chặt cây liễu để khai hoang trồng trọt. Trên núi, có thể nghe tiếng hô: \"Núi đổ theo chiều dọc!\" Cẩn thận với gậy từ trên cây rơi xuống! Cành khô trên cây làm gãy các cây khác và bật trở lại, gậy này đánh người rất mạnh.",
            share_image = "http://imgsrc.baidu.com/forum/w=580/sign=852e30338435e5dd902ca5d746c7a7f5/bd3eb13533fa828b6cf24d82fc1f4134960a5afa.jpg",
            share_url = "",
            longitude = "",
            latitude = "",
            position = "",
            is_del = "0",
            deleteon = "0",
            createon = "3 ngày trước",
            user_name = "Mèo Máy",
            head_img = "http://b167.photo.store.qq.com/psb?/V14EhGon2OmAUI/hQN450lNoDPF.dO82PVKEdFw0Qj5qyGeyN9fByKgWd0!/m/dJWKmWNZEwAAnull",
            comments_list = emptyCommentList,
            like_list = emptyLikeList,
            is_like = 0,
            video = "" // Bổ sung tham số video cho bài đăng không phải video (chuỗi rỗng)
        ),

        // Bài đăng 2: Bài đăng văn bản (sửa lỗi: bổ sung tham số video, xác định rõ kiểu generic cho danh sách rỗng)
        CircleBean.DataBean(
            id = "190382", // Sửa ID (190383 trùng với bài đăng 1)
            user_id = "12",
            type = Constants.TYPE_TEXT, // "1" (bài đăng văn bản)
            content = "Biên giới của Trường Bạch Sơn Hán và đồng cỏ, rừng Hạo Thủy, dưới chân núi, bao quanh bởi rừng nguyên sinh là vài chục hộ gia đình, nhà tranh, giường đối diện, ống khói đặt sau nhà. Ở cuối làng phía đông có một ngôi nhà riêng, đó là điểm tập kết của thanh niên, trước cửa sổ có một con suối nhỏ chảy qua. Học sinh ăn ở đây, hàng ngày xuất phát từ đây đi làm việc với xã viên. Công việc hoặc là đốn gỗ, vận chuyển gỗ trên núi, hoặc là chặt cây liễu để khai hoang trồng trọt. Trên núi, có thể nghe tiếng hô: \"Núi đổ theo chiều dọc!\" Cẩn thận với gậy từ trên cây rơi xuống! Cành khô trên cây làm gãy các cây khác và bật trở lại, gậy này đánh người rất mạnh.",
            files = emptyStringList,
            share_title = "Quá trình phân phối sự kiện",
            share_image = "http://imgsrc.baidu.com/forum/w=580/sign=852e30338435e5dd902ca5d746c7a7f5/bd3eb13533fa828b6cf24d82fc1f4134960a5afa.jpg",
            share_url = "",
            longitude = "",
            latitude = "",
            position = "",
            is_del = "0",
            deleteon = "0",
            createon = "3 ngày trước",
            user_name = "Mèo Máy",
            head_img = "http://b167.photo.store.qq.com/psb?/V14EhGon2OmAUI/hQN450lNoDPF.dO82PVKEdFw0Qj5qyGeyN9fByKgWd0!/m/dJWKmWNZEwAAnull",
            comments_list = emptyCommentList,
            like_list = emptyLikeList,
            is_like = 0,
            video = "" // Bổ sung tham số video
        ),

        // Bài đăng 3: Bài đăng ảnh (sửa lỗi: loại bỏ ép kiểu, xác định rõ kiểu generic cho files)
        CircleBean.DataBean(
            id = "190381",
            user_id = "3372840",
            type = Constants.TYPE_IMAGE, // "2" (bài đăng ảnh)
            content = "",
            // Sử dụng trực tiếp List<String>, không cần ép kiểu MutableList
            files = listOf(
                "http://gips1.baidu.com/it/u=1410005327,4082018016&fm=3028&app=3028&f=JPEG&fmt=auto?w=960&h=1280",
                "https://gips3.baidu.com/it/u=3732338995,3528391142&fm=3028&app=3028&f=JPEG&fmt=auto&q=100&size=f600_800",
                "http://pic.3h3.com/up/2014-3/20143314140858312456.gif",
                "http://gips2.baidu.com/it/u=1506371362,2755593126&fm=3028&app=3028&f=JPEG&fmt=auto?w=1440&h=2560",
                "http://gips1.baidu.com/it/u=2778784063,1731001818&fm=3028&app=3028&f=JPEG&fmt=auto?w=1920&h=2560",
                "http://gips2.baidu.com/it/u=1192674964,3939660937&fm=3028&app=3028&f=JPEG&fmt=auto?w=1280&h=960",
                "http://gips1.baidu.com/it/u=2178851102,884542160&fm=3028&app=3028&f=JPEG&fmt=auto?w=960&h=1280",
                "http://gips2.baidu.com/it/u=1757244148,2197425658&fm=3028&app=3028&f=JPEG&fmt=auto?w=1440&h=2560",
                "https://gips1.baidu.com/it/u=926030969,4240391978&fm=3028&app=3028&f=JPEG&fmt=auto&q=100&size=f576_1024"
            ) as MutableList<String>,
            share_title = "",
            share_image = "",
            share_url = "",
            longitude = "",
            latitude = "",
            position = "",
            is_del = "0",
            deleteon = "0",
            createon = "4 ngày trước",
            user_name = "Quán nhỏ mỉm cười",
            head_img = "http://b167.photo.store.qq.com/psb?/V14EhGon2OmAUI/hQN450lNoDPF.dO82PVKEdFw0Qj5qyGeyN9fByKgWd0!/m/dJWKmWNZEwAAnull",
            comments_list = emptyCommentList,
            like_list = emptyLikeList,
            is_like = 0,
            video = "" // Bổ sung tham số video
        ),

        // Bài đăng 4: Bài đăng web (sửa lỗi: bổ sung tham số video, xác định rõ kiểu generic cho danh sách rỗng)
        CircleBean.DataBean(
            id = "190380",
            user_id = "3372793",
            type = Constants.TYPE_WEB, // "4" (bài đăng web)
            content = "",
            files = emptyStringList,
            share_title = "《Vương Giả Vinh Diệu》 thực sự rất hay, hãy cùng chơi nào! Trong cuộc sống thực, tôi đối mặt một mình với mưa gió cuộc đời, nhưng chỉ cần vào chiến trường, sẽ có đồng đội che chắn cho tôi. Hôm nay thời tiết thật đẹp, ánh nắng lười biếng chiếu xuống đường lát đá, khắp đường đầy những cặp đôi và tôi cô đơn, khiến tôi buồn bã. Nhưng nghĩ đến chỉ còn thiếu 1 sao nữa là lên Vương Giả, tôi lập tức hồi phục toàn bộ sức lực! Chơi không?",
            share_image = "http://imgsrc.baidu.com/forum/w=580/sign=852e30338435e5dd902ca5d746c7a7f5/bd3eb13533fa828b6cf24d82fc1f4134960a5afa.jpg",
            share_url = "", // Bổ sung tham số share_url bị thiếu (theo thứ tự constructor)
            longitude = "",
            latitude = "",
            position = "",
            is_del = "0",
            deleteon = "0",
            createon = "4 ngày trước",
            user_name = "Thái Cung Nhất Hào",
            head_img = "http://b167.photo.store.qq.com/psb?/V14EhGon2OmAUI/hQN450lNoDPF.dO82PVKEdFw0Qj5qyGeyN9fByKgWd0!/m/dJWKmWNZEwAAnull",
            comments_list = listOf(
                CommentListBean(
                    id = "15563",
                    circle_id = "190380",
                    user_id = "3191031",
                    to_user_id = "0",
                    type = "0",
                    content = "Nịnh hót",
                    is_del = "0",
                    deleteon = "0",
                    createon = "1577085821",
                    to_user_name = "",
                    user_name = "Quán nhỏ mỉm cười",
                    // Thông tin người được trả lời: tương ứng với to_user_id và to_user_name
                    replyUser = CommentListBean.CommentsUser(
                        userId = "0",
                        userName = "",
                    ),
                    // Thông tin người bình luận: tương ứng với user_id và user_name của bình luận hiện tại
                    commentsUser = CommentListBean.CommentsUser(
                        userId = "0",
                        userName = ""
                    )
                )
            ) as MutableList<CommentListBean>,
            like_list = listOf(
                LikeListBean(
                    user_id = "3191031",
                    circle_id = "190380",
                    user_name = "Quán nhỏ mỉm cười"
                )
            ) as MutableList<LikeListBean>,
            is_like = 1,
            video = "" // Bổ sung tham số video
        ),

        // Bài đăng 5: Bài đăng video (bình thường, giữ nguyên mã gốc)
        CircleBean.DataBean(
            id = "190379",
            user_id = "3372793",
            type = Constants.TYPE_VIDEO, // "3" (bài đăng video)
            content = "Haha",
            files = listOf(
                "http://img.my.csdn.net/uploads/201701/17/1484647897_1978.jpg"
            ) as MutableList<String>,
            share_title = "",
            share_image = "",
            share_url = "",
            longitude = "",
            latitude = "",
            position = "",
            is_del = "0",
            deleteon = "0",
            createon = "4 ngày trước",
            user_name = "Cười hì hì",
            head_img = "http://b167.photo.store.qq.com/psb?/V14EhGon2OmAUI/hQN450lNoDPF.dO82PVKEdFw0Qj5qyGeyN9fByKgWd0!/m/dJWKmWNZEwAAnull",
            video = "https://vjs.zencdn.net/v/oceans.mp4", // Liên kết video
            comments_list = emptyCommentList,
            like_list = emptyLikeList,
            is_like = 0
        ),

        // Bài đăng 6: Bài đăng ảnh (sửa lỗi: id circle_list trong like_list thống nhất, xác định rõ kiểu generic)
        CircleBean.DataBean(
            id = "190378",
            user_id = "3372793",
            type = Constants.TYPE_IMAGE, // "2" (bài đăng ảnh)
            content = "",
            files = listOf(
                "https://inews.gtimg.com/news_bt/O_qHdht-j-Pc6-oSPkO0Nh5d2-gPK6YTa-ruo4P1aDQ5MAA/1000",
                "https://img1.baidu.com/it/u=2208277039,107339662&fm=253&fmt=auto&app=138&f=JPEG?w=219&h=332",
                "https://img1.baidu.com/it/u=3651140891,1638849478&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800",
                "https://img2.baidu.com/it/u=3143194608,2162960312&fm=253&fmt=auto&app=138&f=JPEG?w=499&h=300",
                "https://img1.baidu.com/it/u=174076537,4056466491&fm=253&fmt=auto&app=120&f=JPEG?w=800&h=1201",
                "https://img2.baidu.com/it/u=3121773192,1157460465&fm=253&fmt=auto&app=138&f=JPEG?w=333&h=500",
                "https://img1.baidu.com/it/u=2377094960,1480051748&fm=253&fmt=auto&app=138&f=JPEG?w=759&h=389",
                "http://gips2.baidu.com/it/u=195724436,3554684702&fm=3028&app=3028&f=JPEG&fmt=auto?w=1280&h=960",
                "http://gips1.baidu.com/it/u=3874647369,3220417986&fm=3028&app=3028&f=JPEG&fmt=auto?w=720&h=1280",
            ) as MutableList<String>,
            share_title = "",
            share_image = "",
            share_url = "",
            longitude = "",
            latitude = "",
            position = "",
            is_del = "0",
            deleteon = "0",
            createon = "4 ngày trước",
            user_name = "Thái Cung Nhị Hào",
            head_img = "http://b162.photo.store.qq.com/psb?/V14EhGon4cZvmh/z2WukT5EhNE76WtOcbqPIgwM2Wxz4Tb7Nub.rDpsDgo!/b/dOaanmAaKQAA",
            comments_list = emptyCommentList,
            like_list = emptyLikeList,
            is_like = 0,
            video = ""
        ),
    )
}

    

2. Xem trước ảnh (ImagePreview)

Composables này hiển thị một hoặc nhiều ảnh trong chế độ toàn màn hình, cho phép vuốt để xem ảnh kế tiếp và hỗ trợ thao tác nhấn đúp hoặc giữ để lưu ảnh.


package com.example.composecircledemo.ui

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
import coil.compose.rememberAsyncImagePainter

/**
 * Hiển thị ảnh xem trước toàn màn hình với khả năng vuốt và tương tác.
 */
@Composable
fun ImagePreview(
    images: List<String>,
    currentIndex: Int,
    onDismiss: () -> Unit,
    onLongClick: () -> Unit
){
    val pagerState = rememberPagerState(initialPage = currentIndex) { images.size }

    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Black)
            .pointerInput(Unit) {
                detectTapGestures(
                    onTap = { onDismiss() }, // Sự kiện nhấn
                    onLongPress = { onLongClick() } // Sự kiện nhấn giữ
                )
            },
    ) {
        // Xem trước ảnh theo trang ngang
        HorizontalPager(state = pagerState) { page ->
            Image(
                // Sửa lỗi: sử dụng ImageRequest cấu hình crossfade (hiệu ứng mờ dần)
                painter = rememberAsyncImagePainter(
                    model = ImageRequest.Builder(LocalContext.current)
                        .data(images[page]) // URL ảnh
                        .crossfade(true) // Hiệu ứng mờ dần (cách dùng đúng của phiên bản mới)
                        .build()
                ),
                contentDescription = "Ảnh xem trước $page",
                modifier = Modifier.fillMaxSize(),
                contentScale = ContentScale.Fit
            )
        }

        // Chỉ báo số trang
        Text(
            text = "${pagerState.currentPage + 1}/${images.size}",
            color = Color.White,
            modifier = Modifier
                .align(Alignment.BottomCenter)
                .padding(24.dp)
        )
    }
}

    

3. Hiển thị ảnh dạng lưới 9 ô (NineGridImages)

Thành phần này chịu trách nhiệm hiển thị tối đa 9 ảnh trong một bố cục lưới, với kích thước ảnh được tính toán dựa trên chiều rộng màn hình và khoảng cách giữa các ảnh.


package com.example.composecircledemo.ui

import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest

/**
 * Hiển thị danh sách ảnh trong bố cục lưới 9 ô.
 */
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun NineGridImages(
    images: List<Any?>,
    modifier: Modifier = Modifier,
    onImageClick: (List<String>, Int) -> Unit,
    overallPadding: Dp = 14.dp,
    itemSpacing: Dp = 6.dp
){
    val imageCount = images.size
    val screenWidth = androidx.compose.ui.platform.LocalConfiguration.current.screenWidthDp.dp
    val totalOverallPadding = overallPadding.times(2)
    val totalItemSpacing = itemSpacing.times(2)
    val availableWidth = screenWidth.minus(totalOverallPadding).minus(totalItemSpacing)
    val itemSize = availableWidth.div(3)
    Log.d("NineGridImages", "Tổng số ảnh: ${images.size}")

    androidx.compose.foundation.layout.FlowRow(
        maxItemsInEachRow = 3,
        horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(4.dp),
        verticalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(4.dp),
        modifier = Modifier.fillMaxWidth().padding(overallPadding)
    ) {
        images.take(9).forEachIndexed { index, url ->
            val imageUrl = url?.toString() ?: ""

            Image(
                painter = rememberAsyncImagePainter(
                    model = ImageRequest.Builder(LocalContext.current)
                        .data(imageUrl)
                        .build()
                ),
                contentDescription = "Ảnh bài đăng $index",
                modifier = Modifier
                    .size(itemSize)
                    .aspectRatio(1f) // Đảm bảo là hình vuông
                    .clickable {
                        // Chuyển đổi kiểu danh sách an toàn
                        val stringList = images.filterIsInstance<String>()
                        onImageClick(stringList, index)
                    },
                contentScale = ContentScale.Crop
            )
        }
    }
}

    

4. Văn bản có thể mở rộng (ExpandableText)

Composables này cho phép hiển thị một đoạn văn bản có giới hạn dòng ban đầu, với nút "Mở rộng" hoặc "Thu gọn" để người dùng có thể xem toàn bộ nội dung.


package com.example.composecircledemo.ui

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

/**
 * Hiển thị văn bản có thể mở rộng/thu gọn.
 */
@Composable
fun ExpandableText(
    text: String,
    modifier: Modifier = Modifier,
    maxLines: Int = 2,
    style: TextStyle = TextStyle(fontSize = 14.sp)
){
    var isExpanded by remember { mutableStateOf(false) }
    var textLayoutResult by remember { mutableStateOf<androidx.compose.ui.text.TextLayoutResult?>(null) }
    val hasMoreLines = textLayoutResult?.lineCount ?: 0 > maxLines

    Column(modifier = modifier) {
        Text(
            text = text,
            style = style,
            maxLines = if (isExpanded) Int.MAX_VALUE else maxLines,
            onTextLayout = { textLayoutResult = it },
            modifier = Modifier.fillMaxWidth()
        )

        if (hasMoreLines) {
            Text(
                text = if (isExpanded) "Thu gọn" else "Mở rộng",
                style = TextStyle(
                    fontSize = 12.sp,
                    color = Color.Blue
                ),
                modifier = Modifier
                    .padding(top = 4.dp)
                    .clickable { isExpanded = !isExpanded }
            )
        }
    }
}

    

5. Mục bình luận (CommentItem)

Composables này hiển thị một bình luận đơn lẻ, bao gồm tên người bình luận, tên người được trả lời (nếu có), và nội dung bình luận. Nó cũng hỗ trợ nhấn để trả lời và nhấn giữ để sao chép.


package com.example.composecircledemo.ui

import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.composecircledemo.ui.model.CommentListBean

/**
 * Hiển thị một mục bình luận.
 */
@Composable
fun CommentItem(
    comment: CommentListBean,
    onReplyClick: () -> Unit,
    onLongClick: () -> Unit
){
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 4.dp)
            .pointerInput(Unit) {
                detectTapGestures(
                    onTap = { onReplyClick() }, // Sự kiện nhấn
                    onLongPress = { onLongClick() } // Sự kiện nhấn giữ
                )
            },
        verticalAlignment = Alignment.CenterVertically
    ) {
        // Nội dung bình luận (phân biệt bình luận thường và trả lời)
        val commentText = if (comment.to_user_id.isNullOrBlank()) {
            "${comment.user_name}:${comment.content}"
        } else {
            "${comment.user_name} Trả lời ${comment.to_user_name}:${comment.content}"
        }

        Text(
            text = commentText,
            style = TextStyle(
                fontSize = 12.sp,
                color = Color.DarkGray
            ),
            modifier = Modifier.weight(1f)
        )

        Text(
            text = "Trả lời",
            style = TextStyle(
                fontSize = 12.sp,
                color = Color.Blue
            ),
            modifier = Modifier.padding(start = 8.dp)
        )
    }
}

    

6. Thanh nhập liệu bình luận (CommentInputBar)

Composables này cung cấp một giao diện nhập liệu bình luận ở dưới cùng màn hình, bao gồm một trường văn bản và nút gửi.


package com.example.composecircledemo.ui

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
/**
 * Thanh nhập liệu bình luận.
 */
@Composable
fun CommentInputBar(
    modifier: Modifier = Modifier,
    hint: String,
    content: String,
    onContentChange: (String) -> Unit,
    onSendClick: () -> Unit,
    onDismiss: () -> Unit
){
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Black.copy(alpha = 0.3f)) // Lớp phủ đen mờ
            .clickable { onDismiss() } // Nhấn bên ngoài để đóng
    ) {
        Row(
            modifier = modifier
                .align(Alignment.BottomCenter)
                .clickable(
                    onClick = { /* Không làm gì khi nhấn vào khu vực nhập liệu */ },
                    indication = null, // Loại bỏ phản hồi khi nhấn
                    interactionSource = androidx.compose.foundation.interaction.MutableInteractionSource()
                ),
            verticalAlignment = Alignment.CenterVertically
        ) {
            OutlinedTextField(
                value = content,
                onValueChange = onContentChange,
                placeholder = {
                    Text(text = hint, fontSize = 14.sp)
                },
                modifier = Modifier.weight(1f),
                maxLines = 3,
                singleLine = false,
                textStyle = TextStyle(fontSize = 14.sp),
                shape = TextFieldDefaults.outlinedShape,
                minLines = 1,
            )

            Spacer(modifier = Modifier.width(8.dp))

            Button(
                onClick = onSendClick,
                enabled = content.isNotBlank(),
                modifier = Modifier.padding(horizontal = 8.dp)
            ) {
                Text(text = "Gửi", fontSize = 14.sp)
            }
        }
    }
}

    

7. Mục bài đăng (CircleItem)

Đây là composable chính để hiển thị một bài đăng riêng lẻ, bao gồm thông tin người dùng, nội dung, ảnh/video/link web, vị trí, danh sách thích và bình luận.


package com.example.composecircledemo.ui

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.IconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.rememberAsyncImagePainter
import com.example.composecircledemo.R
import com.example.composecircledemo.ui.model.CircleBean
import com.example.composecircledemo.ui.model.CommentListBean
import com.example.composecircledemo.utils.Constants
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import coil.request.ImageRequest
import com.example.composecircledemo.ui.model.LikeListBean

/**
 * Hiển thị một mục bài đăng trong danh sách.
 */
@Composable
fun CircleItem(
    data: CircleBean.DataBean,
    onActionClick: () -> Unit,
    onReplyClick: (CommentListBean) -> Unit,
    onDeleteClick: () -> Unit,
    onVideoClick: (String) -> Unit,
    onImageClick: (List<String>, Int) -> Unit,
    onCommentLongClick: (CommentListBean) -> Unit
){
    val likeList = data.like_list

    Column(modifier = Modifier.fillMaxWidth()) {
        // 1. Thanh thông tin người dùng (avatar + tên + thời gian + nút xóa)
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp, 16.dp, 16.dp, 8.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            // Avatar người dùng
            Image(
                painter = rememberAsyncImagePainter(data.head_img ?: "https://picsum.photos/200/200?default"),
                contentDescription = "Avatar người dùng",
                modifier = Modifier
                    .size(48.dp)
                    .clip(CircleShape)
                    .clickable { /* Nhấn vào avatar */ },
                contentScale = ContentScale.Crop
            )

            Spacer(modifier = Modifier.width(12.dp))

            // Tên người dùng + thời gian
            Column(modifier = Modifier.weight(1f)) {
                Text(
                    text = data.user_name.toString(),
                    style = TextStyle(
                        fontSize = 16.sp,
                        fontWeight = FontWeight.Medium
                    )
                )
                Text(
                    text = data.createon.toString(),
                    style = TextStyle(
                        fontSize = 12.sp,
                        color = Color.Gray
                    ),
                    modifier = Modifier.padding(top = 2.dp)
                )
            }

            // Nút xóa (chỉ hiển thị cho bài đăng của chính mình)
            if (data.id == "current_user_id") {
                IconButton(onClick = onDeleteClick) {
                    Icon(
                        painter = painterResource(id = R.drawable.message_bg_delete), // Cần tự thêm tài nguyên icon
                        contentDescription = "Xóa",
                        tint = Color.Gray
                    )
                }
            }
        }

        // 2. Nội dung bài đăng (văn bản có thể mở rộng)
        if (!data.content.isNullOrBlank()) {
            ExpandableText(
                text = data.content,
                modifier = Modifier.padding(horizontal = 12.dp),
                maxLines = 2
            )
        }

        // 3. Khu vực nội dung bài đăng (hiển thị theo loại)
        when (data.type) {
            Constants.TYPE_IMAGE -> {
                NineGridImages(
                    images = data.files,
                    onImageClick = onImageClick,
                    overallPadding = 14.dp,
                    itemSpacing = 6.dp
                )
            }
            Constants.TYPE_VIDEO -> {
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(12.dp)
                        .clickable { data.video.let { onVideoClick(it) } }
                ) {
                    // Ảnh bìa video
                    Image(
                        painter = rememberAsyncImagePainter(
                            model = ImageRequest.Builder(LocalContext.current)
                                .data("https://picsum.photos/800/450") // Thay thế bằng ảnh kiểm thử đáng tin cậy
                                .crossfade(true)
                                .build()
                        ),
                        contentDescription = "Ảnh bìa video",
                        modifier = Modifier
                            .fillMaxWidth()
                            .aspectRatio(16f / 9f), // Đặt tỷ lệ khung hình, đảm bảo ảnh hiển thị
                        contentScale = ContentScale.Crop
                    )

                    // Biểu tượng phát video
                    Icon(
                        painter = painterResource(id = R.mipmap.ic_video_play),
                        contentDescription = "Phát video",
                        modifier = Modifier
                            .size(48.dp)
                            .align(Alignment.Center)
                            .background(Color.Black.copy(alpha = 0.3f), CircleShape)
                            .padding(8.dp),
                        tint = Color.White
                    )
                }
            }
            Constants.TYPE_WEB -> {
                Card(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(8.dp)
                        .background(Color(0xFFF5F5F5), shape = RoundedCornerShape(8.dp))
                        .clickable { /* Sự kiện nhấn vào web */ },
                    elevation = CardDefaults.cardElevation(2.dp)
                ) {
                    Row(
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(8.dp), // Thêm khoảng đệm để nội dung không sát viền
                        verticalAlignment = Alignment.Top
                    ) {
                        // Icon web - kích thước cố định
                        Image(
                            painter = rememberAsyncImagePainter(data.share_image ?: "https://picsum.photos/80/80?web"),
                            contentDescription = "Icon web",
                            modifier = Modifier
                                .size(80.dp)
                                .clip(RoundedCornerShape(4.dp)), // Tùy chọn: thêm bo góc
                            contentScale = ContentScale.Crop
                        )

                        Spacer(modifier = Modifier.width(12.dp))

                        // Khu vực tiêu đề web - sử dụng weight để chiếm không gian còn lại
                        Box(
                            modifier = Modifier
                                .weight(1f) // Quan trọng: chiếm toàn bộ không gian còn lại trong Row
                                .padding(vertical = 4.dp) // Khoảng đệm theo chiều dọc
                        ) {
                            Text(
                                text = data.share_title ?: "Chia sẻ web",
                                style = TextStyle(
                                    fontSize = 14.sp,
                                    color = Color.Black // Đảm bảo màu văn bản tương phản tốt với nền
                                ),
                                maxLines = 5,
                                overflow = TextOverflow.Ellipsis, // Hiển thị dấu ba chấm cho phần vượt quá
                                softWrap = true, // Cho phép xuống dòng tự động
                                modifier = Modifier.fillMaxWidth()
                            )
                        }
                    }
                }
            }
        }

        // 4. Địa điểm
        if (!data.position.isNullOrBlank() && data.position != "Thông tin vị trí này tạm thời không có") {
            Text(
                text = data.position,
                style = TextStyle(
                    fontSize = 12.sp,
                    color = Color.Gray
                ),
                modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)
            )
        }

        // 5. Khu vực Thích + Bình luận
        Column(modifier = Modifier.padding(horizontal = 12.dp, vertical = 12.dp)) {
            // Danh sách thích
            if (data.like_list.isNotEmpty()) {
                Row(modifier = Modifier.padding(bottom = 8.dp)) {
                    Icon(
                        painter = painterResource(id = R.mipmap.menu_add), // Cần tự thêm tài nguyên icon
                        contentDescription = "Thích",
                        modifier = Modifier.size(16.dp),
                        tint = if (data.is_like == 1) Color.Red else Color.Gray
                    )
                    Spacer(modifier = Modifier.width(4.dp))
                    LazyRow(
                        modifier = Modifier.fillMaxWidth(), // Tùy chọn: thêm sửa đổi chiều rộng, tránh tràn nội dung
                        contentPadding = PaddingValues(horizontal = 4.dp) // Tùy chọn: thêm khoảng đệm
                    ) {
                        // 1. Sử dụng itemsIndexed để lấy trực tiếp chỉ số, tránh vấn đề indexOf
                        itemsIndexed(likeList) { index, item ->
                            // item chỉ định rõ kiểu LikeListBean (hoặc thông qua suy luận kiểu)
                            val likeItem = item as LikeListBean // Nếu suy luận thất bại, ép kiểu rõ ràng (đảm bảo kiểu tập hợp chính xác)

                            // 2. Sửa lỗi user_name → userName (khớp tên trường LikeListBean)
                            Text(
                                text = likeItem.user_name ?: "Người dùng không xác định",
                                style = TextStyle(
                                    fontSize = 12.sp,
                                    color = if (data.is_like == 1) Color.Red else Color.Gray
                                ),
                                modifier = Modifier.padding(horizontal = 2.dp)
                            )

                            // 3. Sử dụng index để xác định có phải là phần tử cuối cùng không (không cần indexOf)
                            if (index != likeList.lastIndex) {
                                Text(
                                    text = "、",
                                    fontSize = 12.sp,
                                    color = Color.Gray,
                                    modifier = Modifier.padding(horizontal = 2.dp)
                                )
                            }
                        }
                    }
                }
            }

            // Danh sách bình luận
            if (data.comments_list.isNotEmpty() == true) {
                Column(modifier = Modifier.padding(bottom = 8.dp)) {
                    data.comments_list.forEach { comment ->
                        CommentItem(
                            comment = comment,
                            onReplyClick = { onReplyClick(comment) },
                            onLongClick = { onCommentLongClick(comment) }
                        )
                    }
                }
            }

            // Nút hành động (Thích + Bình luận)
            IconButton(onClick = onActionClick, modifier = Modifier.fillMaxWidth()) {
                Row(
                    verticalAlignment = Alignment.CenterVertically,
                    modifier = Modifier.fillMaxWidth()
                ) {
                    // Icon Thích + văn bản
                    Icon(
                        painter = painterResource(id = if (data.is_like == 1) R.mipmap.live_gift else R.mipmap.live_share),
                        contentDescription = "Thích",
                        modifier = Modifier.size(16.dp),
                        tint = if (data.is_like == 1) Color.Red else Color.Gray
                    )
                    Spacer(modifier = Modifier.width(4.dp))
                    Text(
                        text = if (data.is_like == 1) "Đã thích" else "Thích",
                        fontSize = 12.sp,
                        color = Color.Gray
                    )

                    Spacer(modifier = Modifier.weight(1f)) // Chỗ giữ chỗ, đẩy nút bình luận sang phải

                    // Icon Bình luận + văn bản
                    Icon(
                        painter = painterResource(id = R.mipmap.live_comment),
                        contentDescription = "Bình luận",
                        modifier = Modifier.size(16.dp),
                        tint = Color.Gray
                    )
                    Spacer(modifier = Modifier.width(4.dp))
                    Text(
                        text = "Bình luận(${data.comments_list.size})",
                        fontSize = 12.sp,
                        color = Color.Gray
                    )
                }
            }
        }

        // Dòng phân cách
        Spacer(
            modifier = Modifier
                .fillMaxWidth()
                .height(1.dp)
                .background(Color.LightGray.copy(alpha = 0.3f))
        )
    }
}

    

8. Tiêu đề bài đăng và thông tin người dùng (CircleHeader)

Composables này hiển thị phần đầu của trang, bao gồm ảnh nền, ảnh đại diện người dùng và thông tin về tin nhắn mới.


package com.example.composecircledemo.ui

import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter

/**
 * Hiển thị phần đầu của trang nhật ký người dùng.
 */
@Composable
fun CircleHeader(){
    Column(modifier = Modifier.fillMaxWidth()) {
        Box(modifier = Modifier.height(120.dp)) {
            Image(
                painter = rememberAsyncImagePainter("https://picsum.photos/1080/300"),
                contentDescription = "Ảnh nền nhật ký",
                modifier = Modifier.fillMaxSize(),
                contentScale = ContentScale.Crop
            )

            Box(
                modifier = Modifier
                    .align(Alignment.BottomEnd)
                    .padding(end = 16.dp),
                contentAlignment = Alignment.BottomEnd
            ) {
                Row(
                    verticalAlignment = Alignment.CenterVertically, // Căn giữa dọc tên người dùng và avatar
                    modifier = Modifier
                        .align(Alignment.TopEnd) // Định vị từ trên cùng
                        .padding(end = 16.dp) // Khoảng cách bên phải
                        .offset(y = 30.dp)
                ) {
                    // Tên người dùng (bên trái avatar)
                    Text(
                        text = "Người dùng hiện tại",
                        color = Color.White,
                        modifier = Modifier.padding(end = 12.dp). offset(x=10.dp,y = (-8).dp)
                    )

                    // Avatar người dùng
                    Image(
                        painter = rememberAsyncImagePainter("https://picsum.photos/200/200?user"),
                        contentDescription = "Avatar người dùng",
                        modifier = Modifier
                            .size(60.dp)
                            .clip(CircleShape)
                            .clickable { /* Sự kiện nhấn vào avatar */ }
                            .align(Alignment.Bottom),
                        contentScale = ContentScale.Crop
                    )
                }
            }
        }

        Card(
            modifier = Modifier
                .width(220.dp)
                .height(80.dp)
                .padding(12.dp)
                .offset(x = 20.dp)
                .clickable { /* Sự kiện nhấn vào tin nhắn */ },
            elevation = CardDefaults.cardElevation(4.dp)
        ) {
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                Image(
                    painter = rememberAsyncImagePainter("https://picsum.photos/40/40?msg"),
                    contentDescription = "Icon tin nhắn",
                    modifier = Modifier.size(24.dp)
                )
                Spacer(modifier = Modifier.width(8.dp))
                Text(text = "10 tin nhắn mới")
            }
        }
    }
}

    

9. Trang chi tiết phát video (PlayVideoActivity)

Hoạt động này sử dụng ExoPlayer để phát video được cung cấp qua một URL.


package com.example.composecircledemo

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.ui.StyledPlayerView
import kotlinx.coroutines.awaitCancellation

/**
 * Hoạt động để phát video.
 */
class PlayVideoActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val videoUrl = intent.getStringExtra("url") ?: ""
        setContent {
            VideoPlayerScreen(videoUrl = videoUrl)
        }
    }

    @Composable
    fun VideoPlayerScreen(videoUrl: String) {
        val context = LocalContext.current
        val exoPlayer = remember {
            ExoPlayer.Builder(context).build().apply {
                val mediaItem = MediaItem.fromUri(videoUrl)
                // val mediaItem = MediaItem.Builder().setUri(videoUrl).build()
                setMediaItem(mediaItem)
                prepare() // Chuẩn bị phát
                playWhenReady = true // Tự động phát
            }
        }

        AndroidView(
            factory = { ctx ->
                StyledPlayerView(ctx).apply {
                    player = exoPlayer
                    layoutParams = android.view.ViewGroup.LayoutParams(
                        android.view.ViewGroup.LayoutParams.MATCH_PARENT,
                        android.view.ViewGroup.LayoutParams.MATCH_PARENT
                    )
                }
            },
            modifier = Modifier.fillMaxSize()
        )
        LaunchedEffect(Unit) {
            try {
                awaitCancellation()
            } finally {
                // Giải phóng player khi coroutine bị hủy
                exoPlayer.release()
            }
        }
    }

}

    

10. Gọi trong MainActivity

Trong hoạt động `MainActivity`, `MainScreen` được gọi để hiển thị giao diện chính.


class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            ComposeCircleDemoTheme {
                MainScreen()
            }
        }
    }
}

    

11. Kết quả

Giao diện cuối cùng mô phỏng các chức năng chính của WeChat Moments.

12. Mã nguồn dự án

Bạn có thể tìm thấy mã nguồn đầy đủ của dự án tại đây: Gitee

Thẻ: Jetpack Compose Android UI Mạng xã hội Kotlin ListView

Đăng vào ngày 24 tháng 7 lúc 10:33