Jacopo Notari
Jacopo Notari

Reputation: 1

Why do I get undesired multiple recompositions for the same component?

I'm creating a modify edit pane (in a team tasks tracking app). I'd like to create and visualize some fields with empty values if no teamId is provided (it will be a new team) or existing values taken from a team list (saved in the View Model).

When i try to compose it i noticed (via log prints) that the same component is recomposed 4 times with the same state. This unexpected behavior leads to other strange results. Why does this happen?

Edit team Component

fun NewEditTeam(
    viewModel: AppViewModel = viewModel(),
    navHostController: NavHostController,
    teamId: Int = -1
) {
    val uiState = viewModel.editTeamUi
    val randomNumber = Random.nextInt()
    Log.d("NE", "Recomposed EditTeam view with state: $uiState")
 
    var addRoleExpanded by remember {
        mutableStateOf<TeamMember?>(null)
    }
 
    if (teamId != -1) {
        val team = viewModel.getTeamById(teamId)
        viewModel.updateValuesFromExistingTeam(team)
    } else {
        viewModel.addMember(viewModel.myProfile)
    }
    Scaffold(
        floatingActionButton = {
            FloatingActionButton(
                onClick = {
                    if (viewModel.validateAll()) {
                        viewModel.saveTeam(teamId = teamId)
                        navHostController.popBackStack()
                        viewModel.resetEditTeamUi()
                    }
                content = {
                    Icon(
                        Icons.Default.Check,
                        contentDescription = "AddMembers",
                        modifier = Modifier.size(50.dp)
                    )
                },
            )
        }
    ) { padding ->
        Column {
            NavBar {
                viewModel.resetEditTeamUi()
                navHostController.navigateUp()
            }
            TeamImage(
                imageUri = uiState.image.value,
                name = uiState.name.value,
                setImage = { viewModel.updateTeamImage(it) },
            )
            TeamInfo(
                uiState,
                onNameChange = { viewModel.updateTeamName(it) },
                onDescChange = { viewModel.updateTeamDescription(it) },
            )
            TeamMemberList(
                members = uiState.members.value,
                invitedMembers = uiState.invitedMembers.value,
                addMemberOnClick = {
                    navHostController.navigate("editTeam/inviteMember/")
                },
                findProfileById = { viewModel.getProfileById(it)!! },
                onDeleteRole = { tm, role ->
                    viewModel.deleteRole(tm, role)
                },
                onAdminSwitchChanged = { tm, isAdmin ->
                    viewModel.changeAdmin(tm, isAdmin)
                },
                onAddRole = { addRoleExpanded = it },
                isErrorText = uiState.membersError.value,
                myProfile = viewModel.myProfile
            )
            if (addRoleExpanded != null) {
                AddRoleDialog(
                    assignedRoles = addRoleExpanded!!.roles,
                    removeRole = { viewModel.deleteRole(addRoleExpanded!!, it) },
                    addRole = { viewModel.addRole(addRoleExpanded!!, it) },
                    roleList = uiState.members.value.map { it.roles.toSet() }.flatten()
                        .toSet(),
                    onDismiss = { addRoleExpanded = null },
                    updateDialogStatus = {
                        val newTM = viewModel.findTeamMemberById(teamId, addRoleExpanded!!.profileId)
                        addRoleExpanded = newTM
                    }
                )
 
            }
        }
    }
}

View Model and Ui State classes

data class EditTeamUi(
    val name: MutableState<String> = mutableStateOf(""),
    val desc: MutableState<String> = mutableStateOf(""),
    val members: MutableState<List<TeamMember>> = mutableStateOf(listOf()),
    val image: MutableState<Uri?> = mutableStateOf(null),
    val category: MutableState<String> = mutableStateOf(""),
    val invitedMembers: MutableState<List<TeamMember>> = mutableStateOf(listOf()),
 
    val nameError: MutableState<String> = mutableStateOf(""),
    val membersError: MutableState<String> = mutableStateOf(""),
)
 
class AppViewModel : ViewModel() {
    // data lists as state flows //
 
    var editTeamUi = EditTeamUi()
 
    fun updateTeamName(name: String) {
        editTeamUi.name.value = name
    }
 
    fun updateTeamImage(uri: Uri) {
        editTeamUi.image.value = uri
    }
 
 
    fun updateTeamDescription(desc: String) {
        editTeamUi.desc.value = desc
    }
 
 
    fun deleteRole(tm: TeamMember, role: String) {
        val updatedMember = tm.apply { roles -= role }
 
        if (editTeamUi.members.value.contains(tm)) {
            val newList = editTeamUi.members.value - tm + updatedMember
            editTeamUi.members.value = newList
        } else {
            val newList = editTeamUi.invitedMembers.value - tm + updatedMember
            editTeamUi.invitedMembers.value = newList
        }
    }
 
    fun changeAdmin(tm: TeamMember, admin: Boolean) {
        val updatedMember = tm.apply { isAdmin = admin }
        if (editTeamUi.members.value.contains(tm)) {
            val newList = editTeamUi.members.value - tm + updatedMember
            editTeamUi.members.value = newList
        } else {
            val newList = editTeamUi.invitedMembers.value - tm + updatedMember
            editTeamUi.invitedMembers.value = newList
        }
    }
 
    fun updateValuesFromExistingTeam(teamToEdit: Team?) {
        if (teamToEdit != null) {
            editTeamUi.apply {
                name.value = teamToEdit.teamName
                desc.value = teamToEdit.teamDescription
                image.value = teamToEdit.profilePic
                members.value = teamToEdit.teamMembers
                invitedMembers.value = teamToEdit.invitedMembers
                category.value = teamToEdit.category
            }
        }
    }
 
 
    fun addRole(tm: TeamMember, role: String) {
        val updatedMember = tm.apply { roles += role }
 
        if (editTeamUi.members.value.contains(tm)) {
            val newList = editTeamUi.members.value - tm + updatedMember
            editTeamUi.members.value = newList
        } else {
            val newList = editTeamUi.invitedMembers.value - tm + updatedMember
            editTeamUi.invitedMembers.value = newList
        }
    }
 
    fun addInvitedProfiles(invited: List<Profile>) {
        val inv = invited.map {
            TeamMember(
                profileId = it.id,
            )
        }
        editTeamUi.members.value += inv
    }
 
    private fun validateName() {
        editTeamUi.nameError.value = if (editTeamUi.name.value.isBlank())
            "Team name cannot be empty"
        else ""
    }
 
    private fun validateMembers() {
        editTeamUi.membersError.value =
            if (editTeamUi.members.value.size + editTeamUi.invitedMembers.value.size == 1)
                "A team must contain at least one member"
            else ""
 
    }
 
    fun validateAll(): Boolean {
        validateName()
        validateMembers()
        return (editTeamUi.nameError.value.isBlank() && editTeamUi.membersError.value.isBlank())
    }
 
    fun addMember(profile: Profile) {
        editTeamUi.members.value += TeamMember(profile.id)
    }
 
    fun saveTeam(teamId : Int) {
        val updatedTeam = Team(
                teamId = teamId,
                invitedMembers = editTeamUi.invitedMembers.value,
                category = editTeamUi.category.value,
                teamDescription = editTeamUi.category.value,
                teamName = editTeamUi.name.value,
                teamMembers = editTeamUi.members.value,
                creationDate = Date(),
                profilePic = editTeamUi.image.value
            )
 
        if (updatedTeam.teamId == -1) {
            var newId = 0
            while (teamsList.value.map { it.teamId }.contains(newId)) {
                newId = abs(Random.nextInt())
            }
            updatedTeam.teamId = newId
            val newList = teamsList.value + updatedTeam
            teamsList.value = newList
        } else {
            teamsList.value = teamsList.value.map {
                if (updatedTeam.teamId == it.teamId) updatedTeam
                else it
            }
        }
    }
 
    fun resetEditTeamUi() {
        editTeamUi.apply {
            name.value = ""
            desc.value = ""
            invitedMembers.value = listOf()
            members.value = listOf()
            image.value = null
            category.value = ""
            membersError.value = ""
            nameError.value = ""
        }
    }
 
    fun findTeamMemberById(teamId: Int, profileId: Int): TeamMember? {
        val team = getTeamById(teamId)
        return team?.teamMembers?.find { it.profileId == profileId }
    }
}

Logged the Ui state at every recomposition. I got the same state for the last 3. In the first the fields has been not initialized yet.

2024-06-02 11:31:53.792  5813-5813  NE                      it.polito.lab4_manageteams           D  Recomposed EditTeam view with state: EditTeamUi(name=MutableState(value=)@94259335, desc=MutableState(value=)@233321396, members=MutableState(value=[])@215419869, image=MutableState(value=null)@119247698, category=MutableState(value=)@17622563, invitedMembers=MutableState(value=[])@45012768, nameError=MutableState(value=)@213747929, membersError=MutableState(value=)@222007966)
2024-06-02 11:31:54.155  5813-5813  NE                      it.polito.lab4_manageteams           D  Recomposed EditTeam view with state: EditTeamUi(name=MutableState(value=Backend Development)@94259335, desc=MutableState(value=Responsible for server-side logic, database management, and API integration.)@233321396, members=MutableState(value=[TeamMember(profileId=0, isAdmin=true, roles=[Developer, Designer]), TeamMember(profileId=1, isAdmin=false, roles=[Tester]), TeamMember(profileId=2, isAdmin=false, roles=[Developer]), TeamMember(profileId=3, isAdmin=true, roles=[Project Manager])])@215419869, image=MutableState(value=null)@119247698, category=MutableState(value=)@17622563, invitedMembers=MutableState(value=[TeamMember(profileId=4, isAdmin=false, roles=[Quality Assurance])])@45012768, nameError=MutableState(value=)@213747929, membersError=MutableState(value=)@222007966)
2024-06-02 11:31:54.822  5813-5813  NE                      it.polito.lab4_manageteams           D  Recomposed EditTeam view with state: EditTeamUi(name=MutableState(value=Backend Development)@94259335, desc=MutableState(value=Responsible for server-side logic, database management, and API integration.)@233321396, members=MutableState(value=[TeamMember(profileId=0, isAdmin=true, roles=[Developer, Designer]), TeamMember(profileId=1, isAdmin=false, roles=[Tester]), TeamMember(profileId=2, isAdmin=false, roles=[Developer]), TeamMember(profileId=3, isAdmin=true, roles=[Project Manager])])@215419869, image=MutableState(value=null)@119247698, category=MutableState(value=)@17622563, invitedMembers=MutableState(value=[TeamMember(profileId=4, isAdmin=false, roles=[Quality Assurance])])@45012768, nameError=MutableState(value=)@213747929, membersError=MutableState(value=)@222007966)
2024-06-02 11:31:54.879  5813-5813  NE                      it.polito.lab4_manageteams           D  Recomposed EditTeam view with state: EditTeamUi(name=MutableState(value=Backend Development)@94259335, desc=MutableState(value=Responsible for server-side logic, database management, and API integration.)@233321396, members=MutableState(value=[TeamMember(profileId=0, isAdmin=true, roles=[Developer, Designer]), TeamMember(profileId=1, isAdmin=false, roles=[Tester]), TeamMember(profileId=2, isAdmin=false, roles=[Developer]), TeamMember(profileId=3, isAdmin=true, roles=[Project Manager])])@215419869, image=MutableState(value=null)@119247698, category=MutableState(value=)@17622563, invitedMembers=MutableState(value=[TeamMember(profileId=4, isAdmin=false, roles=[Quality Assurance])])@45012768, nameError=MutableState(value=)@213747929, membersError=MutableState(value=)@222007966)

Upvotes: 0

Views: 40

Answers (1)

Waseem Abbas
Waseem Abbas

Reputation: 878

This is because you're updating the state inside the composable. Any state updates should happen either outside of a composable scope or inside a side effect like LaunchedEffect.

LaunchedEffect(teamId) {
        if (teamId != -1) {
            val team = viewModel.getTeamById(teamId)
            viewModel.updateValuesFromExistingTeam(team)
        } else {
            viewModel.addMember(viewModel.myProfile)
        }
    }

Upvotes: 0

Related Questions