From ab151cc4090d5b3168f596eaec0c891b113ca0bc Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 29 Aug 2023 17:15:55 +0200 Subject: [PATCH] Fixed performances issue by replacing suggestions recyclerview by linearlayout... --- .../calls/adapter/SuggestionsListAdapter.kt | 66 ------ .../main/calls/fragment/StartCallFragment.kt | 59 +----- .../ui/main/calls/model/SuggestionModel.kt | 43 ++++ .../calls/viewmodel/StartCallViewModel.kt | 167 ++++++++++++++- .../viewmodel/SuggestionsListViewModel.kt | 190 ------------------ .../main/contacts/model/ContactAvatarModel.kt | 2 - .../org/linphone/utils/DataBindingUtils.kt | 6 + .../main/res/layout/call_start_fragment.xml | 34 ++-- .../res/layout/call_suggestion_list_cell.xml | 64 ++++++ app/src/main/res/layout/contact_list_cell.xml | 2 +- 10 files changed, 312 insertions(+), 321 deletions(-) delete mode 100644 app/src/main/java/org/linphone/ui/main/calls/adapter/SuggestionsListAdapter.kt create mode 100644 app/src/main/java/org/linphone/ui/main/calls/model/SuggestionModel.kt delete mode 100644 app/src/main/java/org/linphone/ui/main/calls/viewmodel/SuggestionsListViewModel.kt create mode 100644 app/src/main/res/layout/call_suggestion_list_cell.xml diff --git a/app/src/main/java/org/linphone/ui/main/calls/adapter/SuggestionsListAdapter.kt b/app/src/main/java/org/linphone/ui/main/calls/adapter/SuggestionsListAdapter.kt deleted file mode 100644 index 553c41b5c..000000000 --- a/app/src/main/java/org/linphone/ui/main/calls/adapter/SuggestionsListAdapter.kt +++ /dev/null @@ -1,66 +0,0 @@ -package org.linphone.ui.main.calls.adapter - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.annotation.UiThread -import androidx.databinding.DataBindingUtil -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.MutableLiveData -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import org.linphone.R -import org.linphone.databinding.ContactListCellBinding -import org.linphone.ui.main.contacts.model.ContactAvatarModel -import org.linphone.utils.Event - -class SuggestionsListAdapter( - private val viewLifecycleOwner: LifecycleOwner -) : ListAdapter(SuggestionDiffCallback()) { - val contactClickedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val binding: ContactListCellBinding = DataBindingUtil.inflate( - LayoutInflater.from(parent.context), - R.layout.contact_list_cell, - parent, - false - ) - return ViewHolder(binding) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - (holder as ViewHolder).bind(getItem(position)) - } - - inner class ViewHolder( - val binding: ContactListCellBinding - ) : RecyclerView.ViewHolder(binding.root) { - @UiThread - fun bind(contactModel: ContactAvatarModel) { - with(binding) { - model = contactModel - - lifecycleOwner = viewLifecycleOwner - - binding.setOnClickListener { - contactClickedEvent.value = Event(contactModel) - } - - executePendingBindings() - } - } - } - - private class SuggestionDiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: ContactAvatarModel, newItem: ContactAvatarModel): Boolean { - return oldItem.id == newItem.id - } - - override fun areContentsTheSame(oldItem: ContactAvatarModel, newItem: ContactAvatarModel): Boolean { - return true - } - } -} diff --git a/app/src/main/java/org/linphone/ui/main/calls/fragment/StartCallFragment.kt b/app/src/main/java/org/linphone/ui/main/calls/fragment/StartCallFragment.kt index 127f6c159..3cea39d7a 100644 --- a/app/src/main/java/org/linphone/ui/main/calls/fragment/StartCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/calls/fragment/StartCallFragment.kt @@ -32,9 +32,7 @@ import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R import org.linphone.core.tools.Log import org.linphone.databinding.CallStartFragmentBinding -import org.linphone.ui.main.calls.adapter.SuggestionsListAdapter import org.linphone.ui.main.calls.viewmodel.StartCallViewModel -import org.linphone.ui.main.calls.viewmodel.SuggestionsListViewModel import org.linphone.ui.main.contacts.adapter.ContactsListAdapter import org.linphone.ui.main.contacts.model.ContactAvatarModel import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener @@ -61,15 +59,7 @@ class StartCallFragment : GenericFragment() { R.id.startCallFragment ) - private val suggestionsListViewModel: SuggestionsListViewModel by navGraphViewModels( - R.id.startCallFragment - ) - private lateinit var contactsAdapter: ContactsListAdapter - private lateinit var suggestionsAdapter: SuggestionsListAdapter - - private var contactsListReady = false - private var suggestionsListReady = false private val listener = object : ContactNumberOrAddressClickListener { @UiThread @@ -122,59 +112,32 @@ class StartCallFragment : GenericFragment() { } } - binding.contactsList.layoutManager = LinearLayoutManager(requireContext()) - - suggestionsAdapter = SuggestionsListAdapter(viewLifecycleOwner) - binding.suggestionsList.setHasFixedSize(true) - binding.suggestionsList.adapter = suggestionsAdapter - - suggestionsAdapter.contactClickedEvent.observe(viewLifecycleOwner) { - it.consume { model -> - startCall(model) + viewModel.onSuggestionClickedEvent.observe(viewLifecycleOwner) { + it.consume { address -> + coreContext.postOnCoreThread { + coreContext.startCall(address) + } } } - binding.suggestionsList.layoutManager = LinearLayoutManager(requireContext()) + binding.contactsList.layoutManager = LinearLayoutManager(requireContext()) contactsListViewModel.contactsList.observe( viewLifecycleOwner ) { Log.i("$TAG Contacts list is ready with [${it.size}] items") - contactsAdapter.submitList(it) { - // Otherwise list won't show until keyboard is opened for example... - binding.contactsList.requestLayout() - } + contactsAdapter.submitList(it) viewModel.emptyContactsList.value = it.isEmpty() - if (suggestionsListReady && !contactsListReady) { - Log.i("$TAG Suggestions list is also ready, start postponed enter transition") - (view.parent as? ViewGroup)?.doOnPreDraw { - startPostponedEnterTransition() - } + Log.i("$TAG Suggestions list is also ready, start postponed enter transition") + (view.parent as? ViewGroup)?.doOnPreDraw { + startPostponedEnterTransition() } - contactsListReady = true - } - - suggestionsListViewModel.suggestionsList.observe(viewLifecycleOwner) { - Log.i("$TAG Suggestions list is ready with [${it.size}] items") - suggestionsAdapter.submitList(it) { - // Otherwise list won't show until keyboard is opened for example... - binding.suggestionsList.requestLayout() - } - viewModel.emptySuggestionsList.value = it.isEmpty() - - if (contactsListReady && !suggestionsListReady) { - Log.i("$TAG Contacts list is also ready, start postponed enter transition") - (view.parent as? ViewGroup)?.doOnPreDraw { - startPostponedEnterTransition() - } - } - suggestionsListReady = true } viewModel.searchFilter.observe(viewLifecycleOwner) { filter -> contactsListViewModel.applyFilter(filter) - suggestionsListViewModel.applyFilter(filter) + viewModel.applyFilter(filter) } } diff --git a/app/src/main/java/org/linphone/ui/main/calls/model/SuggestionModel.kt b/app/src/main/java/org/linphone/ui/main/calls/model/SuggestionModel.kt new file mode 100644 index 000000000..377542b78 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/main/calls/model/SuggestionModel.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.ui.main.calls.model + +import androidx.annotation.UiThread +import androidx.annotation.WorkerThread +import org.linphone.core.Address +import org.linphone.utils.LinphoneUtils + +class SuggestionModel @WorkerThread constructor( + val address: Address, + private val onClicked: ((Address) -> Unit)? = null +) { + companion object { + private const val TAG = "[Suggestion Model]" + } + + val name = LinphoneUtils.getDisplayName(address) + + val initials = LinphoneUtils.getInitials(name) + + @UiThread + fun onClicked() { + onClicked?.invoke(address) + } +} diff --git a/app/src/main/java/org/linphone/ui/main/calls/viewmodel/StartCallViewModel.kt b/app/src/main/java/org/linphone/ui/main/calls/viewmodel/StartCallViewModel.kt index 1f7da0a84..4b9981999 100644 --- a/app/src/main/java/org/linphone/ui/main/calls/viewmodel/StartCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/calls/viewmodel/StartCallViewModel.kt @@ -20,18 +20,183 @@ package org.linphone.ui.main.calls.viewmodel import androidx.annotation.UiThread +import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import java.util.ArrayList +import org.linphone.LinphoneApplication +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.contacts.ContactsListener +import org.linphone.core.Address +import org.linphone.core.Friend +import org.linphone.core.MagicSearch +import org.linphone.core.MagicSearchListenerStub +import org.linphone.core.SearchResult +import org.linphone.core.tools.Log +import org.linphone.ui.main.calls.model.SuggestionModel +import org.linphone.ui.main.model.isInSecureMode +import org.linphone.utils.Event +import org.linphone.utils.LinphoneUtils class StartCallViewModel @UiThread constructor() : ViewModel() { + companion object { + private const val TAG = "[Start Call ViewModel]" + } + val searchFilter = MutableLiveData() val emptyContactsList = MutableLiveData() - val emptySuggestionsList = MutableLiveData() + val suggestionsList = MutableLiveData>() + + val onSuggestionClickedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + private var currentFilter = "" + private var previousFilter = "NotSet" + private var limitSearchToLinphoneAccounts = true + + private lateinit var magicSearch: MagicSearch + + private val magicSearchListener = object : MagicSearchListenerStub() { + @WorkerThread + override fun onSearchResultsReceived(magicSearch: MagicSearch) { + Log.i("$TAG Magic search contacts available") + processMagicSearchResults(magicSearch.lastSearch) + } + } + + private val contactsListener = object : ContactsListener { + @WorkerThread + override fun onContactsLoaded() { + Log.i("$TAG Contacts have been (re)loaded, updating list") + applyFilter( + currentFilter, + if (limitSearchToLinphoneAccounts) LinphoneApplication.corePreferences.defaultDomain else "", + MagicSearch.Source.CallLogs.toInt() or MagicSearch.Source.ChatRooms.toInt() or MagicSearch.Source.Request.toInt(), + MagicSearch.Aggregation.Friend + ) + } + } + + init { + coreContext.postOnCoreThread { core -> + val defaultAccount = core.defaultAccount + limitSearchToLinphoneAccounts = defaultAccount?.isInSecureMode() ?: false + + coreContext.contactsManager.addListener(contactsListener) + magicSearch = core.createMagicSearch() + magicSearch.limitedSearch = false + magicSearch.addListener(magicSearchListener) + } + + applyFilter(currentFilter) + } + + @UiThread + override fun onCleared() { + coreContext.postOnCoreThread { + magicSearch.removeListener(magicSearchListener) + coreContext.contactsManager.removeListener(contactsListener) + } + super.onCleared() + } @UiThread fun clearFilter() { searchFilter.value = "" } + + @WorkerThread + fun processMagicSearchResults(results: Array) { + Log.i("$TAG Processing [${results.size}] results") + + val list = arrayListOf() + for (result in results) { + val address = result.address + + if (address != null) { + val friend = coreContext.core.findFriend(address) + // We don't want Friends here as they would also be in contacts list + if (friend == null) { + // If user-input generated result (always last) already exists, don't show it again + if (result.sourceFlags == MagicSearch.Source.Request.toInt()) { + val found = list.find { + it.address.weakEqual(address) + } + if (found != null) { + Log.i( + "$TAG Result generated from user input is a duplicate of an existing solution, preventing double" + ) + continue + } + } + + val model = SuggestionModel(address) { + onSuggestionClickedEvent.value = Event(it) + } + list.add(model) + } + } + } + + suggestionsList.postValue(list) + Log.i("$TAG Processed [${results.size}] results, extracted [${list.size}] suggestions") + } + + @UiThread + fun applyFilter(filter: String) { + coreContext.postOnCoreThread { + applyFilter( + filter, + if (limitSearchToLinphoneAccounts) LinphoneApplication.corePreferences.defaultDomain else "", + MagicSearch.Source.CallLogs.toInt() or MagicSearch.Source.ChatRooms.toInt() or MagicSearch.Source.Request.toInt(), + MagicSearch.Aggregation.Friend + ) + } + } + + @WorkerThread + private fun applyFilter( + filter: String, + domain: String, + sources: Int, + aggregation: MagicSearch.Aggregation + ) { + if (previousFilter.isNotEmpty() && ( + previousFilter.length > filter.length || + (previousFilter.length == filter.length && previousFilter != filter) + ) + ) { + magicSearch.resetSearchCache() + } + currentFilter = filter + previousFilter = filter + + Log.i( + "$TAG Asking Magic search for contacts matching filter [$filter], domain [$domain] and in sources [$sources]" + ) + magicSearch.getContactsListAsync( + filter, + domain, + sources, + aggregation + ) + } + + @WorkerThread + private fun createFriendFromSearchResult(searchResult: SearchResult): Friend { + val friend = coreContext.core.createFriend() + + val address = searchResult.address + if (address != null) { + friend.address = address + + friend.name = LinphoneUtils.getDisplayName(address) + friend.refKey = address.asStringUriOnly().hashCode().toString() + } + + return friend + } } diff --git a/app/src/main/java/org/linphone/ui/main/calls/viewmodel/SuggestionsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/calls/viewmodel/SuggestionsListViewModel.kt deleted file mode 100644 index ccecc02f1..000000000 --- a/app/src/main/java/org/linphone/ui/main/calls/viewmodel/SuggestionsListViewModel.kt +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright (c) 2010-2023 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.linphone.ui.main.calls.viewmodel - -import androidx.annotation.UiThread -import androidx.annotation.WorkerThread -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import java.util.ArrayList -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.contacts.ContactsListener -import org.linphone.core.Friend -import org.linphone.core.MagicSearch -import org.linphone.core.MagicSearchListenerStub -import org.linphone.core.SearchResult -import org.linphone.core.tools.Log -import org.linphone.ui.main.contacts.model.ContactAvatarModel -import org.linphone.ui.main.model.isInSecureMode -import org.linphone.utils.LinphoneUtils - -class SuggestionsListViewModel @UiThread constructor() : ViewModel() { - companion object { - private const val TAG = "[Suggestions List ViewModel]" - } - - val suggestionsList = MutableLiveData>() - - private var currentFilter = "" - private var previousFilter = "NotSet" - private var limitSearchToLinphoneAccounts = true - - private lateinit var magicSearch: MagicSearch - - private val magicSearchListener = object : MagicSearchListenerStub() { - @WorkerThread - override fun onSearchResultsReceived(magicSearch: MagicSearch) { - Log.i("$TAG Magic search contacts available") - processMagicSearchResults(magicSearch.lastSearch) - } - } - - private val contactsListener = object : ContactsListener { - @WorkerThread - override fun onContactsLoaded() { - Log.i("$TAG Contacts have been (re)loaded, updating list") - applyFilter( - currentFilter, - if (limitSearchToLinphoneAccounts) corePreferences.defaultDomain else "", - MagicSearch.Source.CallLogs.toInt() or MagicSearch.Source.ChatRooms.toInt() or MagicSearch.Source.Request.toInt(), - MagicSearch.Aggregation.Friend - ) - } - } - - init { - coreContext.postOnCoreThread { core -> - val defaultAccount = core.defaultAccount - limitSearchToLinphoneAccounts = defaultAccount?.isInSecureMode() ?: false - - coreContext.contactsManager.addListener(contactsListener) - magicSearch = core.createMagicSearch() - magicSearch.limitedSearch = false - magicSearch.addListener(magicSearchListener) - } - - applyFilter(currentFilter) - } - - @UiThread - override fun onCleared() { - coreContext.postOnCoreThread { - magicSearch.removeListener(magicSearchListener) - coreContext.contactsManager.removeListener(contactsListener) - } - super.onCleared() - } - - @WorkerThread - fun processMagicSearchResults(results: Array) { - Log.i("$TAG Processing [${results.size}] results") - suggestionsList.value.orEmpty().forEach(ContactAvatarModel::destroy) - - val list = arrayListOf() - - for (result in results) { - val address = result.address - - if (address != null) { - val friend = coreContext.core.findFriend(address) - // We don't want Friends here as they would also be in contacts list - if (friend == null) { - // If user-input generated result (always last) already exists, don't show it again - if (result.sourceFlags == MagicSearch.Source.Request.toInt()) { - val found = list.find { - it.friend.address?.weakEqual(address) == true - } - if (found != null) { - Log.i( - "$TAG Result generated from user input is a duplicate of an existing solution, preventing double" - ) - continue - } - } - - val fakeFriend = createFriendFromSearchResult(result) - val model = ContactAvatarModel(fakeFriend) - model.noAlphabet.postValue(true) - - list.add(model) - } - } - } - - suggestionsList.postValue(list) - Log.i("$TAG Processed [${results.size}] results, extracted [${list.size}] suggestions") - } - - @UiThread - fun applyFilter(filter: String) { - coreContext.postOnCoreThread { - applyFilter( - filter, - if (limitSearchToLinphoneAccounts) corePreferences.defaultDomain else "", - MagicSearch.Source.CallLogs.toInt() or MagicSearch.Source.ChatRooms.toInt() or MagicSearch.Source.Request.toInt(), - MagicSearch.Aggregation.Friend - ) - } - } - - @WorkerThread - private fun applyFilter( - filter: String, - domain: String, - sources: Int, - aggregation: MagicSearch.Aggregation - ) { - if (previousFilter.isNotEmpty() && ( - previousFilter.length > filter.length || - (previousFilter.length == filter.length && previousFilter != filter) - ) - ) { - magicSearch.resetSearchCache() - } - currentFilter = filter - previousFilter = filter - - Log.i( - "$TAG Asking Magic search for contacts matching filter [$filter], domain [$domain] and in sources [$sources]" - ) - magicSearch.getContactsListAsync( - filter, - domain, - sources, - aggregation - ) - } - - @WorkerThread - private fun createFriendFromSearchResult(searchResult: SearchResult): Friend { - val friend = coreContext.core.createFriend() - - val address = searchResult.address - if (address != null) { - friend.address = address - - friend.name = LinphoneUtils.getDisplayName(address) - friend.refKey = address.asStringUriOnly().hashCode().toString() - } - - return friend - } -} diff --git a/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt index b68c21de1..083656ee6 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt @@ -51,8 +51,6 @@ class ContactAvatarModel @WorkerThread constructor(val friend: Friend) { val firstContactStartingByThatLetter = MutableLiveData() - val noAlphabet = MutableLiveData() - val showTrust = MutableLiveData() private val friendListener = object : FriendListenerStub() { diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index 9b0c4aba0..053b13087 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -201,6 +201,12 @@ fun ImageView.setPresenceIcon(presence: ConsolidatedPresence?) { setImageResource(icon) } +@UiThread +@BindingAdapter("avatarInitials") +fun AvatarView.loadInitials(initials: String?) { + avatarInitials = initials.orEmpty() +} + @UiThread @BindingAdapter("accountAvatar") fun AvatarView.loadAccountAvatar(account: AccountModel?) { diff --git a/app/src/main/res/layout/call_start_fragment.xml b/app/src/main/res/layout/call_start_fragment.xml index 938686b93..05bbf3841 100644 --- a/app/src/main/res/layout/call_start_fragment.xml +++ b/app/src/main/res/layout/call_start_fragment.xml @@ -145,7 +145,7 @@ android:layout_height="wrap_content" android:layout_margin="10dp" android:src="@drawable/illu" - android:visibility="gone" + android:visibility="@{viewModel.emptyContactsList && viewModel.suggestionsList.size() == 0 ? View.VISIBLE : View.GONE}" app:layout_constraintBottom_toTopOf="@id/no_contacts_nor_suggestion_label" app:layout_constraintDimensionRatio="1:1" app:layout_constraintEnd_toEndOf="parent" @@ -161,7 +161,7 @@ android:layout_height="wrap_content" android:text="No suggestion and no contact for the moment..." android:textSize="16sp" - android:visibility="gone" + android:visibility="@{viewModel.emptyContactsList && viewModel.suggestionsList.size() == 0 ? View.VISIBLE : View.GONE}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/no_contacts_nor_suggestion_image" /> @@ -169,7 +169,7 @@ + app:layout_constraintBottom_toTopOf="@id/suggestions_list"/> - diff --git a/app/src/main/res/layout/call_suggestion_list_cell.xml b/app/src/main/res/layout/call_suggestion_list_cell.xml new file mode 100644 index 000000000..15b2b565c --- /dev/null +++ b/app/src/main/res/layout/call_suggestion_list_cell.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/contact_list_cell.xml b/app/src/main/res/layout/contact_list_cell.xml index a3f82a79c..6fe1cd7aa 100644 --- a/app/src/main/res/layout/contact_list_cell.xml +++ b/app/src/main/res/layout/contact_list_cell.xml @@ -45,7 +45,7 @@ android:layout_height="wrap_content" android:layout_marginStart="12dp" android:text="@{model.firstLetter, default=`A`}" - android:visibility="@{model.noAlphabet ? View.GONE : model.firstContactStartingByThatLetter ? View.VISIBLE : View.INVISIBLE}" + android:visibility="@{model.firstContactStartingByThatLetter ? View.VISIBLE : View.INVISIBLE}" android:textColor="@color/gray_10" android:textSize="20sp" app:layout_constraintStart_toStartOf="parent"