Tworzenie list: jak zaimplementować RecyclerView w architekturze MVVM? Wykorzystaj DiffUtils

Korzystanie z RecyclerView stało się już standardem w przypadku aplikacji mobilnych. Głównie z uwagi na elastyczność i wiele możliwości rozszerzania. Dzięki nim RecyclerView sprawdza się, gdy trzeba wyświetlić listę na ekranie smartfonu lub tabletu.
Ciągły rozwój tego narzędzia wpływa na wzrost wydajności i łatwość implementacji. Nie jest jednak tak różowo, jak mogłoby się wydawać. Przy dużej liczbie dostępnych bibliotek, architektur i metod, wybór odpowiedniego rozwiązania nie jest prosty.
Poznaj jeden ze sposobów, jaki wykorzystujemy w Holdapp.
Czym jest RecyclerView?
Android RecyclerView to biblioteka, której używa się do wyświetlania dużych zbiorów danych. Jej działanie opiera się na ponownym użyciu widoków poprzez załadowanie do nich innych danych. Dzięki temu nie tracisz zasobów na tworzenie, przechowywanie czy niszczenie widoków.
Mechanizm jest dość prosty. Tworzysz widoki na podstawie szablonu (ViewHolder). Widoki wypełniane są danymi elementów, które akurat powinny być wyświetlane. Dodatkowo biblioteka RecyclerView trzyma w buforze załadowane widoki elementów spoza obszaru aktualnie wyświetlanego na ekranie. Celem jest zachowanie płynności działania.
DiffUtil – czym jest i dlaczego warto go używać?
To narzędzie, które pozwala obliczać różnicę pomiędzy dwoma listami. DiffUtil zwraca listę operacji, które przekształcają pierwszą listę w drugą. Możesz to wykorzystać do zwiększenia efektywności Adaptera stosowanego w RecyclerView.
DiffUtil używa algorytmu Eugene W. Mayersa. Służy on do obliczania minimalnej liczby aktualizacji potrzebnych do przekształcenia pierwotnej listy w wynikową. Wprawdzie sam algorytm nie wykrywa, które elementy zmieniły pozycję, ale DiffUtil sam sobie z tym poradzi.
Jaką rolę pełni ViewModel?
Jeśli bierzesz na tapet Clean Architecture + MVVM, aktualizacja stanu listy powinna odbywać się we ViewModel. Mowa tutaj nie tylko o zmianie długości listy, ale także o zmianie stanu wyświetlanych elementów.
Zmianę stanu można wywołać poprzez operację na tym elemencie (np. kliknięcie). ViewModel aktualizuje stan listy, która jest obserwowana z poziomu Aktywności lub Fragmentu. Musi jednak zostać poinformowany, na którym elemencie wywołano akcję.
Po zmianie obserwowanej listy następuje ponowne załadowanie nowej listy do Adaptera. Dzięki zastosowaniu DiffUtil proces aktualizacji listy będzie bardziej wydajny. Przebudowane będą tylko te elementy, które zmieniły stan.
Przykład
Przeanalizuję prosty przypadek użycia RecyclerView, jakim jest wyświetlenie listy klikalnych elementów. Kliknięcie powoduje zmianę koloru wyświetlanego tekstu.
Warstwa danych przekazuje do warstwy UI listę elementów, które mam pokazać na ekranie. Wyświetla ją warstwa UI. Proste? Spójrz na to od strony technicznej – przy założeniu, że zastosowano zalecaną architekturę i MVVM.
Warstwa wyświetlania elementów ma pokazać listę na podstawie aktualnego stanu aplikacji. Stan aplikacji (czyli lista do wyświetlenia) przechowywany jest we ViewModel. Lista została pobrana z warstwy danych poprzez Repository. ViewModel wywołuje akcję związaną z elementem listy.
W wywołaniu trzeba przekazać informacje potrzebne do aktualizacji stanu. Najczęściej przekazywany jest cały element wywołujący akcję. Ostatecznie na podstawie stanu powinien zostać zaktualizowany widok (MVVM).
Wyświetlanie elementów listy
Warto również rozważyć przypadek, kiedy nie wyświetlasz wszystkich informacji, jakie zawiera element pobrany z Repository. Taki element może być bardzo duży i obejmować wiele niepotrzebnych informacji i powiązań.
Wtedy przekształcasz element pobrany z Repository w taki sposób, żeby zawierał tylko informacje wyświetlane na liście. Dodatkowo musisz dodać atrybuty związane z wyświetlaniem elementu (jeśli nie są powiązane z atrybutem elementu pobranego z warstwy danych). Jak to wygląda w praktyce?
Przygotowanie do implementacji wyświetlania listy
Do implementacji używam Android Studio i narzędzi JetPack polecanych przez Google. Żeby pokazać jak grać, najpierw trzeba zbudować boisko, czyli należy zaimplementować Activity, Fragment, ViewModel, Repository i Model.
Poniżej przedstawiam tylko fragmenty kodu potrzebne do wyjaśnienia opisywanej wersji RecyclerView.
data class User(
val id: Int,
val firstName: String,
val lastName: String,
val nickName: String,
val email: String,
val city: String,
val country: String,
val speaksEnglish: Boolean,
val motto: String,
val position: JobCategory
)
Implementacja klasy User data
Jak proponowałem powyżej, przekształcam szczegółowy typ na taki, który ma tylko pola potrzebne do wyświetlenia listy i powiązania go z elementem bazowym.
data class UserPresentation(
val id: Int,
val firstName: String,
val lastName: String,
val isHighlighted: Boolean = false
)
Implementacja klasy UserPresentation
Klasa UserPresentation to data class, która powinna być strukturą niezmienną (według zaleceń dokumentacji języka Kotlin). Aby ją zmienić, muszę wykonać kopię głęboką. W tym celu dodano również metody rozszerzające listę elementów tego typu.
fun List<UserPresentation>.changeSingleSelectionOnListOfPresentationItems(
selectedItemId: Int
): List<UserPresentation> = this.map {
it.copy(
isHighlighted = if (it.id == selectedItemId) !it.isHighlighted else false
)
}
fun List<UserPresentation>.changeMultipleSelectionOnListOfPresentationItems(
selectedItemId: Int
): List<UserPresentation> = this.map {
it.copy(
isHighlighted = if (it.id == selectedItemId) !it.isHighlighted else it.isHighlighted
)
}
Metody rozszerzenia w pliku UserPresentationExtension
Metody aktualizują pole odpowiedzialne za wyświetlanie. Pierwsza z nich ma zastosowanie w sytuacji, kiedy można wybrać tylko jeden element.
Z drugiej korzysta się wtedy, gdy istnieje możliwość zaznaczenia większej liczby elementów.
Implementacja RecyclerView
We fragmencie lub aktywności potrzebuję adaptera.
private lateinit var listAdapter: UserRecyclerListAdapter
Deklaracja adaptera jako właściwości w klasie Fragmentu
Później inicjuję nowe pole i przypisuję do adaptera RecyclerView zadeklarowanego w widoku. Tutaj określam również layoutManager. Opcjonalnie dodaję element rozdzielający DividerItemDecoration.
private fun initRecyclerView() = with(binding.clickableListFragmentRecyclerview) {
listAdapter = UserRecyclerListAdapter(::onUserSelected)
adapter = listAdapter
layoutManager = LinearLayoutManager(requireContext())
addItemDecoration(
DividerItemDecoration(context, (layoutManager as LinearLayoutManager).orientation)
)
}
Inicjalizacja RecyclerView we Fragmencie
Powyższa metoda wywoływana jest w onViewCreated.
Implementacja DiffUtillCallback
object UserPresentationDiffUtilItemCallback :
DiffUtil.ItemCallback<UserPresentation>() {
override fun areItemsTheSame(oldItem: UserPresentation, newItem: UserPresentation): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: UserPresentation, newItem: UserPresentation): Boolean =
oldItem == newItem
}
Implementacja klasy UserPresentationDiffUtilCallback
Przy implementacji DiffUtillCallback typ porównywanych elementów został wdrożony z użyciem data class (zastanawiam się, czy wiesz dlaczego).
Implementacja Adaptera
class UserRecyclerListAdapter(
private val onItemClick: (Int) -> Unit
) : ListAdapter<UserPresentation, ViewHolder>(
UserPresentationDiffUtilItemCallback
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val itemBinding = UserPresentationItemViewBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return UserPresentationViewHolder(itemBinding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
(holder as UserPresentationViewHolder).bind(position)
}
inner class UserPresentationViewHolder(
private val itemBinding: UserPresentationItemViewBinding
) : RecyclerView.ViewHolder(itemBinding.root){
fun bind(position: Int){
val bindingItem = getItem(position)
itemBinding.userPresentationItemFirstName.text = bindingItem.firstName
itemBinding.userPresentationItemLastName.text = bindingItem.lastName
updateHighlight(bindingItem.isHighlighted)
itemBinding.userPresentationItemRoot.setOnClickListener {
onItemClick(bindingItem.id)
}
}
private fun updateHighlight(isHighlighted: Boolean){
if ( isHighlighted ){
itemBinding.root.setCardBackgroundColor(
ContextCompat.getColor(itemView.context, R.color.red)
)
itemBinding.userPresentationItemFirstName.let {
it.setTypeface( it.typeface, Typeface.BOLD)
}
itemBinding.userPresentationItemLastName.typeface = Typeface.DEFAULT_BOLD
}
else {
itemBinding.root.setCardBackgroundColor(
ContextCompat.getColor(itemView.context, R.color.grey_light)
)
itemBinding.userPresentationItemFirstName.typeface = Typeface.DEFAULT
itemBinding.userPresentationItemLastName.typeface = Typeface.DEFAULT
}
}
}
}
Implementacja klasy Adapter
Współpraca RecyclerView i ViewModel
private fun onUserSelected(presentationItemId: Int) {
viewModel.onSingleSelectUserPresentationClick(presentationItemId)
}
Metoda OnClick w implementacji SingleSelectFragment
private fun onUserSelected(presentationItemId: Int){
viewModel.onMultipleSelectUserPresentationClick(presentationItemId)
}
Metoda OnClick w implementacji MultipleSelectFragment
fun onSingleSelectUserPresentationClick(userId: Int) {
viewModelScope.launch(defaultDispatcher) {
userRepository.getUserById(userId).fold(
{ user ->
_stateRecyclerList.update { state ->
state.copy(
userPresentationsList = _stateRecyclerList.value.userPresentationsList
.changeSingleSelectionOnListOfPresentationItems(userId),
currentSelectedUser = user
)
}
},
{ _stateRecyclerList.update { state -> state.copy(error = it) } }
)
}
}
Metoda OnClick dla Single Select w implementacji ViewModel
fun onMultipleSelectUserPresentationClick(userId: Int) {
viewModelScope.launch(defaultDispatcher) {
userRepository.getUserById(userId).fold(
{ user ->
_stateRecyclerList.update { state ->
state.copy(
userPresentationsList = _stateRecyclerList.value.userPresentationsList
.changeMultipleSelectionOnListOfPresentationItems(userId),
currentSelectedUser = user
)
}
},
{ _stateRecyclerList.update { state -> state.copy(error = it) } }
)
}
}
Metoda OnClick w Multiple Select w implementacji ViewModel
Rezultat
Na ilustracjach widzisz, co można osiągnąć z pomocą przedstawionej tutaj implementacji. Na pierwszym ekranie obydwu grup znajduje się arkusz z „pustymi” wartościami.
Po wyborze elementu z listy arkusz wypełnia się danymi wybranej osoby.
W przypadku pierwszej grupy można wybrać tylko jeden element z listy. W drugiej grupie masz już kilka elementów. W obydwu przypadkach na ekranie głównym wyświetlają się dane ostatnio klikniętego elementu. Powtórnie kliknięty element listy jest odznaczany.
Mam nadzieję, że teraz lepiej rozumiesz, jak szybko zaimplementować RecyclerView z klikalnymi elementami. Na koniec ostatnia wskazówka – gdy mowa o metodzie opisanej w tym artykule, warto zwrócić uwagę na poprawną implementację Stanu, jego aktualizacje i poprawność współdziałania z DiffUtillCallback.
Powyższy kod aplikacji znajdziesz na GitHubie.
I to wszystko! Jeśli masz jakieś pytania, napisz je w komentarzu, a postaram się pomóc.