По давней традиции вместе с новой версией Android выходит обновление Support Library. Пока библиотека вышла в стадии альфа, но список изменений уже намного интереснее, чем такой же список у Android P. Google несправедливо мало рассказал и написал об основных нововведениях главной библиотеки для Android. Приходится читать исходники и разбираться, в чем особенности новых фич и зачем они нужны. Восстановлю справедливость и расскажу, чем нас порадовал Google:

  • RecyclerView selection — выбор элементов теперь из коробки;
  • Slices — новый способ отображать контент другого приложения;
  • новые элементы дизайна: BottomAppBar, ChipGroup и другие;
  • мелкие изменения одной строкой.

RecyclerView selection


В 2014 году, вместе с релизом Lollipop, Google добавила в support новый элемент — RecyclerView, как замену устаревшему ListView. Все было хорошо с ним, да не хватало одного метода из ListView — setSelectionMode(). Спустя 4 года этот метод косвенно был реализован в RecyclerView в виде целой библиотеки.

Что же волшебного в selection? Selection mode — режим, которой инициализируется долгим нажатием по элементу списка. Далее можем выбрать несколько других элементов и сделать общее действие на ними. Пример: в Google Photos selection mode значительно облегчает жизнь.



Давайте разберемся на практике, как обстоит дело в support.

Добавим в gradle зависимости. Интересно, что Google выделила selection в отдельный репозиторий.

dependencies {
   implementation "com.android.support:recyclerview-selection:28.0.0-alpha1"
}

Напишем стандартный адаптер для RecyclerView.

class WordAdapter(private val items: List<Word>) : RecyclerView.Adapter<WordViewHolder>() {

   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = WordViewHolder(
           LayoutInflater
                   .from(parent.context)
                   .inflate(R.layout.item_word, parent, false)
   )

   override fun getItemCount() = items.size

   override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
       val item = items[position]
       holder.bind(item)
   }

   class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

       private val text: TextView = itemView.findViewById(R.id.item_word_text)

       fun changeText(word: Word) {
           text.text = word.text
       }

   }

}

Модель Word используем в качестве данных.

@Parcelize
data class Word(val id: Int, val text: String) : Parcelable

Фундамент есть, приступим к реализации выбора. Сперва нужно определиться, что будет идентифицировать элемент списка. Google предлагает на выбор три варианта: Long, String, Parcelable. Для этой цели у нас уже сформирован Word, не хватает только реализации Parcelable. Реализацию добавим аннотацией @Parcelize, которая доступная в экспериментальной версии Kotlin. В Android Studio 3.2 пока есть проблемы со сборкой проекта с экспериментальным Kotlin, но никто не отменял студийные шаблоны.

SelectionTracker — главный класс библиотеки. Объект этого класса обладает информацией про выбранные пользователем элементы и позволяет из кода изменять этот список. Чтобы инициализировать данный класс, понадобятся реализации двух абстрактных классов: ItemKeyProvider и ItemDetailsLookup. Первый нужен для двусторонней связи позиции элемента в коллекции и ключа.

// В конструкторе ItemKeyProvider мы выбираем метод предоставления доступа к данным:
//  SCOPE_MAPPED - ко всем данным. Позволяет реализовать функционал, требующий наличие всех элементов в памяти
//  SCOPE_CACHED - к данным, которые были недавно или сейчас на экране. Экономит память
class WordKeyProvider(private val items: List<Word>) : ItemKeyProvider<Word>(ItemKeyProvider.SCOPE_CACHED) {
   override fun getKey(position: Int) = items.getOrNull(position)
   override fun getPosition(key: Word) = items.indexOf(key)
}

ItemDetailsLookup нужен для получения позиции элемента и его ключа по координатам x и y.

class WordLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup<Word>() {

   override fun getItemDetails(e: MotionEvent) = recyclerView.findChildViewUnder(e.x, e.y)
           ?.let {
               (recyclerView.getChildViewHolder(it) as? ViewHolderWithDetails<Word>)?.getItemDetail()
           }

}

Напишем также интерфейс для получение данных из ViewHolder и реализуем его.

interface ViewHolderWithDetails<TItem> {

   fun getItemDetail(): ItemDetails<TItem>

}

class WordDetails(private val adapterPosition: Int, private val selectedKey: Word?) : ItemDetails<Word>() {

   override fun getSelectionKey() = selectedKey

   override fun getPosition() = adapterPosition

}

inner class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), ViewHolderWithDetails<Word> {

   override fun getItemDetail() = WordDetails(adapterPosition, items.getOrNull(adapterPosition))

}

Везде стандартный код. Удивительно, почему разработчики support library не добавили классическую реализацию сами.

Сформируем трекер в Activity.

val tracker = SelectionTracker
       .Builder<Word>(
               // идентифицируем трекер в контексте
               "someId",
               recyclerView,
               // для Long ItemKeyProvider реализован в виде StableIdKeyProvider
               WordKeyProvider(items),
               WordLookup(recyclerView),
               // существуют аналогичные реализации для Long и String
               StorageStrategy.createParcelableStorage(Word::class.java)
       ).build()

Поправим ViewHolder, добавим реакцию на изменение состояния выбора.

fun setActivatedState(isActivated: Boolean) {
   itemView.isActivated = isActivated
}

Добавим трекер в адаптер, переопределим onBindViewHolder с payload. Если изменения касаются только состояния выбора, то в payloads будет находиться константа SelectionTracker.SELECTION_CHANGED_MARKER.

override fun onBindViewHolder(holder: WordViewHolder, position: Int, payloads: List<Any>) {
   holder.setActivatedState(tracker.isSelected(items[position]))

   if (SelectionTracker.SELECTION_CHANGED_MARKER !in payloads) {
       holder.changeText(items[position])
   }

}

Tracker готов и работает, как часы. Добавим немного красоты и смысла. Пусть AppBar меняет цвет, заголовок начнет отображать количество выбранных элементов и добавляется кнопка Clear в меню, когда пользователь что-нибудь выбирает. Для этого есть ActionMode и поддержка его в AppCombatActivity.

Первым делом напишем реализацию ActionMode.Callback.

class ActionModeController(
       private val tracker: SelectionTracker<*>
) : ActionMode.Callback {

   override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
       mode.menuInflater.inflate(R.menu.action_menu, menu)
       return true
   }

   override fun onDestroyActionMode(mode: ActionMode) {
       tracker.clearSelection()
   }

   override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean = true

   override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean = when (item.itemId) {
       R.id.action_clear -> {
           mode.finish()
           true
       }
       else -> false
   }

}

Добавим observer к SelectionTracker и свяжем изменения в трекере с ActionMode в Activity.

tracker.addObserver(object : SelectionTracker.SelectionObserver<Any>() {
           override fun onSelectionChanged() {
               super.onSelectionChanged()
               if (tracker.hasSelection() && actionMode == null) {
                   actionMode = startSupportActionMode(ActionModeController(tracker))
                   setSelectedTitle(tracker.selection.size())
               } else if (!tracker.hasSelection()) {
                   actionMode?.finish()
                   actionMode = null
               } else {
                   setSelectedTitle(tracker.selection.size())
               }
           }
       })
   }

   private fun setSelectedTitle(selected: Int) {
       actionMode?.title = "Selected: $selected"
   }

Теперь точно все. Наслаждаемся простотой и красотой.



Мы сделали стандартный вариант. Кратко отмечу, что в Builder много методов для кастомизации процесса выбора. Например, с помощью метода withSelectionPredicate(predicate: SelectionPredicate) можно ограничить количество выбранных элементов или запретить выбор особых элементов. Также в Builder предусмотрены методы по добавлению поведения, которое может конфликтовать с selection при традиционном способе добавления. Например, при помощи withOnDragInitiatedListener(listener: OnDragInitiatedListener) можно настроить Drag&Drop.

Slices


Самой странной новинкой оказался Slice. Google посвятила очень мало времени объяснениям сообществу, что это за диковина. Есть только код и документации к половине классов. Давайте разбираться.

За основу возьму код отсюда, потому что они придумали, как обходить баги с Permission в Android P DP1. Хочу отметить, что Slices не является новинкой support library. Фича появилась в Android SDK 28, а в support ареал обитания расширен до 24 версии SDK. На этом можно завершить рассказ и продолжить его через несколько лет. Пока minSdkVersion может быть максимум 19, поговорим в общем об идее этой технологии и о том, зачем она вообще нужна.

Slices — библиотека, которая позволит запрашивать из одного приложения (клиент или хост) часть или статичный кусочек другого приложения (отправитель или провайдер). Очень похоже на описание RemoteViews, которое часто используется для программирования кастомных виджетов и уведомлений.

Slice — это данные в каркасе без дизайна и интерактивности, как HTML без CSS и Js. Дизайн будет подстраиваться под тему приложения-хоста. Пример слайса.

Отправитель — это ContentProvider, которому нужно реализовать простой метод onBindSlice(sliceUri: Uri): Slice и внутри метода сформировать Slice. У нас провайдер будет отсылать время и количество вызовов.

class SliceContentProvider : SliceProvider() {

   private var counter = 0
   override fun onBindSlice(sliceUri: Uri): Slice {
       return when (sliceUri.path) {
           "/time" -> createTimeSlice(sliceUri)
           else -> throw IllegalArgumentException("Bad url")
       }
   }

   override fun onCreateSliceProvider(): Boolean {
       Toast.makeText(context, "Slice content provider is launched", Toast.LENGTH_LONG).show()
       return true
   }

   private fun createTimeSlice(sliceUri: Uri): Slice = ListBuilder(context, sliceUri)
           .apply {
               counter++
               setHeader(
                       ListBuilder.HeaderBuilder(this)
                               .setTitle("What's the time now?")
               )
               addRow(
                       ListBuilder.RowBuilder(this)
                               .setTitle("It is ${SimpleDateFormat("HH:mm").format(Calendar.getInstance().time)}")
               )
               addRow(
                       ListBuilder.RowBuilder(this)
                               .setTitle("Slice has called $counter times")
               )
           }
           .build()

}

Клиенту нужно сделать запрос по URI к провайдеру, запросить через него slice, получить и передать его в SliceView. Все действия производятся через SliceManager. Важно не забыть про permission.

private val baseSliceUri: Uri = Uri.parse("content://ru.touchin.provider/")
   private val timeSliceUri = baseSliceUri.buildUpon().appendPath("time").build()

   private lateinit var sliceManager: SliceManager

   override fun onCreate(savedInstanceState: Bundle?) {
       // стандартные процедуры инициализации View
       sliceManager = SliceManager.getInstance(this)

       findViewById<View>(R.id.get_slice).setOnClickListener {
           tryShowingSlice(timeSliceUri)
       }
   }

   override fun onStart() {
       super.onStart()
       if (providerAppNotInstalled(packageManager, baseSliceUri.authority)) {
           showMissingProviderDialog(this, { finish() }, baseSliceUri)
           return
       }

   }

   private fun tryShowingSlice(sliceUri: Uri) {
       if (sliceManager.missingPermission(sliceUri, appName = getString(R.string.app_name))) {
           // запрашиваем permission сложным образом из-за Android P DP1
           }
       } else {
           getSliceAndBind(sliceUri)
       }
   }

   private fun getSliceAndBind(sliceUri: Uri) {
       sliceView.setSlice(sliceManager.bindSlice(sliceUri))
   }

SliceManager предоставляет возможность подписаться с помощью SliceLiveData на изменения Slice в провайдере и внутри подписки обновлять SliceView. К сожалению, оно сейчас не работает. Мы использовали менее реактивный вариант.

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



В большинстве случаев RemoteView используется для виджетов и уведомлений. Slices плохо подходят под эти цели, они мало кастомизируемые и, как я уже писал, подстраиваются под дизайн приложения. Идеально подходят под приложения, которые используют данные других приложений. Под категорию всеобъемлющих подходят голосовые ассистенты — Google Assistant, Алиса и так далее. Как было замечено в блоге компании Novada, с помощью конструктора slice можно собирать слайсы, очень похожие на ответы для Google Assistant.



И тут самое время для теории.

Возьмем за основу то, что Slice сделан для программирования ответов в Google Assistant — стратегически важный продукт для компании. Очевидно, что мы живем во времена, когда графический интерфейс постепенно вытесняется голосовым: растет популярность домашних ассистентов и есть прогресс в разработке голосового искусственного интеллекта посредством ИИ, нейронный сетей и других хайповых технологий.

Для Google самым логичным вариантом было бы развивать и наращивать Google Assistant, чтобы за год-два он стал мощным инструментом. Slice — теоретически отличный инструмент для накачки дополнениями от сторонних разработчиков. Так ассистент станет мощнее, все действия можно проводить через него и отпадет надобность в рабочих столах и иконках. Тогда Google Assistant станет основой для Android.

На данный момент нам ничего не рассказали толком про Slice: ни целей, ни преимуществ над RemoteView. Хотя по количеству кода в новой версии support Slice занимает чуть ли не первое место. Поэтому я думаю, на ближайшей I/O нам будут подробно рассказывать про Slice. И возможно расскажут о планах эволюции ОС или даже представят версию Android с голосовым интерфейсом для разработчиков.

Но все это спекуляция и желание автора раскрыть теорию заговора и добраться к истине. Единственное, что можно сказать на сто процентов, на Google I/O нас ждет развязка истории.

Новые элементы:


MaterialCardView и MaterialButton


MaterialCardView наследуется от CardView и практически ничем не отличается от него. Добавлена только возможность задавать границы карточки и в качестве background используется другой drawable. Найдите 10 отличий.
MaterialButton является наследником AppCombatButton и тут различия заметны. Разработчики сюда добавили больше способов кастомизировать кнопку: цвет ripple эффекта, разные радиусы кнопок, границы, как у MaterialCardView.



Chip и ChipGroup


Тут и слова лишние.



BottomAppBar


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

Меню на BottomAppBar нужно добавлять искусственно, для этого есть метод replaceMenu(@MenuRes int newMenu).

Дизайнеры классно придумали, как сочетать FloatingActionButton и BottomAppBar. Но без кнопки BottomAppBar смотрится лишним. Убирается вырез, остается подбородок с кнопками меню с одной стороны. Проблему с меню на больших экранах можно было бы решить интересней, например по длинному нажатию на FloatingActionButton трансформировать ее в меню внизу экрана.



Список коротких нововведений:


  • Android KTX, который анонсировали ранее. Куча open-source extension-ов на Kotlin. Очень полезно.
  • HEIF Writer. Новый формат кодирования одного или последовательности изображений дошел до Android через год после анонса на ios. Здесь не идёт речь о полной замене форматов, как у Apple. Просто библиотечка с конвертацией.
  • Browser Actions — протокол для кастомизации контекстного меню браузера под определенный url. Кастомизация ограничивается добавлением нескольких MenuItem-ов со своими иконкой, текстом и Intent-ом. Протокол подразумевает реализацию логики также со стороны браузера. В Chrome пока не реализовано.

Для тех, кто хочет поковыряться:


  1. Используйте Android studio 3.1 и выше. Эти версии пока не в релизе, но работают стабильно, я работал с 3.2.
  2. Немного пошаманить в build.gradle с версиями. Ну и, естественно, нужно добавить нужные зависимости.

    android {
       compileSdkVersion 'android-P'
       defaultConfig {
           targetSdkVersion 'P' // или 28
       }
    }
  3. Пока код, который использовал support 28, запускался только на эмуляторе с Android P. Все, что старее, ругалось и выдавало кучу ошибок при попытке запуска.

Список новых фич не окончательный. Если анализировать changelog библиотеки за предыдущие 2-3 года и экстраполировать данные на этот год, то в мае нас ожидает ещё много-много интересного. Ждём.

Полезные ссылки:


Комментарии (17)


  1. j-ker
    24.03.2018 16:56

    "Удивительно, почему разработчики support library не добавили классическую реализацию сами." — потому что они от этих ваших котлинов сами в шоке… Уверен, что и статья эта заказная от котлино-девелов с целью самопиара писана и захабрена. Да!111


    1. Bringoff
      24.03.2018 20:38

      Чё?


    1. maxbach Автор
      24.03.2018 21:12

      Мне очень льстит ваш комментарий. Надеюсь, Jetbrains мне уже перевели гонорар.


    1. Necessitudo
      24.03.2018 21:14

      Да ладно вам, Котлин и правда шагает семимильными шагами как Свифт. Без него уже тяжко, а с ним пусть будет.


    1. AlexeyGorovoy
      24.03.2018 21:48

      отому что они от этих ваших котлинов сами в шоке…


      Настолько в шоке, что примеры кода в своих видео и на сайте они уже стали на котлине приводить.


      1. Kalobok
        25.03.2018 05:07

        Примеры кода в видео? Больше можно ничего не говорить.


    1. eskander_service
      25.03.2018 14:58

      Поддерживаю, уже отовсюду прёт) и главное все думают что, в целом нужно с него начинать учится программированию. Зачем тогда Scala ?)) Почему те кто придерживается java уже считаются не программистами всего лишь потому что им не нужен этот котлин. И наверное ему (котлину), стоит хотя бы дорасти до половины возраста java, что бы таковым (языком программирования) себя преподносить. С чего такая навязчивость? Кажись гугель джетбрейн за дройдовую студию своей «поддержкой», так благодарит))


      1. vlad2711
        27.03.2018 11:09

        Почему те кто придерживается java уже считаются не программистами

        Этого никто не говорил, хотите кодить на Джаве — флаг вам в руки, хотите на котлине, можете и на котлине. Суть в том что котлин развивается на порядок быстрее чем Джава, в нем уже есть то, к чему начали переходить в Джава 10, и куча других вкусных штук, которые будут добавлены в Джаву через 1 — 2 года. В случае с андроидом это даже более чем критично, ведь в нем переход на новую версию Джавы занимает еще 1-2 года. Вспомните как давно в андроиде появилась Джава 8, это было совсем недавно(если не учитывать Jack с его закидонами), а уже вышла Джава 10, а андроид даже на девятую не перешел.

        И наверное ему (котлину), стоит хотя бы дорасти до половины возраста java, что бы таковым (языком программирования) себя преподносить

        Наверное стоит упомянуть что котлину уже 7 лет, и он явно не вчера появился как многие думают.Это конечно не 23 года как у Джавы, но все же немало.


  1. Error1024
    24.03.2018 21:49

    Наслаждаемся простотой и красотой.

    Поразительно много кода, для элементарного MultiSelect, которого при использовании голого C WinApi из 80-90х в разы меньше.
    Катимся товарищи!


    1. Zoolander
      25.03.2018 13:59

      за это мобильным разработчикам и платят больше — за муки


      1. petrovichtim
        25.03.2018 14:02

        ORLY?


        1. Zoolander
          27.03.2018 07:32
          +1

          если вы не застали инструменты быстрой разработки конца 90х — скачайте с торрентов Delphi или, если вы мобильный разработчик, Borland C++ Builder (в принципе можно брать и версию начала нулевых, не помню точно, когда он закончился). И попробуйте набросать прототип приложения — будете очень приятно удивлены.

          Работало это все в 90х без тормозов на машинах, которые хуже современных телефонов.

          Вот почему для тех, кто помнит формошлепство в 90х (даже без Builder и Delphi) — нынешние фреймворки для мобильных систем выглядят overcomplicated мутантом.

          Я работал и там, и там. Справедливости ради, Kotlin/Anko дали хороший синтаксический сахар, но API осталось прежним. Вспомните сами, если вы мобильный разработчик, сколько сторонних библиотек приходится подключать из проекта в проект — от ButterKnife до RxJava — как стандартную часть. Почему этого не сделал Google? Почему, если объявили о поддержке Kotlin, не сделали стандартной частью API те же всем известные библиотеки, хотя бы Picasso? К создателю официальных фреймворков есть много вопросов, но главный, пожалуй, выглядит так — «Почему спустя 20 лет мы получили такие чудовищные и прожорливые системы разработки, а визуальный билдер интерфейса подглюкивает и тормозит даже на современных компьютерах?»


          1. petrovichtim
            27.03.2018 08:05

            я возмущен фразой «платят больше — за муки»
            не платят им больше, чем остальным, а мук точно больше, Джим Макейт до сих пор футболки шлет и триалы для студии, жаль только, что я уже давно на линуксе живу


    1. Malligan0
      25.03.2018 14:57

      А можно немного подробнее об этом? Лучше бы разгромную статью о том, насколько WinApi лучше, особенно в контексте тёмного таинства запуска самой обычной, честной windows на ARM, демо которого показывали не так уж и давно.


    1. Zoolander
      27.03.2018 07:37

      справедливости ради, в WinApi не было такого жизненного цикла:

      PS: и по этой карусели приложение может двинуться в любой момент, с поворотом экрана или переключением на другую активность (для мобильных разработчиков — тут я понимаю не Activity, а именно другую деятельность пользователя)


      1. Error1024
        27.03.2018 10:59

        Безусловно, а принес ли этот монстр хоть какую-то пользу, кроме боли — перерождающуюся в бесчисленные баги и падения, а также, огромный порог вхождения?

        К слову аргументом может быть «меньшее потребление ОЗУ, за счет выгрузки UI», однако, у меня на телефоне 3ГБ, а более 5 приложений в памяти он не держит. И да — я знаю про лимит на приложение, но вспоминая ПК с Win98 и ОЗУ менее ГБ, с кучей запущенных полноценных программ, я хочу плакать.


  1. makaveliS
    25.03.2018 14:58

    А теперь все тоже самое, только на Java