Продолжаем курс по обучению основам разработки мобильных приложений в Android Studio на языке Kotlin.
Это урок 7, в котором разберемся, зачем сохранять состояние активити при изменениях конфигурации и какие инструменты для этого лучше использовать: savedInstanceState или ViewModel и LiveData.
Предыдущий урок, на котором мы разбирали жизненный цикл активити, здесь.
Зачем сохранять состояние активити?
Создадим приложение. В макет экрана добавим поле для ввода текста и кнопку. Также оставим здесь текстовое поле по умолчанию, только изменим размер текста. Убедимся, что каждый элемент макета экрана имеет идентификатор. Код макета экрана:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/textView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginTop="8dp" app:layout_constraintTop_toBottomOf="@+id/editText"
app:layout_constraintHorizontal_bias="0.503" app:layout_constraintVertical_bias="0.191"
android:textSize="24sp" android:typeface="normal"/>
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="textPersonName"
android:ems="10"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="16dp"
android:layout_marginStart="16dp"
android:id="@+id/editText"/>
<Button
android:text="Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/button" app:layout_constraintStart_toEndOf="@+id/editText"
android:layout_marginStart="8dp" app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="8dp"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintTop_toTopOf="@+id/editText" app:layout_constraintBottom_toBottomOf="@+id/editText"/>
</android.support.constraint.ConstraintLayout>
В теле функции onCreate присвоим слушатель кнопке. По ее нажатию будем отправлять набранный текст из поля editText в поле textView.
button.setOnClickListener {
textView.text = editText.text
}
Зачем нам нужно сохранять состояние? Для ответа на этот вопрос запустим приложение сейчас. Напишем какой-то текст в editText и нажмем кнопку. Текст передался в текстовое поле, все ок. Но стоит нам повернуть устройство, как текст исчезает. Это происходит потому, что активити пересоздается, соответственно, пересоздаются все экранные компоненты. Но в editText текст сохраняется, если поле имеет идентификатор, а в textView ничего не сохраняется.
Активити уничтожается и создается заново не только при повороте устройства, а и при других изменениях конфигурации, таких как смена локали, изменение размера экрана, переход в многооконный режим или даже подключение физической клавиатуры. Во всех случаях данные текстового поля будут потеряны.
Функция onSaveInstanceState()
Для сохранения текста в textView можно воспользоваться объектом savedInstanceState, который приходит в качестве параметра в функцию onCreate(). Объект savedInstanceState имеет тип Bundle, который представляет собой набор пар “ключ – значение” и может быть использован для сохранения предыдущего состояния активити. Для сохранения данных в объект savedInstanceState используется функция onSaveInstanceState().
Переопределим функцию onSaveInstanceState() с набором данных Bundle. В теле функции run объекта бандла вызываем функцию putString для создания элемента коллекции с ключом “KEY” и текстом из editText в качестве значения.
override fun onSaveInstanceState(outState: Bundle?) {
outState?.run {
putString("KEY", textView.text.toString())
}
super.onSaveInstanceState(outState)
}
Также переопределим функцию onRestoreInstanceState, которая вызывается при старте активити в том случае, если имеется бандл, ранее сохраненный функцией onSaveInstanceState. В теле функции onRestoreInstanceState мы можем получить из бандла и передать в текстовое поле ранее сохраненный текст.
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
super.onRestoreInstanceState(savedInstanceState)
textView.text = savedInstanceState?.getString("KEY")
}
Теперь запустите приложение и убедитесь, что при смене конфигурации активити текст, переданный в TextView, сохраняется.
ViewModel
Но что если нам нужно сохранить не одно значение, а целый список, загруженный из сети или базы данных? При смене конфигурации устройства данные будут уничтожены и их придется загружать заново.
Архитектурные компоненты из набора Android Jetpack предоставляют вспомогательный класс ViewModel для контроллера, который отвечает за подготовку данных для пользовательского интерфейса. Объекты ViewModel автоматически сохраняются во время изменений конфигурации, так что содержащиеся в них данные сразу же становятся доступны для следующего экземпляра активити или фрагмента.
Чтобы более тесно на практике познакомиться с чистой архитектурой и архитектурными компонентами, записывайтесь на продвинутый курс по разработке приложения «Чат-мессенжер»
На схеме ниже видно, как ViewModel взаимодействует с жизненным циклом активити:
При пересоздании активити ViewModel остается живым и используется во вновь созданном активити.
Например, если вам нужно отобразить список пользователей в вашем приложении, нужно реализовать получение и сохранение списка пользователей не в коде активити или фрагмента, а во ViewModel.
Приложение с ViewModel
Рассмотрим простой пример приложения, которое использует ViewModel.
За основу был взят этот пример на Github.
В файле сборки build.gradle модуля app добавьте такие зависимости для работы со списком и ViewModel:
dependencies {
...
implementation 'com.android.support:recyclerview-v7:28.0.0'
implementation "android.arch.lifecycle:extensions:1.1.1"
}
Поскольку приложение будет работать со списком пользователей, нам понадобится модель, сущность:
data class User (
var name: String = "",
var description: String = ""
)
Здесь два поля – имя и описание.
Далее создадим файл object, выполняющий роль поставщика данных:
object UserData {
fun getUsers() = listOf(
User("Marilyn Monroe", "American actress, singer, model"),
User("Abraham Lincoln", "US President during American civil war"),
User("Mother Teresa", "Macedonian Catholic missionary nun"),
User("John F. Kennedy ", "US President 1961 – 1963")
)
fun getAnotherUsers() = listOf(
User("Martin Luther King", "American civil rights campaigner"),
User("Nelson Mandela", "South African President anti-apartheid campaigner"),
User("Queen Elizabeth", "British monarch since 1954"),
User("Winston Churchill", "British Prime Minister during WWII"),
User("Donald Trump", "Businessman, US President."),
User("Bill Gates", "American businessman, founder of Microsoft"),
User("Muhammad Ali", "American Boxer and civil rights campaigner"),
User("Mahatma Gandhi", "Leader of Indian independence movement"),
User("Margaret Thatcher", "British Prime Minister 1979 – 1990"),
User("Christopher Columbus", "Italian explorer"),
User("Charles Darwin", "British scientist, theory of evolution"),
User("Elvis Presley", "American musician"),
User("Albert Einstein", "German scientist, theory of relativity"),
User("Paul McCartney", "British musician, member of Beatles")
)
}
В реальном приложении данные поставляются из сети или БД, здесь же для простоты просто создаются два статичных списка пользователей. Первый список, который поменьше, будем отображать при старте приложения. Второй список будем отображать по нажатию кнопки в меню главного экрана.
Для отображения списка нам нужно создать файл макета элемента списка user_item.xml в папке ресурсов res/layout:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:orientation="vertical">
<TextView
android:id="@+id/userName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
tools:text="Nelson Mandela" />
<TextView
android:id="@+id/userDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/userName"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
tools:text="South African President anti-apartheid campaigner" />
</RelativeLayout>
Изменим файл макета activity_main.xml для размещения списка на главном экране, добавив виджет списка RecyclerView:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/userList"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/user_item"
tools:itemCount="12"/>
</FrameLayout>
Также нужно создать меню, для этого в папке res создадим папку menu и в ней файл main_menu.xml:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/refresh"
android:title="Refresh"
app:showAsAction="withText" />
</menu>
Это меню с одним пунктом Refresh, по нажатию которого будем обновлять список.
Теперь адаптер, который будет создавать список и наполнять его данными:
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import kotlinx.android.synthetic.main.user_item.view.*
import java.util.ArrayList
class UserAdapter : RecyclerView.Adapter<UserAdapter.UserHolder>() {
private var users: List<User> = ArrayList()
//создает ViewHolder и инициализирует views для списка
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserHolder {
return UserHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.user_item, parent, false)
)
}
//связывает views с содержимым
override fun onBindViewHolder(viewHolder: UserHolder, position: Int) {
viewHolder.bind(users[position])
}
override fun getItemCount() = users.size
//передаем данные и оповещаем адаптер о необходимости обновления списка
fun refreshUsers(users: List<User>) {
this.users = users
notifyDataSetChanged()
}
//внутренний класс ViewHolder описывает элементы представления списка и привязку их к RecyclerView
class UserHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(user: User) = with(itemView) {
userName.text = user.name
userDescription.text = user.description
}
}
}
Унаследуем наш адаптер от RecyclerView.Adapter и указываем наш собственный ViewHolder, который предоставит доступ к View-компонентам. Далее инициализируем список. Функция onCreateViewHolder создает ViewHolder и инициализирует View-компоненты для списка. Функция onBindViewHolder связывает View-компоненты с содержимым. В функции refreshUsers передаем данные и оповещаем адаптер о необходимости обновления списка вызовом notifyDataSetChanged(). Внутренний класс ViewHolder описывает View-компоненты списка и привязку их к RecyclerView.
Теперь мы подходим к самому главному – получению данных для списка.
Этим будет заниматься класс UserViewModel, унаследованный от ViewModel:
import android.arch.lifecycle.MutableLiveData
import android.arch.lifecycle.ViewModel
class UserViewModel : ViewModel() {
var userList : MutableLiveData<List<User>> = MutableLiveData()
//инициализируем список и заполняем его данными пользователей
init {
userList.value = UserData.getUsers()
}
fun getListUsers() = userList
//для обновления списка передаем второй список пользователей
fun updateListUsers() {
userList.value = UserData.getAnotherUsers()
}
}
Для списка пользователей используется объект класса MutableLiveData – это подкласс LiveData, который является частью Архитектурных компонентов, и следует паттерну Observer (наблюдатель). Если вы знакомы с RxJava, класс LiveData похож на Observable. Но если с Observable вы должны удалять связи вручную, то класс LiveData зависит от жизненного цикла и выполняет всю очистку самостоятельно. Подписчиками LiveData являются активити и фрагменты. LiveData принимает подписчика и уведомляет его об изменениях данных, только когда он находится в состоянии STARTED или RESUMED. Состояние подписчиков определяется их объектом LifeCycle. Более подробно LifeCycle и состояния жизненного цикла мы рассматривали на прошлом уроке.
Класс MutableLiveData предоставляет методы setValue и postValue (второй – поточно-безопасный), посредством которых можно получить и отправить данные любым активным подписчикам.
В классе UserViewModel мы инициализируем список и заполняем его данными пользователей. Функция getListUsers() возвращает список, а функция updateListUsers() обновляет список, сохраняя в него второй список пользователей из класса UserData.
Теперь код MainActivity:
import android.arch.lifecycle.Observer
import android.arch.lifecycle.ViewModelProviders
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.view.Menu
import android.view.MenuItem
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
//инициализируем ViewModel ленивым способом
private val userViewModel by lazy {ViewModelProviders.of(this).get(UserViewModel::class.java)}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//инициализируем адаптер и присваиваем его списку
val adapter = UserAdapter()
userList.layoutManager = LinearLayoutManager(this)
userList.adapter = adapter
//подписываем адаптер на изменения списка
userViewModel.getListUsers().observe(this, Observer {
it?.let {
adapter.refreshUsers(it)
}
})
}
//создаем меню
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.main_menu, menu)
return super.onCreateOptionsMenu(menu)
}
//при нажатии пункта меню Refresh обновляем список
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when(item?.itemId) {
R.id.refresh -> {
userViewModel.updateListUsers()
}
}
return super.onOptionsItemSelected(item)
}
}
Инициализируем объект класса UserViewModel так называемым ленивым способом с помощью функции lazy(). Это функция, которая принимает лямбду и возвращает экземпляр класса Lazy<T>, который служит делегатом для реализации ленивого свойства: первый вызов get() запускает лямбда-выражение, переданное lazy() в качестве аргумента, и запоминает полученное значение, а последующие вызовы просто возвращают вычисленное значение. Таким образом, объект UserViewModel инициализируется только при первом вызове, а далее используется уже инициализированный объект.
В теле onCreate() инициализируем адаптер и присваиваем его списку. Далее подписываем адаптер на изменения списка с помощью функции observe(@NonNull LifecycleOwner owner, @NonNull Observer<T> observer), которой на вход передается объект LifecycleOwner (текущее активити) и интерфейс Observer – колбек, уведомляющий об успешном получении данных. При этом вызывается метод обновления списка адаптера и ему передается обновленный список.
Ниже создаем меню и обрабатываем нажатие пункта меню Refresh, по которому обновляем список.
Запуск приложения
Теперь запустим приложение на эмуляторе и проверим его работу.
После запуска открывается экран со списком пользователей. Обновим список из меню. Теперь отображается другой, расширенный список. Но если мы перезапустим приложение, то снова увидим первоначальный список, который открывается по умолчанию.
Снова обновим список. Теперь покрутим устройство. Как мы знаем, при повороте активити уничтожается, однако на экране все еще отображается второй список. Это значит, что, несмотря на уничтожение активити, список сохраняется в объекте ViewModel и новое активити использует его данные. С другой стороны, если данные в списке будут обновлены, то посредством LiveData список также будет обновлен.
Дополнительно о LiveData можно почитать здесь.
Исходный код приложения можно посмотреть здесь.
Kotlin Android Extensions
Внимание! Kotlin Android Extensions теперь deprecated, это значит, что его поддержка не гарантируется. Альтернативные методы описаны здесь: ссылка
Если вы заметили, мы обращаемся к экранным компонентам без вызова метода findViewById, прямо по идентификатору. Это происходит благодаря использованию плагина Kotlin Android Extensions — это плагин для Kotlin, который включён в стандартный пакет. Он позволяет восстанавливать view из Activities, Fragments, и Views таким вот простым способом.
Плагин генерирует дополнительный код, который позволяет получить доступ к view в виде XML, так же, как если бы вы имели дело с properties с именем id, который вы использовали при определении структуры.
Также он создаёт локальный кэш view. При первом использовании свойства, плагин выполнит стандартный findViewById. В последующем, view будет восстановлен из кэша, поэтому доступ к нему будет быстрее.
По умолчанию плагин уже интегрирован в модуль благодаря вот такой строчке в файле сборки модуля:
apply plugin: 'kotlin-android-extensions'
При первом обращении к любому экранному компоненту в MainActivity автоматически добавляется такой импорт:
import kotlinx.android.synthetic.main.activity_main.*
Больше о Kotlin Android Extensions рекомендую почитать в переводе статьи Antonio Leiva на Медиуме.
На этом наш урок подошел к концу. Вопросы задавайте в комментариях. Всем добра!
Урок 8. Android Data Binding – основы
Здравствуйте. Использую Android Studio Dolphin | 2021.3.1 Patch 1
Build #AI-213.7172.25.2113.9123335, built on September 30, 2022
Runtime version: 11.0.13+0-b1751.21-8125866 amd64
VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o.
Windows 10 10.0
GC: G1 Young Generation, G1 Old Generation
Memory: 1280M
Cores: 8
Registry:
external.system.auto.import.disabled=true
ide.text.editor.with.preview.show.floating.toolbar=false
дошел до
В файле сборки build.gradle модуля app добавьте такие зависимости для работы со списком и ViewModel:
dependencies {
…
implementation ‘com.android.support:recyclerview-v7:28.0.0’
implementation “android.arch.lifecycle:extensions:1.1.1”
}
добавил implementation(“androidx.recyclerview:recyclerview:1.3.2”) вместо первой строки. компилится.
на второй строке затык:
Duplicate class android.support.v4.os.ResultReceiver found in modules core-1.12.0-runtime (androidx.core:core:1.12.0) and support-compat-26.1.0-runtime (com.android.support:support-compat:26.1.0)
Duplicate class android.support.v4.os.ResultReceiver$1 found in modules core-1.12.0-runtime (androidx.core:core:1.12.0) and support-compat-26.1.0-runtime (com.android.support:support-compat:26.1.0)
Duplicate class android.support.v4.os.ResultReceiver$MyResultReceiver found in modules core-1.12.0-runtime (androidx.core:core:1.12.0) and support-compat-26.1.0-runtime (com.android.support:support-compat:26.1.0)
Duplicate class android.support.v4.os.ResultReceiver$MyRunnable found in modules core-1.12.0-runtime (androidx.core:core:1.12.0) and support-compat-26.1.0-runtime (com.android.support:support-compat:26.1.0)
что делать?
Здравствуйте. У вас дублируются классы из разных библиотек: androidx и support-compat. Библиотека support-compat используется для поддержки старых версий, проект в уроке собран на ней. В новых проектах используется библиотека androidx. Вы можете использовать старую библиотеку или перевести проект на androidx через меню Android Studio.
Рабочая версия. март 2022
https://github.com/suckbaby/ViewModelFanDroid
Здравствуйте, пытаюсь актуализировать код. Перепроверил все, вроде все логично, но не могу найти ошибку. Может кто подскажет что переделать?!
https://github.com/VitalmagO/SandBox_AndroidKotlin/tree/master
Commit: “Example ViewModel Lists (Adapter-Holder)”
“Зачем нам нужно сохранять состояние? Для ответа на этот вопрос запустим приложение сейчас. Напишем какой-то текст в editText и нажмем кнопку. Текст передался в текстовое поле, все ок. Но стоит нам повернуть устройство, как текст исчезает. Это происходит потому, что активити пересоздается, соответственно, пересоздаются все экранные компоненты. Но в editText текст сохраняется, если поле имеет идентификатор, а в textView ничего не сохраняется.”
Ничего не исчезает, когда переворачивается.
package com.example.for_android_experiments
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
textView.text = editText.text
}
}
}
В общем, наглядный пример того, как автору надоело писать статьи, и он решил делать их на отвали. Половину статьи ещё держался, а потом, – всё.
Запутанный кусок кода, после которого идёт не менее запутанный кусок текста: “Унаследуем наш адаптер от RecyclerView.Adapter и указываем наш собственный ViewHolder, который предоставит доступ к View-компонентам. Далее инициализируем список. Функция onCreateViewHolder создает ViewHolder и инициализирует View-компоненты для списка. Функция onBindViewHolder связывает View-компоненты с содержимым. В функции refreshUsers передаем данные и оповещаем адаптер о необходимости обновления списка вызовом notifyDataSetChanged(). Внутренний класс ViewHolder описывает View-компоненты списка и привязку их к RecyclerView.”
Как вы думаете, сколько людей хоть что-нибудь поймут из этого куска текста? Ни об одном из этих компонентов в предыдущих уроках речь не шла, нигде не написано их описание, и для чего они используются, ни слова о том, что такое Adapter или ViewHolder. В общем, кто-то явно куда-то очень спешил. Не надо так.
Вот наглядный пример того, что читателю надоело читать статьи и учиться, и он решил делать это на отвали. Половину статьи ещё держался, а потом, — всё. Устал, наверное. Ему и код запутанный, и слова непонятные. А нужно было отдохнуть и перечитать снова о том, что адаптер, это компонент, “…который будет создавать список и наполнять его данными…”. А ViewHolder “…предоставит доступ к View-компонентам…”, “…описывает View-компоненты списка и привязку их к RecyclerView…”. А если еще заглянуть в документацию, или посмотреть наши видеоуроки здесь и на канале Start Android, где про адаптеры и вью-холдеры разжевано до оскомины… Как вы думаете, сколько людей хоть что-нибудь поняли из этого куска текста? Отвечу: 6721 человек. А 6722-й не понял, и ему было легче написать длиннопост критики и гнева, чем прочитать еще раз и разобраться в теме. Не надо так.
Есть в Ваших словах доля правды. RecuclerView – тема отдельного урока, а работа с ViewModel – вообще отдельный курс! Возможно, есть смысл всё-таки поискать информацию из других источников и позже вернуться сюда снова.
Виталий, что означает запись “UserViewModel::class.java” в строчке: “private val userViewModel by lazy {ViewModelProviders.of(this).get(UserViewModel::class.java)}” ? Почему нельзя написать проще: “private val userViewModel by lazy {UserViewModel}” ? Почему приходиться действовать через класс “ViewModelProviders”?
особенности синтаксиса
Виталий, за что отвечает параметр app:showAsAction=”withText” в файле main_menu.xml ? Если этот параметр удалить, то приложение всё равно запускается, может он не нужен?
Он отвечает за способ отображения пункта меню. Подробности ищите в документации и в уроках на нашем канале
Виталий, во фрагменте кода:
//инициализируем адаптер и присваиваем его списку
val adapter = UserAdapter() // получаем экземпляр класса “UserAdapter”
userList.layoutManager = LinearLayoutManager(this) // откуда взялся объект “userList” ?
userList.adapter = adapter
Где создаётся объект “userList” и каково его назначение?
userList – идентификатор виджета списка RecyclerView в макете activity_main.xml. Мы можем обращаться к нему напрямую, благодаря библиотеке Kotlin Android Extensions, о ней написано в уроке.
Виталий, можете подробно объяснить что происходит в строчке:
class UserAdapter : RecyclerView.Adapter() {
не проще бы было написать так:
class UserAdapter : RecyclerView.Adapter{
Непонятно для чего нужна конструкция: (), можете объяснить её назначение?
Без «()» не скомпилируется, так как RecyclerView.Adapter класс, а не интерфейс
Для тех, кто как и я страдает после июля 2019-го с неработающим кодом.
В первом участке кода замените зависимость RecyclerView на “androidx.recyclerview:recyclerview:1.1.0-beta02”.
Так же проследите, чтобы библиотека appcompat была версии 1.1.0 и выше, у меня сейчас “androidx.appcompat:appcompat:1.1.0-rc01”
Открывающий тэг RecyclerView в activity_main.xml меняем на новый androidx.recyclerview.widget.RecyclerView, то же касается соответствующих импортов в адаптере и MainActivity, в котором дополнительно нужно поменять импорт LinearLayoutManager на androidx.recyclerview.widget.LinearLayoutManager
Для тех кто страдает в феврале 2021 в уроках о КОТЛИН, и ПРОДВИНУТЫХ КУРСАХ.
Автор создал гениальные уроки по Котлин где то в 2018 году. Но то, что работает в 2018 теперь не работает (для новичков) в 2021г. Изменились андроид технологии. Об этом частично правильно отметил в своей заметке Freaketools. Нужно все исправлять. Особенно build.gradle и модель и прожект уровни. Естественно у автора огромная загруженность. Я конечно понимаю, что котлин уроки это не тоже самое что поддержка правильной работы СИСТЕМы в андроид проекте. Но автор пишет, что не обладая навыками программирования можно легко освоить уроки на котлин. ЭТО НЕ ТАК. НОВИЧКАМ В программировании. Не напрягайтесь повторять эти уроки. У вас ничего не получится, если только вы не изучили систему GRADLE, и систему андроид в целом. Будут только сплошные разочарования. Пока автор сам не перейдет на котлин 1.4.Х. и android studio 4.x.x и не сделает рекомендации по всем урокам по КОТЛИН, по всем программам ПРОДВИНУТЫХ КУРСОВ – выполнять уроки БЕСПОЛЕЗНО. Только для общего образования, что вот так котлин работал в 2017-2019гг. Поправьте меня если я не ПРАВ. С автора новые уроки на современном уровне, а нам новые знания от автора.
Отчасти вы правы. В программировании все меняется ежедневно. Но все изучить невозможно. Я, например, тоже не знаю Gradle досконально. И не успеваю следить за всеми изменениями.
Надо просто принять тот факт, что изменения постоянны, и жить с этим.
В случае проблем нужно вникать в то, что происходит и внимательно читать сообщения среды разработки. Абсолютно все проблемы можно решить элементарным поиском в интернете, поскольку нет ничего уникального, тем более у новичков.