Android UI Completado

Search RecyclerView

Implementación de funcionalidad de búsqueda en RecyclerView con filtros en tiempo real y animaciones suaves

Search RecyclerView Demo

Descripción del Proyecto

Este proyecto demuestra la implementación de funcionalidad de búsqueda avanzada en RecyclerView con filtros en tiempo real. Incluye características como búsqueda por múltiples campos, filtros dinámicos, animaciones suaves y optimizaciones de rendimiento para listas grandes.

La implementación utiliza CardView para un diseño moderno, DataClass para modelos de datos eficientes, y algoritmos de filtrado optimizados que mantienen la fluidez de la interfaz incluso con datasets grandes.

Capturas de Pantalla

Lista inicial

Vista inicial con todos los elementos

Búsqueda activa

Búsqueda en tiempo real

Resultados filtrados

Resultados filtrados

Tecnologías Utilizadas

🔍

Search Algorithm

Búsqueda eficiente en tiempo real

🔄

RecyclerView

Lista optimizada y flexible

🎴

CardView

Diseño Material Design

📊

DataClass

Modelos de datos Kotlin

🎨

Filter Animations

Animaciones de filtrado

Kotlin

Lenguaje moderno y conciso

Características Principales

Búsqueda en Tiempo Real

Los resultados se actualizan instantáneamente mientras el usuario escribe, sin lag perceptible.

🎯

Búsqueda Multi-campo

Busca en múltiples campos simultáneamente: título, descripción, categoría, etc.

🔤

Búsqueda Inteligente

Búsqueda insensible a mayúsculas, acentos y con soporte para búsqueda parcial.

🎭

Animaciones Suaves

Transiciones fluidas cuando los elementos aparecen o desaparecen del filtro.

🚀

Alto Rendimiento

Optimizado para manejar listas con miles de elementos sin impacto en performance.

🎨

Highlighting

Resaltado automático de términos de búsqueda en los resultados mostrados.

Implementación Técnica

Adapter con Filtrado

class SearchableRecyclerAdapter(
    private val originalList: List<SearchableItem>
) : RecyclerView.Adapter<SearchableRecyclerAdapter.ViewHolder>(), Filterable {

    private var filteredList = originalList.toMutableList()
    private var currentQuery = ""

    override fun getFilter(): Filter {
        return object : Filter() {
            override fun performFiltering(constraint: CharSequence?): FilterResults {
                val query = constraint?.toString()?.lowercase()?.trim() ?: ""
                currentQuery = query
                
                val filtered = if (query.isEmpty()) {
                    originalList
                } else {
                    originalList.filter { item ->
                        item.isMatchingQuery(query)
                    }
                }
                
                return FilterResults().apply {
                    values = filtered
                    count = filtered.size
                }
            }
            
            override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
                val newList = results?.values as? List<SearchableItem> ?: emptyList()
                updateList(newList)
            }
        }
    }
    
    private fun updateList(newList: List<SearchableItem>) {
        val diffCallback = SearchItemDiffCallback(filteredList, newList)
        val diffResult = DiffUtil.calculateDiff(diffCallback)
        
        filteredList.clear()
        filteredList.addAll(newList)
        diffResult.dispatchUpdatesTo(this)
    }
}

Modelo de Datos Searchable

data class SearchableItem(
    val id: String,
    val title: String,
    val description: String,
    val category: String,
    val tags: List<String> = emptyList(),
    val imageUrl: String? = null
) {
    fun isMatchingQuery(query: String): Boolean {
        val normalizedQuery = query.lowercase().trim()
        
        return title.lowercase().contains(normalizedQuery) ||
               description.lowercase().contains(normalizedQuery) ||
               category.lowercase().contains(normalizedQuery) ||
               tags.any { tag -> tag.lowercase().contains(normalizedQuery) }
    }
    
    fun getHighlightedTitle(query: String): SpannableString {
        return highlightText(title, query)
    }
    
    fun getHighlightedDescription(query: String): SpannableString {
        return highlightText(description, query)
    }
    
    private fun highlightText(text: String, query: String): SpannableString {
        val spannable = SpannableString(text)
        
        if (query.isNotEmpty()) {
            val startIndex = text.lowercase().indexOf(query.lowercase())
            if (startIndex >= 0) {
                spannable.setSpan(
                    BackgroundColorSpan(Color.YELLOW),
                    startIndex,
                    startIndex + query.length,
                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
                )
            }
        }
        
        return spannable
    }
}

ViewHolder con Highlighting

class ViewHolder(private val binding: ItemSearchableBinding) : 
    RecyclerView.ViewHolder(binding.root) {
    
    fun bind(item: SearchableItem, query: String) {
        binding.apply {
            // Configurar CardView
            cardView.apply {
                radius = 12f
                cardElevation = 4f
                setCardBackgroundColor(
                    ContextCompat.getColor(context, R.color.surface)
                )
            }
            
            // Texto con highlighting
            if (query.isNotEmpty()) {
                textTitle.text = item.getHighlightedTitle(query)
                textDescription.text = item.getHighlightedDescription(query)
            } else {
                textTitle.text = item.title
                textDescription.text = item.description
            }
            
            textCategory.text = item.category
            
            // Cargar imagen si existe
            item.imageUrl?.let { url ->
                Glide.with(imageView.context)
                    .load(url)
                    .placeholder(R.drawable.placeholder)
                    .into(imageView)
            }
            
            // Tags
            chipGroup.removeAllViews()
            item.tags.forEach { tag ->
                val chip = Chip(chipGroup.context).apply {
                    text = tag
                    isClickable = false
                    chipBackgroundColor = ColorStateList.valueOf(
                        ContextCompat.getColor(context, R.color.primary_light)
                    )
                }
                chipGroup.addView(chip)
            }
            
            // Click listener
            root.setOnClickListener {
                // Manejar click en item
            }
        }
    }
}

Activity con SearchView

class SearchActivity : AppCompatActivity() {
    
    private lateinit var binding: ActivitySearchBinding
    private lateinit var adapter: SearchableRecyclerAdapter
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivitySearchBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        setupRecyclerView()
        setupSearchView()
    }
    
    private fun setupRecyclerView() {
        adapter = SearchableRecyclerAdapter(generateSampleData())
        
        binding.recyclerView.apply {
            adapter = this@SearchActivity.adapter
            layoutManager = LinearLayoutManager(this@SearchActivity)
            
            // Añadir animaciones personalizadas
            itemAnimator = DefaultItemAnimator().apply {
                addDuration = 300
                removeDuration = 300
                changeDuration = 300
            }
            
            // Decoración entre items
            addItemDecoration(
                DividerItemDecoration(this@SearchActivity, DividerItemDecoration.VERTICAL)
            )
        }
    }
    
    private fun setupSearchView() {
        binding.searchView.apply {
            setOnQueryTextListener(object : SearchView.OnQueryTextListener {
                override fun onQueryTextSubmit(query: String?): Boolean {
                    return false
                }
                
                override fun onQueryTextChange(newText: String?): Boolean {
                    adapter.filter.filter(newText)
                    return true
                }
            })
            
            // Configurar placeholder
            queryHint = "Buscar elementos..."
            
            // Focus automático
            requestFocus()
        }
    }
}

Optimizaciones de Rendimiento

  • DiffUtil: Cálculo eficiente de diferencias para animaciones suaves
  • Debouncing: Evita filtros excesivos durante escritura rápida
  • Background Threading: Filtrado en hilo de fondo para UI fluida
  • View Recycling: Reutilización optimizada de ViewHolders
  • Memory Management: Gestión cuidadosa de listas grandes
  • Lazy Loading: Carga diferida de contenido no visible