0

I am making a chat bot application. I've searched in many places to solve this problem but couldn't find anything clear. I have an empty list. Bot messages and user messages are added to this list, but it is empty at first. I show this list with the help of lazycolumn. Since it is empty at first, no message is displayed on the screen, but at first I showed a bot message on the screen so that the user can interact with the user, and in response, the user enters a message, if the user enters a message, I show a bot message again and in the same way, the user responds to this bot message. enters a message. In short, there is a loop like this, but I want the screen to scroll when the messages reach the end of the screen. I couldn't find how to do this because I don't have a specific size list at first it is filled with empty user responses and bot messages. In the second question in my mind, the screen size of each phone is different, so I understand that the questions come to the end of the screen and overflow, how do I scroll, because as I said, this will vary according to the screen of the phone. I will share my codes and screenshot, you will better understand what I mean.


@SuppressLint("CoroutineCreationDuringComposition")
@Composable
fun FirstScreen(
    viewModel: ChatBotViewModel = hiltViewModel()
) {

    val list = remember { mutableStateListOf<Message>() }
    val botList = listOf("Peter", "Francesca", "Luigi", "Igor")
    val random = (0..3).random()
    val botName: String = botList[random]
    val hashMap: HashMap<String, String> = HashMap()

    viewModel.customBotMessage(message = Message1, list)

    Column(modifier = Modifier.fillMaxHeight(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Top) {
        Divider()
        LazyColumn(
            modifier = Modifier.fillMaxSize(),
            ){
            items(list.size) { i ->
                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement =
                    if (list[i].id == RECEIVE_ID)
                        Arrangement.Start
                    else
                        Arrangement.End
                ) {
                    if (list[i].id == RECEIVE_ID) {
                        Item(
                            message = list[i],
                            botName,
                            botcolor,
                            list,
                            simpleTextFlag = true,
                            hashMap,
                            modifier = Modifier
                                .padding(
                                    start = 32.dp,
                                    end = 4.dp,
                                    top = 4.dp
                                ),
                        )
                    }
                    else {
                        Item(
                            message = list[i],
                            "user",
                            usercolor,
                            list,
                            simpleTextFlag = false,
                            hashMap,
                            Modifier.padding(
                                start = 4.dp,
                                end = 32.dp,
                                top = 4.dp
                            )
                        )
                    }
                }
            }
        }
    }
}

ITEM

@Composable
fun Item(
    message: Message,
    person: String,
    color: Color,
    list: SnapshotStateList<Message>,
    simpleTextFlag: Boolean,
    hashMap: HashMap<String, String>,
    modifier: Modifier,
    viewModel: ChatBotViewModel = hiltViewModel()
) {

    Column(verticalArrangement = Arrangement.Top) {
        Card(
            modifier = Modifier
                .padding(10.dp),
            backgroundColor = color,
            elevation = 10.dp
        ) {
            Row(
                verticalAlignment = Alignment.Top,
                modifier = Modifier.padding(3.dp)
            ) {
                Text(
                    buildAnnotatedString {
                        withStyle(
                            style = SpanStyle(
                                fontWeight = FontWeight.Medium,
                                color = Color.Black
                            )
                        ) {
                            append("$person: ")
                        }
                    },
                    modifier = Modifier
                        .padding(4.dp)
                )
                Text(
                    buildAnnotatedString {
                        withStyle(
                            style = SpanStyle(
                                fontWeight = FontWeight.W900,
                                color = Color.White//Color(/*0xFF4552B8*/)
                            )
                        )
                        {
                            append(message.message)
                        }
                    }
                )
            }
        }
        if (simpleTextFlag && list.size != 13) {
            SimpleText(list = list, hashMap, viewModel)
        }
    }
}

@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
@Composable
fun SimpleText(
    list: SnapshotStateList<Message>,
    hashMap: HashMap<String, String>,
    viewModel: ChatBotViewModel,
) {

    val coroutineScope = rememberCoroutineScope()
    val keyboardController = LocalSoftwareKeyboardController.current
    val bringIntoViewRequester = remember { BringIntoViewRequester() }
    var visible by remember { mutableStateOf(true) }

    var text by remember { mutableStateOf("") }

    AnimatedVisibility(
        visible = visible,
        enter = fadeIn() + slideInHorizontally(),
        exit = fadeOut() + slideOutHorizontally()
    ) {
        Column {
            Row(
                verticalAlignment = Alignment.Bottom,
                horizontalArrangement = Arrangement.End,
                modifier = Modifier.bringIntoViewRequester(bringIntoViewRequester)
            ) {
                OutlinedTextField(
                    modifier = Modifier
                        .padding(2.dp)
                        .onFocusEvent { focusState ->
                            if (focusState.isFocused) {
                                coroutineScope.launch {
                                    bringIntoViewRequester.bringIntoView()
                                }
                            }
                        },
                    keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
                    keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }),
                    value = text,
                    onValueChange = { text = it },
                    shape = RoundedCornerShape(12.dp),
                    label = { Text("send message") })
                IconButton(onClick = {
                    if (text.isNotEmpty()) {
                        //Adds it to our local list
                        list.add(
                            Message(
                                text,
                                Constants.SEND_ID,
                                Timestamp(System.currentTimeMillis()).toString()
                            )
                        )
                        hashMap[Messages.listOfMessageKeys[viewModel.index.value - 1]] = text
                        viewModel.customBotMessage(listOfMessages[viewModel.index.value], list)
                        viewModel.index.value += 1
                        visible = false
                    }
                    text = ""
                }) {
                    Icon(
                        modifier = Modifier.padding(2.dp),
                        painter = painterResource(id = R.drawable.ic_baseline_send_24),
                        contentDescription = "send message img"
                    )
                }
            }
            if (list.size == 3 || list.size == 5 || list.size == 7) {
                Button()
                if(viewModel.isClickedBtn.value){
                    visible = false
                    viewModel.isClickedBtn.value = false
                    list.add(
                        Message(
                            text,
                            Constants.SEND_ID,
                            Timestamp(System.currentTimeMillis()).toString()
                        )
                    )
                    hashMap[Messages.listOfMessageKeys[viewModel.index.value - 1]] = text
                    viewModel.customBotMessage(listOfMessages[viewModel.index.value], list)
                    viewModel.index.value += 1
                }
            }
        }
    }
}

@Composable
fun Button(
    viewModel: ChatBotViewModel = hiltViewModel()
) {

    Button(
        onClick = { viewModel.isClickedBtn.value = true },
    ) {
        Text("skip")
    }
}

ChatBotViewModel

class ChatBotViewModel @Inject constructor() : ViewModel() {

    var index = mutableStateOf(value = 1)
    val isClickedBtn = mutableStateOf(false)

    fun customBotMessage(message: String, list: SnapshotStateList<Message>) {

        GlobalScope.launch {
            delay(1000)
            withContext(Dispatchers.Main) {
                list.add(
                    Message(
                        message,
                        Constants.RECEIVE_ID,
                        Timestamp(System.currentTimeMillis()).toString()
                    )
                )
            }
        }
    }
}

enter image description here

There is 1 more message(user:dsdssd) after the last user message, it should automatically slide there. If this happens, the user has to scroll manually, which is a very bad user experience.

NewPartizal
  • 604
  • 4
  • 18

1 Answers1

0

You can use the LazyListState to interact with a LazyList.

Given an extension function like

fun LazyListState.isScrolledToTheEnd() : Boolean {
    val lastItem = layoutInfo.visibleItemsInfo.lastOrNull()
    return lastItem == null || lastItem.size + lastItem.offset <= layoutInfo.viewportEndOffset
}

you could scroll to the end of the list, when it is necessary.

var messages by remember {
    mutableStateOf(emptyList<String>())
}
LaunchedEffect(true) {
    while (true) {
        delay(1000)
        messages = messages + "Test"
    }
}

val listState = rememberLazyListState()
LaunchedEffect(messages.size) {
    if (!listState.isScrolledToTheEnd()) {
        val itmIndex = listState.layoutInfo.totalItemsCount - 1
        if (itmIndex >= 0) {
            val lastItem = listState.layoutInfo.visibleItemsInfo.lastOrNull()
            lastItem?.let {
                listState.animateScrollToItem(itmIndex, it.size + it.offset)
            }
        }
    }
}

LazyColumn(
    state = listState
) {
    ...
}

Compose lists (control scroll position) documentation: link

SerjantArbuz
  • 982
  • 1
  • 12
  • 16
  • I defined it like this : val list = remember { mutableStateListOf() } and this defines as SnapshotStateList is that a problem ? because when I implemented your code and After adding the first message my code, although no message is added to the list, it constantly adds the first added message, so it shouldn't be like this. – NewPartizal Jan 16 '23 at 12:45
  • How do you add the message to the list? messages.add("Test") does work for me. Please be aware that my while loop is just an example. You should move your messages list to your ViewModel. – Conrad Peyer Jan 17 '23 at 09:21
  • you can see in ViewModel customBotMessage – NewPartizal Jan 17 '23 at 09:35
  • I was able to reproduce the problem. To solve the issue I had to move the SnapShotStateList to the ViewModel and post the initial bot message in the ViewModel during initialization. I guess the FirstScreen composable was re-composed each time a bot message was added to the list. BTW: I changed my example to scroll to the very bottom of the LazyList. – Conrad Peyer Jan 17 '23 at 15:22