среда, 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 запись будет удалена, что будет сразу отражено в интерфейсе.

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

вторник, 6 марта 2018 г.

Язык Котлин. Часть 4. Архитектурные компоненты. Room, работа с БД.

Платформа Андроид до 2017-го года страдала полным отсутствием штатных высокоуровневых компонентов, которые позволяли бы выполнять рутинные операции по работе с базой данных, не городить огород из костылей для адекватной обработки поворотов экрана и т.п. Вообще, складывается ощущение, что у Гугла нет главного архитектора, который бы спроектировал удобную библиотечную среду для комфортного написания программ. Программирование под Андройд всегда сопровождалось и сопровождается постоянными спотыканиями на ровном месте. Скажем прямо — среда продумана плохо.

Тем не менее, ситуация немного сдвигается с мёртвой точки. В мае 2017 года Гуглом были представлены некоторые архитектурные компоненты, призванные попробовать решить часть вышеописанных проблем. Конечно, это только первый шаг, поскольку всё ещё остаётся масса вопросов по их применению в реальных приложениях в случае, когда приложения эти чуть отличаются от упрощённых синтетических примеров. То ли так плохо спроектирована сама среда, то ли они пустили всё на самотёк, науке это неизвестно, но программировать под Андроид даже с новыми компонентами всё ещё очень сложно.

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

Room

Компонент Room представляет собой обёртку над базой данных SQLite для упрощения работы с ней. Она позволяет избежать собственноручного написания тонн кода, когда мы реализовывали своё взаимодействие с базой с использованием SQLiteOpenHelper, который, в свою очередь, тоже является некой обёрткой над ней. Фактически Room — это обёртка над обёрткой SQLiteOpenHelper.

Room действительно лихо упрощает работу с базой данных в плане автоматизации её создания и реализации запросов, хотя отслеживание изменений в ней (для этого используется другой компонент LiveData) вызывает много вопросов, когда дело касается работы не с одним, полным, а с разными возвращаемыми наборами данных из разных запросов, отображаемых в одном и том же RecyclerView. Рекомендуемой практики я не нашёл, поэтому придумал аж три варианта такой работы, но ни один из них не оставил у меня ощущения правильности. Но сейчас не об этом. Разберём работу с Room в отдельности от всего остального.

Для работы с базой данных достаточно подключить библиотеку в build.gradle модуля в разделе dependencies:

    // Room
    implementation "android.arch.persistence.room:runtime:$room_version"
    kapt "android.arch.persistence.room:compiler:$room_version"

где $room_version прописана в build.gradle проекта в разделе buildscript, например:

    ext {
        kotlin_version = '1.2.30'
        room_version = '1.1.0-alpha3'
        coroutines_version = '0.22.2'
        support_version = '27.1.0'
    }

Теперь создаём файл, например, DB.kt, в котором прописываем два класса и один интерфейс. Один класс описывает таблицу, второй — базу, в которой эта таблица содержится, а интерфейс описывает запросы, которые будут сформированы к этой базе. В качестве примеров я буду демонстрировать реальный код моего приложения по учёту показаний счётчиков ЖКХ.

Класс, описывающий таблицу (я добавил сюда дополнительный класс конвертера для поля даты, пусть он вас не смущает):

@Entity
class ReadoutTable(
        @ColumnInfo var addr: String?,  // String? - Поля могут иметь значения NULL
        @ColumnInfo var hvs: String?,
        @ColumnInfo var gvs: String?,
        @ColumnInfo var t1: String?,
        @ColumnInfo var t2: String?,
        @ColumnInfo var t3: String?,
        @ColumnInfo var comment: String?,
        @field:TypeConverters(DateConverter::class)   // Добавим дополнительные преобразователи типов (а именно, написанный нами класс DateConverter),                                                  
        @ColumnInfo var curDate: Date?                // которые может использовать поле даты. "field:" - применение аннотации TypeConverters к полю.
){

    @PrimaryKey(autoGenerate = true) var id: Long = 0 // Long - без вопросительного знака, значит поле NOT NULL
}

/*
 * Класс конвертеров Long в Date и обратно для TypeConverters() в Room, чтобы автоматически сохранять в базе типы, на которые она не рассчитана
 * Та или иная функция выбирается автоматически исходя из входного типа данных
 */
class DateConverter {
    @TypeConverter  // Указать, что метод является конвертером
    fun toDate(timestamp: Long?) = timestamp?.let { Date(it) }
    @TypeConverter  // Указать, что метод является конвертером
    fun toTimestamp(date: Date?) = date?.time
}

Класс, описывающий взаимодействие с базой:

/*
 * Интерфейс доступа к базе данных (DAO - Data Access Object)
 */
@Dao
@TypeConverters(DateConverter::class)                   // Указываем, что при доступе к некоторым полям будет задействован наш конвертер DateConverter
interface ReadoutModelDao {

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

    @Query("select * from ReadoutTable where id = :id")
    fun getReadoutById(id: Long): ReadoutTable

    @Query("select * from ReadoutTable where addr = :addr")
    fun getReadoutByAddr(addr: String): List<ReadoutTable>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun addReadout(readoutModel: ReadoutTable)

    @Update
    fun updateReadout(readoutModel: ReadoutTable)

    @Delete
    fun deleteReadout(readoutModel: ReadoutTable)
}

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

/*
 * Описание базы данных.
 */
@Database(entities = [ReadoutTable::class], version = 3)  // Перечисляем в entities, какие классы будут использоваться для создания таблиц.
abstract class ZhkhDatabase : RoomDatabase() {

    abstract fun readoutDAO(): ReadoutModelDao           // Описываем абстрактные методы для получения объектов интерфейса BorrowModelDao, которые вам понадобятся

    // Сопутствующий объект для получения базы данных (фактически синглтон). Можно не использовать.
    companion object {

        private var INSTANCE: ZhkhDatabase? = null

        fun getDatabase(context: Context): ZhkhDatabase? {
            if (INSTANCE == null) {
                INSTANCE = Room.databaseBuilder<zhkhdatabase>(context, ZhkhDatabase::class.java, "readout_db")
                        .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
                        .build()
            }
            return INSTANCE
        }

        fun destroyInstance() {
            INSTANCE = null
        }

        // Апдейт базы с 1 на 2 версию, добавление поля
        val MIGRATION_1_2: Migration = object : Migration(1, 2) {
            override fun migrate(database: SupportSQLiteDatabase) {
                database.execSQL("ALTER TABLE ReadoutTable ADD COLUMN comment TEXT NOT NULL DEFAULT ''")
            }
        }

        // Апдейт базы с 2 на 3 версию, переделка всех полей из NOT NULL в NULL
        val MIGRATION_2_3: Migration = object : Migration(2, 3) {
            override fun migrate(database: SupportSQLiteDatabase) {
                database.execSQL("ALTER TABLE ReadoutTable RENAME TO ReadoutTable_old")
                database.execSQL("CREATE TABLE ReadoutTable (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT DEFAULT 0, addr TEXT NULL, hvs TEXT NULL, gvs TEXT NULL, t1 TEXT NULL, t2 TEXT NULL, t3 TEXT NULL, comment TEXT NULL, curDate INTEGER)")
                database.execSQL("INSERT INTO ReadoutTable(id, addr, hvs, gvs, t1, t2, t3, comment, curDate) SELECT id, addr, hvs, gvs, t1, t2, t3, comment, curDate FROM ReadoutTable_old")
                database.execSQL("DROP TABLE ReadoutTable_old")
            }
        }
    }
}

Получение объекта базы данных будет выглядеть так:

val zhkhDatabase = ZhkhDatabase.getDatabase(this.getApplication())  // Создать или получить базу данных

И теперь читаем набор данных из таблицы (вернётся только одна запись с id = 0, а из неё читаем поле addr) с помощью прописанной в интерфейсе БД функции. Поле addr выводим в интерфейс:

val r = async(CommonPool) {                           // Асинхронно считаем данные из базы. Получаем запись с идентификатором ноль.
    zhkhDatabase?.readoutDAO()?.getReadoutById(0)     // Считаем нулевую запись. Результат последнего выражения потока возвращается в переменную r
}
launch (UI) {                                         // Запустить и забыть. Поток в потоке интерфейса UI. Ждём данные и выводим их в интерфейс.
    var rec = r.await()                               // Подождём результата.
    editText.setText(rec?.addr)                       // Выведем в интерфейс.
}

Апдейт нулевой записи:

var gorod = etAddr.text.toString()
launch(CommonPool) {
    val rec = zhkhDatabase?.readoutDAO()?.getReadoutById(0) // Асинхронно считаем данные из базы. Получаем запись с идентификатором ноль.
    if (rec != null) {
        rec.addr = gorod                                    // Заносим новые данные в поле адреса.
        zhkhDatabase?.readoutDAO()?.updateReadout(rec)      // Обновляем запись в базе.
    }                                    
}

Аналогично производится удаление и добавление записей. Вот, примерно так работает Room.

Следующий вопрос, который сразу встаёт, это как осуществить автосинхронизацию данных в таблице БД и в интерфейсе. А вот для этого Room умеет возвращать свои записи не только в голом виде в виде простых списков, но и внутри специального класса, который рассылает сообщения своим подписчикам при изменении заключенных в него данных. Возвращаемые данные внутри такого класса становятся наблюдаемыми. Для этого применяется ещё один архитектурный компонент LiveData.

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

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

четверг, 1 марта 2018 г.

Язык Котлин. Часть 3. Многопоточность. Сопрограммы (корутины).

В Котлин появились удобные и компактные инструменты для организации многопоточности — сопрограммы (или, как калька с английского, корутины). Для их использования в файле build.gradle модуля app в раздел dependencies (зависимости) дописываем строчки:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.22.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.22.2" // Поддержка UI

Они подключат библиотеки сопрограмм. Далее, проверяем, чтобы вначале этого же файла был подключен плагин обработчика Котлиновских аннотаций:

apply plugin: 'kotlin-kapt' // Обработчик аннотаций для Kotlin (работает и с java-аннотациями).

А разделе dependencies везде annotationProcessor замените на kapt. Это должно быть сделано во всех Котлин-проектах, иначе при компиляции могут вылезать ошибки.

Например, без плагина kotlin-kapt при использовании библиотеки Room при компиляции возникает ошибка в @Dao: Error:Each bind variable in the query must have a matching method parameter. Cannot find method parameters for :id. Часто эту ошибку обходят использованием arg0 вместо id, но это не совсем правильно.

Пример использования сопрограмм

Давайте рассмотрим использование сопрограмм на примере получения данных и вывода их в интерфейс. Инициируем действие нажатием на кнопку, код которой разместим в тестовой активности:

override fun onClick(v: View?) {
    Log.i("TEST","1.Нажали кнопку")   // Информируем в лог
    textView.text = "1.Нажали кнопку" // Информируем в интерфейс, тут можно обновлять интерфейс

    // ЗАПУСК «ТЯЖЁЛОЙ РАБОТЫ»
    // Запускаем первую сопрограмму (в фоновом пуле потоков CommonPool) и присваиваем её переменной myThread
    val myThread = async(CommonPool) {                 // Запустить сопрограмму и присвоить её переменной myThread.
        Log.i("TEST","3.Первая сопрограмма запущена")  // Информируем только в лог. Интерфейс обновлять нельзя.
        sleep(2000)                                    // Типа что-то делаем длительное
        Log.i("TEST","6.Первая сопрограмма завершена") // Информируем только в лог. Интерфейс обновлять нельзя.
        "Это типа результат работы первой сопрограммы" // Результат работы сопрограммы - результат последнего действия
    }
    Log.i("TEST","2.Запустили первую сопрограмму и продолжаем")
    // Тут можем продолжать работу, а сопрограмма выполняется в фоне.

    // ПРИЁМ И ОБРАБОТКА РЕЗУЛЬТАТА
    // Запустим вторую сопрограмму (теперь в контексте пользовательского интерфейса)
    // Она просто будет ожидать результат и выведет его в интерфейс
    launch (UI) {  // Запустить и забыть. Владелец сопрограммы - пользовательский интерфейс UI.
        Log.i("TEST","5.Вторая сопрограмма запущена")  // Информируем в лог, но можно и в интерфейс
        val myResult = myThread.await()                // Подождём результата
        Log.i("TEST","7.Вторая сопрограмма завершена, результат: $myResult")   // Информируем в лог, но можно и в интерфейс
        textView.text = "7.Вторая сопрограмма завершена, результат: $myResult" // Выводим результат в интерфейс
    }
    Log.i("TEST","4.Запустили вторую сопрограмму и продолжаем") // Информируем в лог, но можно и в интерфейс
    // Тут можем продолжать работу, а сопрограмма дождётся результата, и сама выведет его в интерфейс.
}

В область импорта добавим библиотеку контекста пользовательского интерфейса:

import kotlinx.coroutines.experimental.android.UI

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

Итак, при нажатии на кнопку мы выводим сообщение об этом в лог и в textView. Затем запускаем асинхронную сопрограмму, и присваиваем её переменной myThread. Присваивается не результат выполнения сопрограммы, а именно живая сопрограмма. Через метод await этой сопрограммы, выраженной переменной, мы сможем в дальнейшем получить и сам результат.

Асинхронная сопрограмма запущена в фоновом пуле потоков CommonPool, и мы не можем напрямую выводить оттуда информацию в поток интерфейса. Поэтому сообщаем о запуске сопрограммы только в лог. Напомню, что эти сообщения мы можем читать во время выполнения программы на вкладке Logcat в нижней части Android Studio.

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

return@async "Это типа результат работы первого потока"

но мы используем краткую форму, поскольку без оператора return результатом выполнения сопрограммы является результат последнего действия. В нашем случае это просто вышеуказанная строка текста.

После запуска первой асинхронной сопрограммы мы можем продолжить другие операции, а сопрограмма будет в фоне выполнять свою работу. Но как нам узнать, когда она завершится, и как забрать из неё результат? Для этого у нас есть переменная сопрограммы, которой мы эту сопрограмму присвоили ранее. Ожидание завершения сопрограммы и получение результата осуществляется через ожидающую функцию, которой является метод потока myThread.await().

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

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

После того, как первая сопрограмма завершится, вторая сопрограмма сама считает её результат и выведет в интерфейс.

Такая организация распараллеливания задач выглядит гораздо проще, чем работа с дополнительным классом в Java с реализацией его абстрактных методов. Если убрать вывод сообщений в лог, код будет выглядеть ещё компактнее и проще:

override fun onClick(v: View?) {
    val myThread = async(CommonPool) {                 // Запустить сопрограмму и присвоить её переменной myThread.
        sleep(2000)                                    // Типа что-то делаем длительное
        "Это типа результат работы первой сопрограммы" // Результат работы сопрограммы - результат последнего действия
    }
    launch (UI) {                                      // Запустить и забыть. 
        val myResult = myThread.await()                // Подождём результата
        textView.text = "Вторая сопрограмма завершена, результат: $myResult" // Выводим результат в интерфейс
    }
}

Более того, код выглядит, как последовательный, и не требует нагромождения лишних сущностей. Таким образом, применеение сопрограмм значительно упрощает программирование.

Помимо сопрограмм acync и launch имеются ещё один вид сопрограммы для системы Андроид. Это сопрограмма runBlocking, позволяющая дождаться выполнение первой фоновой сопрограммы в основном коде с блокированием интерфейса. Используется для написания кода в блокирующем стиле.