среда, 7 марта 2018 г.

Язык Котлин. Часть 5. Архитектурные компоненты. LiveData и ViewModel

В прошлой части я описывал компонент Room, упрощающий работу с базой данных SQLite. Но в реальных приложениях этот компонент редко когда применяется отдельно от других архитектурных компонентов. В этой части мы рассмотрим ещё два компонента, которые делают возможным автоматическое обновление данных в интерфейсе, если они меняются в базе данных.

Об одном компоненте (LiveData) уже было упомянуто а конце прошлой части. Это компонент, который сам следит за изменениями данных в базе и предоставляет классическое событие onChange (как обычно в виде функции обратного вызова) для прописывания в нём кода реакции на изменения (например, обновить данные в интерфейсе).

Ещё один компонент (ViewModel) будет являться вспомогательным. Он организует объект, находящийся между активностью и базой данных, который не пропадает при поворотах экрана, когда активность уничтожается и пересоздаётся вновь в другой ориентации. Всё взамодействие с базой прописывается внутри этого объекта, а активность в любой ориентации, когда будет готова, может сразу получить доступ к этому объекту и прочитать из него готовые данные, запрошенные, например, ещё в прошлом своём жизненном цикле при другой ориентации.

Раньше правильная работа с базой данных так и строилась, но всё писалось вручную. Теперь же у нас есть готовый компонент. Примером его использования может служить следующий код. Сначала создаём класс, который у нас будет посредником. Прописываем в нём инициализацию переменной нашей базой данных и опционально, к примеру, функцию удаления указанной записи:

class ListViewModel(application: Application) : AndroidViewModel(application) {
    // Инициализация переменных при создании класса
    val zhkhDatabase = ZhkhDatabase.getDatabase(application)         // Создать или получить базу данных
    var readoutList = zhkhDatabase?.readoutDAO()?.allReadoutItems    // Получить все данные из БД

    // Удалить запись
    fun deleteItem(readoutTable: ReadoutTable) {
        launch(CommonPool){ 
            zhkhDatabase?.readoutDAO()?.deleteReadout(readoutTable)  // Удалить указанную запись
        }
    }
}

А затем, в методе активности onCreate прописываем получение нашего объекта через провайдер этих самых объектов:

lateinit var viewModel: ListViewModel

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    viewModel = ViewModelProviders.of(this).get(ListViewModel::class.java)  // Создаём объект класса ListViewModel, в него считываются данные из БД
    ...
}

Если объекта нет (при первом запуске приложения), он создаётся и возвращается. Если есть, он просто возвращается, и через него мы получаем доступ ко всем тем процессом, которые начали в прошлом жизненном цикле активности. Всё.

Теперь вернёмся к основному нашему компоненту —  LiveData.

Room уже сделал за нас половину работы — в нём потенциально реализованы механизмы слежения за теми наборами данных, которые мы прописали в виде функций в интерфейсе DAO. Чтобы воспользоваться данной возможностью, надо просто указать Room, за какими конкретно наборами данных ему надо следить. Это делается всего лишь указанием скорректированного типа возвращаемых такими функциями данных, например:

// Было
@get:Query("select * from ReadoutTable")          
val allReadoutItems: List<readouttable>

// Стало
@get:Query("select * from ReadoutTable")          
val allReadoutItems: LiveData<List<ReadoutTable>>       // Обёртываем возвращаемое значение LiveData<...> чтобы отслеживать изменения в базе. При изменении данных будут рассылаться уведомления

Теперь Room знает, что надо следить, например, за всеми данными, и при изменениях в них возвращать список записей, обёрнутый в класс LiveData<T>.

На этом работа со стороны базы заканчивается. При изменении данных Room будет готов вызвать событие onChange, которое мы позже реализуем в основном коде в наблюдателе. Затем мы подпишем наблюдатель на это изменение, и Room будет готов предоставить ему на вход изменённые данные, как только активность будет готова.

Итак, нам осталось только создать наблюдателя и подписать его на конкретные данные. Делаем это в основном коде активности:

obs = Observer { readoutsList -> readoutsList?.let { recyclerViewAdapter.addItems(it) } }  // Создаём наблюдатель, который при изменении данных будет заполнять адаптер списка RecyclerView.
viewModel.readoutList?.observe(this@MainActivity, obs )                                    // Устанавливаем наблюдение и указываем наблюдателя, который будет получать данные

В этом коде создаётся наблюдатель, в котором прописывается реакция на изменение данных. Внутри класса наблюдателя запрятан единственный метод onChange, фактически находящийся внутри Room, который вызывается при изменении данных. Ему на вход поступает обновлённый набор данных. Поскольку метод единственный, мы можем описать его лямбда-выражением, что мы и делаем. Данные передаются в метод addItems адаптера, который мы напишем чуть позже. Адаптер примет в себя данные и отошлёт сообщение о том, что RecyclerView должен обновиться новыми данными.

Второй строчкой мы ставим наблюдатель на данные, завёрнутые в класс LeavData, которые нам возвращает интерфейс DAO. Объект readoutList этого класса имеет метод observe, позволяющий это сделать. В первом параметре передаётся контекст активности, потому что активность реализует метод getLifecycle, благодаря чему компонент LeavData знает, когда можно запускать onChange, а когда следует дождаться старта нового жизненного цикла, например, после поворота экрана. Вторым же параметром передаётся собственно наблюдатель, сформированный нами в предыдущей строчке.

И напоследок, пишем класс адаптера (привожу код из реального приложения):

class RecyclerViewAdapter(private var readoutTableList: List<readouttable>?, private val ctx: Context ) : RecyclerView.Adapter<recyclerviewadapter.recyclerviewholder="">() {

    // Создать вью элемента списка
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerViewHolder {
        val viewHolder = RecyclerViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.recycler_item, parent, false))
        return viewHolder
    }

    // Заполнение текущего вью
    override fun onBindViewHolder(holder: RecyclerViewHolder, position: Int) {
        val readoutModel = readoutTableList?.get(position) // Забрать из переданного в адаптер массива записей
        holder.tvAddrNum.text = readoutModel?.addr
        holder.tvHvsNum.text = readoutModel?.hvs
        holder.tvGvsNum.text = readoutModel?.gvs
        holder.tvT1Num.text = readoutModel?.t1
        holder.tvT2Num.text = readoutModel?.t2
        holder.tvT3Num.text = readoutModel?.t3
        holder.tvDate.text = DateFormat.getDateInstance(DateFormat.SHORT).format(readoutModel?.curDate)
        holder.tvComment.text = readoutModel?.comment
        holder.itemView.tag = readoutModel
        holder.itemView.setOnLongClickListener(ctx as View.OnLongClickListener) // Присвоить вьюхе интерфейс OnLongClickListener из основной активности
        holder.itemView.setOnClickListener(ctx as View.OnClickListener)         // Присвоить вьюхе интерфейс OnClickListener из основной активности
    }

    // Получить количество записей
    override fun getItemCount(): Int {
        return readoutTableList?.size ?: -1
    }

    // Переданные данные залить в адаптер и отправить сообщение, что данные изменились, чтобы заметил Observer и обновил recyclerView
    fun addItems(readoutTableList: List<readouttable>?) {
            this.readoutTableList = readoutTableList
            notifyDataSetChanged()
    }

    // Класс
    class RecyclerViewHolder(view: View) : RecyclerView.ViewHolder(view) {
            // Добавить в класс переменные и присвоить им элементы текущего вью
            val tvHvsNum = view.tvHvsNum
            val tvGvsNum = view.tvGvsNum
            val tvT1Num = view.tvT1Num
            val tvT2Num = view.tvT2Num
            val tvT3Num = view.tvT3Num
            val tvAddrNum = view.tvAddrNum
            val tvDate = view.tvDate
            val tvComment = view.tvComment
    }
}

В адаптере RecyclerView реализован классический вариант оптимизированного использования элементов списка, которые не пересоздаются при прокрутке, если в наличии имеются уже созданные (но уже ненужные, уехавшие за край экрана) элементы. Точно так же, как вы это, наверное, уже делали в адаптерах ListView, когда хотели ускорить прокрутку.

Дополнительно следует отметить присваивание формируемому элементу списка в onBindViewHilder слушателей нажатия и долгого нажатия, интерфейсы которых прописываются и реализуются в самой активности. Это позволит обрабатывать оба типа нажатий на каждом из элементов списка.

Адаптер подключаем в методе onCreate активности так:

recyclerViewAdapter = RecyclerViewAdapter(ArrayList(), this)    // Создать адаптер
recyclerView.adapter = recyclerViewAdapter                      // Присвоить адаптер списку
recyclerView.layoutManager = LinearLayoutManager(this)          // Создать и присвоить списку менеджер разметки
recyclerView.addItemDecoration(DividerItemDecoration(recyclerView.getContext(), (recyclerView.layoutManager as LinearLayoutManager).getOrientation())) // Добавить разделитель

Заодно добавляем полоску разделения между элементами списка. Теперь данные отслеживаются. Например, при вызове deleteItem запись будет удалена, что будет сразу отражено в интерфейсе.

На этом пока всё.

Комментариев нет:

Отправить комментарий