JavaRush /Java блог /Random UA /Складні списки – це просто
Paul Soia
26 рівень
Kiyv

Складні списки – це просто

Стаття з групи Random UA
Всім привіт. Вирішив порушити таку тему, як складні списки в андроїді. Ця тема спочатку ставила мене в глухий кут. Це здавалося мені дуже складним, але насправді все простіше. І гадаю, тому, хто зараз зіткнувся з цією проблемою, стаття буде корисною. Усі приклади написані на Kotlin. Я намагався скрізь писати коментарі та максимально зрозуміло зробити для тих, хто пише на Java. Отже, ось наша структура класів: Складні списки - це просто - 1MainActivityце основний клас. ListAdapter- Тут ми робимо прив'язки нашого списку до view-елементів. ViewHolder(їх там кілька штук) це наша розмітка для кожного типу елемента. Item-класи – це дані для списку (pojo). Xml-файли - це розмітки ( activity_mainдля головного екрана, інші для кожного типу списку). Перше, що нам треба зробити, — додати залежність, щоб ми могли використовувати списки (при створенні проекту чомусь цієї залежності немає). У файлі build.gradleв блоці dependenciesдодаємо рядок:
implementation "androidx.recyclerview:recyclerview:1.1.0"
Далі знаходимо файл activity_main.xml: Складні списки - це просто - 2Тут є лише наш елемент для списків - RecyclerView. Будь-які відступи та форматування додаємо за смаком. Далі створюємо клас BaseViewHolder. Він буде абстрактним, тому що від нього успадковуватимуться всі наші класи ViewHolder. Цілком можна без нього обійтися, але все ж таки рекомендую робити, щоб всі класи-спадкоємці були однаковими за структурою. (В Kotlin замість extendsі implementвикористовується :. Як і в Java, тут можна успадковуватися від одного класу і реалізувати багато інтерфейсів) Далі створюємо наші дата класи: ItemTitle, ItemGoogleі ItemApple. У Java це звичайні класи з конструктором та гетерами.
data class ItemGoogle(
    val name: String,
    val product: String,
    val version: String,
    val isUse: Boolean
) : ListMarker
data class ItemApple(
    val name: String,
    val country: String,
    val year: Int
) : ListMarker
data class ItemTitle(
    val title: String,
    val amount: Int
) : ListMarker
Зупинимося тут на інтерфейсі ListMarker.
//это наш маркер. Его должны реализовать все айтемы, которые будут
//отображаться в конечном итоге в списке
interface ListMarker
Всі наші дата-класи реалізують цей інтерфейс, щоб привести їх до одного типу. Це нам знадобиться далі для нашого списку. Далі створюємо наші вьюхолдер для кожного елемента списку. Не забуваємо кожен із них успадковувати від BaseViewHolder. У цих класах ми задаємо, який елемент viewвідповідає полю із дата-класів.
abstract class BaseViewHolder<t>(itemView: View) : RecyclerView.ViewHolder(itemView) {
    abstract fun bind(item: T)
}
class GoogleViewHolder(view: View) : BaseViewHolder<itemgoogle>(view) {

    override fun bind(item: ItemGoogle) {
        itemView.tvName.text = item.name
        itemView.tvProduct.text = item.product
        itemView.tvVersion.text = item.version
        if (item.isUse) {
            itemView.tvProduct.visibility = View.GONE
        } else {
            itemView.tvProduct.visibility = View.VISIBLE
        }
    }
}
class AppleViewHolder(view: View) : BaseViewHolder<itemapple>(view) {

    override fun bind(item: ItemApple) {
        //можем делать так
        itemView.tvName.text = item.name
        itemView.tvCountry.text = item.country
        itemView.tvYear.text = item.year.toString()

        /*----сверху и снизу два идентичных блока----*/

        //а можем сделать такой блок и не использовать в каждой строке itemView
        with(itemView) {
            tvName.text = item.name
            tvCountry.text = item.country
            tvYear.text = item.year.toString()
        }
    }
}
class TitleViewHolder(view: View) : BaseViewHolder<itemtitle>(view) {

    override fun bind(item: ItemTitle) {
        itemView.tvTitle.text = item.title
        itemView.tvAmount.text = item.amount.toString()
    }
}
Тут особливо ніякої логіки немає, все дуже просто. Також своя верстка для кожного вьюхолдера: list_item_title.xmlСкладні списки - це просто - 3list_item_google.xmlСкладні списки - це просто - 4list_item_apple.xmlСкладні списки - це просто - 5Тепер переходимо до найскладнішого — ListAdapter.
class ListAdapter : RecyclerView.Adapter<baseviewholder<*>>() {

    companion object {
        //задаем константы для каждого типа айтема
        private const val TYPE_TITLE = 0
        private const val TYPE_GOOGLE = 1
        private const val TYPE_APPLE = 2
    }

    //здесь можно использовать обычный ArrayList
    //сюда добавляются все айтемы, которые реализовали интерфейс ListMarker
    //як вариант можно было сделать mutableListOf<any>() и обойтись без интерфейса
    private val items = mutableListOf<listmarker>()

    internal fun swapData(list: List<listmarker>) {
        items.clear()
        items.addAll(list)
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<*> {
        return when(viewType) {
            //задаем разметку для каждого типа айтема
            TYPE_TITLE -> TitleViewHolder(parent.inflate(R.layout.list_item_title))
            TYPE_GOOGLE -> GoogleViewHolder(parent.inflate(R.layout.list_item_google))
            TYPE_APPLE -> AppleViewHolder(parent.inflate(R.layout.list_item_apple))
            else -> throw IllegalArgumentException("Invalid view type")
        }
    }

    override fun getItemViewType(position: Int): Int {
        return when (items[position]) {
            is ItemTitle -> TYPE_TITLE
            is ItemGoogle -> TYPE_GOOGLE
            is ItemApple -> TYPE_APPLE
            else -> throw IllegalArgumentException("Invalid type of item $position")
        }
    }

    override fun getItemCount(): Int {
        //этот метод определяет размер списка
        return items.size
    }

    override fun onBindViewHolder(holder: BaseViewHolder<*>, position: Int) {
        val element = items[position]
        when (holder) {
            //отправляем каждый айтем к своему ViewHolder
            is TitleViewHolder -> holder.bind(element as ItemTitle)
            is GoogleViewHolder -> holder.bind(element as ItemGoogle)
            is AppleViewHolder -> holder.bind(element as ItemApple)
            else -> throw IllegalArgumentException()
        }
    }
}
</listmarker></listmarker></any></baseviewholder<*>
Коли ми успадковуємо наш клас від RecyclerView.Adapter, то треба буде перевизначити три методи: onCreateViewHolder, getItemCount, onBindViewHolder. onCreateViewHolder- Тут ми ініціалізуємо наші класи ViewHolder. getItemCount- Цей метод відповідає за розмір списку. onBindViewHolder- Тут ми передаємо елементи списку до наших класів ViewHolder. Для звичайного списку з одним типом цього елемента було б достатньо. Але для різних типів треба ще перевизначити метод getItemViewType(для цього використовуємо константи, які є у верху нашого класу-адаптера. У Java можна використовувати finalзмінні для цього). Також у Java у методі onBindViewHolderзамість виразу whenможна використовувати звичайний if. І, нарешті, переходимо до нашого головного класу - MainActivity.
class MainActivity : AppCompatActivity() {

    private val adapter = ListAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initRecyclerView()
    }

    private fun initRecyclerView() {
        rvList.layoutManager = LinearLayoutManager(this)

        //здесь мы задаем разделитель между айтемами, чтоб они не сливались друг с другом
        val divider = DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
        rvList.addItemDecoration(divider)

        rvList.adapter = adapter
        stubData()
    }

    private fun stubData() {
        val list = mutableListOf<listmarker>()
        list.add(ItemTitle("title1", 4))
        list.add(ItemGoogle("android", "product1", "17.0v", true))
        list.add(ItemGoogle("no name", "product2", "3.1v", false))
        list.add(ItemApple("macOs", "USA", 2005))
        list.add(ItemApple("iOs", "China", 2007))
        list.add(ItemTitle("title2", 2))
        list.add(ItemGoogle("map", "product3", "23.0v", true))
        list.add(ItemApple("car", "England", 2018))
        list.add(ItemTitle("title3", 0))
        //отправляем все данные в адаптер
        adapter.swapData(list)
    }
}
</listmarker>
Тут відбувається ініціалізація нашого RecyclerViewта ListAdapter. Це досить шаблонний код, з яким багато хто стикався. У методі stubDataя заповнив список даними (як ви бачите, дані з різними елементами) і передав цей список адаптер. Далі запускаємо нашу програму, і ми повинні побачити щось таке на нашому екрані: Складні списки - це просто - 6Як бачите, в одному списку знаходяться різні елементи, що й було нашою метою. PS Забув згадати про файл Extension. Ось так він виглядає:
//это расширение для класса ViewGroup. Теперь мы можем по всему проекту использовать
//короткое inflate замість длинного LayoutInflater.from(context).inflate
fun ViewGroup.inflate(@LayoutRes layoutRes: Int, attachToRoot: Boolean = false): View =
    LayoutInflater.from(context).inflate(layoutRes, this, attachToRoot)
Це не клас, а файл, тому немає назви всередині нього. Проте тепер ми можемо використовувати в адаптері просто inflateзамість довгої конструкції. На жаль, Java це не передбачено, тому пишіть LayoutInflater.from(context).inflate. На цьому все. До наступних зустрічей :) Посилання на GitHub
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ