Объект в Kotlin и паттерн «Одиночка»

Туториалы

Объект в Kotlin и паттерн «Одиночка»

Сегодня вы изучите, как использовать ключевое слово object в Kotlin для определения одиночных, сопутствующих и анонимных объектов и обеспечения взаимодействия с Java.

Работая с Kotlin, вы часто пересекаетесь с ключевым словом object. Это слово имеет несколько значений в зависимости от контекста.

В этой статье вы создадите приложение для покупок и изучите несколько способов использования object. В этом приложении пользователи могут просматривать продукты, добавлять их в корзину и удалять оттуда. Во время написания приложения вы изучите:

  • Как определить одиночные, сопутствующие и анонимные объекты
  • Объектные выражения
  • Взаимодействия с Java: как предоставить «объект» по вызову в Java
  • Лучшие практики для синглтона

Время начинать!

Итак, начнем

Загрузите стартовый проект.

Откройте свой проект в Android Studio 4.1 или выше. Выберите «Open an Existing Project», а затем выберите имя начального проекта.

Открыв стартовый проект, вы обнаружите там уже некую структуру:

  1. MainActivity.kt: Начальный экран приложения
  2. Product.kt: Модель для представления продукта в магазине
  3. ProductListAdapter.kt: ListAdapter, который и MainActivity и ShoppingCartActivity используют для наполнения RecyclerView продуктами
  4. ShoppingCartActivity.kt: Действие, которое вы будете использовать для отображения корзины покупок пользователя, общей стоимости и кнопки для очистки корзины.
  5. StringUtils.kt: Содержит удобное расширение для Int для преобразования в строку вида $12.34

Далее соберите и запустите проект. Вы увидите экран с разнообразными Android- тематическими продуктами для продажи:

Тем не менее вы пока ничего не можете купить. Продолжайте чтение, чтобы добавить возможность покупок, параллельно изучая способы использования object в Kotlin.

Использование синглотонов (одиночек) в Kotlin

Используя слово object в приложении, вы определяете синглтон (одиночку). Одиночка – это шаблон проектирования, в котором данный класс имеет только один экземпляр внутри всего приложения.

Одиночка чаще всего применяется:

  1. Для обмена данными между двумя не связанными друг с другом областями вашего проекта
  2. Для обмена логикой, не связанной с состоянием, во всем приложении.

Держите в уме, что одиночки не решение по хранению данных. Данные в них живут только пока живо ваше приложение в памяти.

Читайте далее, как определить синглтон!

Использование Object для определения синглтона корзины покупок

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

Вы собираетесь использовать возможности ключевого слова object, чтобы управлять этим с помощью одиночки ShoppingCart. Чтобы создать приложение перейдите в ▸ src ▸ main ▸ java ▸ com ▸ raywenderlich ▸ android ▸ kotlinobject ▸ ShoppingCart.kt и используйте object для определения ShoppingCart, далее выполните следующие действия:

  1. В Android Studio выберите File ▸ New ▸ Kotlin File/Class
  2. В открывшемся окне назовите файл ShoppingCart, выберите Object и нажмите Enter

Android Studio создаст файл с пустым object для вас. Это будет выглядеть так:

object ShoppingCart {
}

Обратите внимание, насколько это объявление объекта похоже на объявление класса в Kotlin! Единственное отличие состоит в том, что вы используете ключевое слово object вместо ключевого слова class. Но какая разница?

Вы можете многое узнать об этой разнице, посмотрев на эквивалентный код Java. К счастью для вас, в Android Studio есть способ сделать это.

Перейдите в Tools ▸ Kotlin ▸ Show Kotlin Bytecode и нажмите кнопку Decompile в верхней части окна Kotlin Bytecode. Вы увидите что-то вроде следующего:

public final class ShoppingCart {
   @NotNull
   public static final ShoppingCart INSTANCE;

   private ShoppingCart() {
   }

   static {
      ShoppingCart var0 = new ShoppingCart();
      INSTANCE = var0;
   }
}

На что обратить внимание:

  • Java класс имеет закрытую конструкцию
  • Имеет статический INSTANCE одиночки ShoppingCart для вызова

Помните об этом, когда пытаетесь использовать Kotlin вместе с Java!

Создание публичного интерфейса синглтона

Теперь, создав синглтон, необходимо создать его публичный интерфейс, чтобы другие классы могли общаться с ним. Для этого добавьте следующий код внутрь ShoppingCart:

// 1.
var products: List = emptyList()
  private set

// 2.
fun addProduct(product: Product) {
  products = products + listOf(product)
}

// 3.
fun clear() {
  products = emptyList()
}

Добавив этот код, вы:

  1. Создали список товаров. Эти продукты, которые пользователь добавил в свою корзину. У нее есть частный сеттер, потому что только синглтон должен иметь возможность напрямую изменять список.
  2. Создали функцию для добавления продукта в список products в корзине. MainActivity вызовет эту общедоступную функцию, когда пользователь добавит товар в свою корзину.
  3. Дали пользователю возможность очистить свою корзину покупок. Вызов этой функции очистит список продуктов, хранящийся в ShoppingCart.

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

Получение публичного интерфейса синглтона

Теперь добавьте некоторую логику для получения одиночки. Для этого:

  1. Откройте MainActivity.kt
  2. Найдите addProductToCart(), которую ProductListAdapter вызывает при выборе пользователем товара. Эта логика уже на месте. Все, что вам нужно – это заменить // TODO внутри addProductToCart() на код ниже, обязательно импортируя android.widget.Toast:
ShoppingCart.addProduct(product)
Toast.makeText(
    this,
    R.string.product_added_toast, Toast.LENGTH_SHORT
).show()

Заметьте, что вы вызываете ShoppingCart.addProduct(product), используя имя.

Для визуализации обратной связи, позволяющей убедиться, что вы успешно добавили продукт, вы также показываете Toast.

Теперь соберите и запустите. Нажмите на продукт, и вы увидите Toast, подтверждающий, что вы добавили его в корзину:

Далее вы узнаете, как определять сопутствующие объекты. Вы будете использовать сопутствующий объект, чтобы помочь вам запустить новое действие для отображения содержимого ShoppingCart.

Работа с сопутствующими объектами

Для создания Intent, с помощью которого запускается ShoppingCartActivity, вы определите сопутствующий объект.

Сопутствующие объекты аналогичны автономным одиночным объектам, таким как ShoppingCart, но с одним ключевым отличием: сопутствующий объект принадлежит содержащему его классу.

Это будет важно, когда вы узнаете, как получить доступ к сопутствующим объектам в следующем разделе. А пока сосредоточитесь на определении сопутствующего объекта.

Определение сопутствующего объекта

Для определения сопутствующего объекта откройте ShoppingCartActivity и найдите // TODO перед последней закрывающей скобкой. Замените на следующее:

companion object {
  fun newIntent(context: Context): Intent {
    return Intent(context, ShoppingCartActivity::class.java)
  }
}

Если Android Studio побуждает так сделать, импортируйте android.content.Context и android.content.Intent для Context и Intent соответственно.

Поздравляю, вы определили сопутствующий объект!

Как и синглтон, который вы определили ранее, сопутствующий объект также содержит общедоступную функцию – в данном случае newIntent(). Сопутствующий объект также является единственным экземпляром, общим для всего вашего проекта.

В отличие от ShoppingCart, сопутствующий объект принадлежит к содержащему его классу. Таким образом, чтобы получить доступ к нему, вам нужно ссылаться на него по имени содержащего его класса, а не по его прямому имени, как в случае с одноэлементным объектом. Вы узнаете, как это сделать, в следующем разделе.

Заметка

Основное различие между объявлениями объектов и сопутствующими объектами заключается в том, как они инициализируются. Объявления объектов инициализируются при первом использовании. Сопутствующие объекты инициализируются при загрузке содержащего их класса, что соответствует поведению статических инициализаторов Java.

Получение сопутствующего объекта для старта задачи

На этом этапе вы проложили дорожку для покупок. Определили сопутствующий объект, дающий Intent открывать задачу по покупкам, когда пользователь нажимает на Go to Cart. Но вам еще нужно начать ShoppingCartActivity.

Для этого:

  1. Откройте MainActivity и найдите goToCart()
    Замените // TODO на:
val intent = ShoppingCartActivity.newIntent(this)
startActivity(intent)

Обратите внимание, что вы ссылаетесь на сопутствующий объект, используя имя содержащего его класса: ShoppingCartActivity. Вы также применяете Intent, который был создан сопутствующим объектом для начала задачи.

Теперь соберите проект и запустите его. Нажмите на Go to Cart и вы увидите, что приложение теперь показывает новый пустой экран:

Но куда же подевались все товары из вашей корзины? Не волнуйтесь, вы займетесь этим в следующем разделе.

Отображение товаров из корзины

Теперь вы создадите экран корзины для покупок, используя:

  • TextView с итоговой ценой за выбранные товары
  • Список товаров, которые пользователь добавил в корзину
  • Кнопку Clear Cart, которая, как вы поняли, очищает корзину

Для начала вы примените Java класс для подсчета итоговой цены корзины. В процессе вы изучите, как совместимость между Kotlin и Java одиночками устроена.

Получение Kotlin одиночки из Java для подсчета цены

Начните с создания нового Java класса для выполнения этих вычислений, используя следующие шаги:

  1. В Android Studio перейдите в File ▸ New ▸ Java Class
  2. Назовите класс ShoppingCartCalculator и выберите Class
  3. Нажмите Enter и OK

Android Studio создаст Java класс ShoppingCartCalculator. Далее добавьте метод перед закрывающей скобкой:

Integer calculateTotalFromShoppingCart() {
  List products = ShoppingCart.INSTANCE.getProducts();
  int totalPriceCents = 0;
  for (Product product : products) {
    totalPriceCents += product.getPriceCents();
  }

  return totalPriceCents;
}

Этот метод использует ShoppingCart.INSTANCE для доступа к экземпляру одиночки и затем подсчитывает сумму, добавляя стоимость всех товаров из корзины. Этот подход отличается от Kotlin, где вам не нужно использовать INSTANCE.

Пока этот метод отрабатывает, вы можете его немного подчистить, используя @JvmStatic:

  • Откройте ShoppingCart
  • Найдите var products
  • Добавьте @JvmStatic перед строкой с объявлением products

Добавив аннотацию, объявление products должно выглядеть так:

@JvmStatic
var products: List = emptyList()
  private set

Теперь вы можете удалить INSTANCE при ссылке на продукты. Для этого удалите INSTANCE из ShoppingCartCalculator.java. Теперь метод должен выглядеть так:

Integer calculateTotalFromShoppingCart() {

  // Removed INSTANCE below. Rest is identical:
  List products = ShoppingCart.getProducts();
  int totalPriceCents = 0;
  for (Product product : products) {
    totalPriceCents += product.getPriceCents();
  }

  return totalPriceCents;
}

В Java использование @JvmStatic эффективно преобразует свойство экземпляра одиночки в статическое поле в классе ShoppingCart.

Другими словами, в Java то, что раньше находилось внутри объекта с именем ShoppingCart.INSTANCE, теперь является статическим полем верхнего уровня в ShoppingCart. Вы можете использовать уловку, которую вы узнали ранее в этой статье, чтобы проверить эквивалентный Java код для ShoppingCart, чтобы увидеть, как это работает в действии:

public final class ShoppingCart {
   @NotNull
   private static List products;
   @NotNull
   public static final ShoppingCart INSTANCE;
   // ...
}

Отображение товаров в корзине

Вам нужно будет подключить все, чтобы отображались товары и общая цена в корзине. Для этого:

  1. Откройте ShoppingCartActivity и найдите setupProducts()
  2. Замените все тело этой функции следующим кодом:
// 1.
products = ShoppingCart.products

// 2.
val calculator = ShoppingCartCalculator()
val totalPriceCents = 
    calculator.calculateTotalFromShoppingCart()

// 3.
viewBinding.textTotalCartValue.text = 
    getString(R.string.text_total_price,
        totalPriceCents.asPriceString)

Вот что делает приведенный выше код:

  1. Читает товары из ShoppingCart. Синглтоны — это единичные экземпляры во всем приложении. Таким образом, код будет содержать все продукты, которые пользователь добавил в корзину.
  2. Создает экземпляр класса Java, который вы определили ранее, ShoppingCartCalculator, и использует его для расчета общей стоимости всех товаров в корзине. Метод Java считывает Kotlin одиночку внутри и возвращает общую стоимость. Один и тот же объект одиночки читается как кодом Java в калькуляторе, так и выше в коде Kotlin.
  3. Обновляет TextView, отображающий общую цену, преобразуя ее в денежный формат с помощью расширения Kotlin, определенного в StringUtils.kt.

Теперь соберите и запустите проект. Убедитесь, что он работает, добавив несколько продуктов в корзину и нажав Go to Cart. Вы увидите экран, на котором отображается корзина с товарами плюс общая цена:

Вы только что узнали, как заставить Java получить доступ к данным из синглтона Kotlin. Далее вы узнаете, как удалить товары из корзины с помощью кнопки Clear Cart.

Определение слушателей объектов

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

Уведомление слушателей об изменениях корзины

Чтобы уведомить слушателей об изменениях корзины, откройте ShoppingCart и добавьте следующий код перед последней закрывающей скобкой:

interface OnCartChangedListener {
  fun onCartChanged()
}

Здесь вы определяете интерфейс, который ShoppingCart будет использовать для уведомления слушателей об изменении своих данных.

Затем добавьте следующий код между var products и addProduct() в ShoppingCart:

private var onCartChangedListener: WeakReference? = null

fun setOnCartChangedListener(listener: OnCartChangedListener) {
  this.onCartChangedListener = WeakReference(listener)
}

Если Android Studio просит импортировать, импортируйте java.lang.ref.WeakReference для WeakReference.

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

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

Теперь, когда у вас есть слушатель, вы должны его уведомить! Еще в ShoppingCart добавьте следующую функцию:

private fun notifyCartChanged() {
  onCartChangedListener?.get()?.onCartChanged()
}

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

И теперь, добавьте вызов этого метода в конец addProduct() и clear(). Когда закончите, это должно выглядеть так:

fun addProduct(product: Product) {
  products = products + listOf(product)
  notifyCartChanged() // New
}

fun clear() {
  products = emptyList()
  notifyCartChanged() // New
}

Теперь, когда бы пользователь не добавил товар в корзину, ShoppingCart уведомит его слушателей.

Прослушивание изменений корзины: анонимные объекты

Здесь вы используете другую форму слова object! На этот раз вы определите анонимный объект, который использует интерфейс, ранее вами определенный.

Для этого вернитесь в ShoppingCartActivity и добавьте следующее свойство между var products и onCreate():

private var onCartChangedListener =
    object : ShoppingCart.OnCartChangedListener {
      override fun onCartChanged() {
        setupProducts()
        setupRecyclerView()
      }
    }

Поскольку в этом интерфейсе есть метод onCartChanged (), вы реализовали его прямо в объявлении объекта!

Вы определили этот анонимный объект как свойство своего ShoppingCartActivity. Это означает, что переопределенный onCartChanged() может получить доступ к любым функциям и свойствам в действии.

Имея это в виду, вы вызываете несколько методов, setupProducts() и setupRecyclerView(), из действия при изменении корзины. Эти методы вызовут повторный ре-рендеринг RecyclerView и общей цены.

Теперь вам нужно указать одиночке использовать только что созданное свойство в качестве его слушателя. Найдите // Your code внутри onCreate() и замените его на:

ShoppingCart.setOnCartChangedListener(onCartChangedListener)

Это сообщит одиночке вызвать onCartChangedListener, когда содержимое корзины меняется.

Далее найдите setupClearCartButton() и замените // TODO на:

viewBinding.clearCartButton.setOnClickListener { 
  ShoppingCart.clear()
}

Этот код вызывает clear() в синглтон, когда пользователь нажимает кнопку Clear Cart.

Когда пользователь очищает корзину для покупок, одиночка уведомляет своего слушателя. Поскольку слушатель был настроен на анонимный объект в ShoppingCartActivity, именно об этом и отправляется уведомление.

Теперь соберите и запустите приложение. Добавьте товары в корзину, нажмите Go to Cart и Clear Cart. Это очистит корзину и обновит view:

И вот: финальный вид вашей корзины покупок!

Далее вы изучите некоторые лучшие приемы по работе с Kotlin объектами.

Лучшие приемы для синглтонов и сопутствующих объектов

Прежде чем закончить статью, уделите немного времени рассмотрению некоторых передовых моментов.

  • Избегайте чрезмерного использования синглтона: соблазнительно использовать синглтон в качестве решения для всех ваших потребностей при обмене данными. Хотя поначалу это удобно, чрезмерное использование одиночек вызовет проблемы с поддержкой кода, потому что многие его части внезапно будут зависеть от синглтона. Вы обнаружите, что одно изменение повлияет на несколько несвязанных частей вашего проекта. Используйте синглтон разумно, чтобы избавиться от лишней головной боли.
  • Синглтоны могут вызывать проблемы с использованием памяти: избегайте хранения слишком большого количества данных в синглтонах. Помните, что они глобальны, и сборщик мусора никогда не освобождает данные автоматически, сильно удерживаемые синглтоном.
  • Синглтоны могут вызывать утечки памяти: когда вы используете синглтон для того, чтобы сослаться на экземпляр, который вызывается извне, могут произойти утечки. Это особенно относится к классам, связанным с Android, таким как действия, фрагменты, адаптеры и другие. Если ваш синглтон содержит сильную ссылку на любой из них, сборщик мусора не освободит их, и они останутся в памяти на неопределенный срок. Чтобы избежать этой проблемы, реструктурируйте свой код, чтобы синглтоны либо не содержали экземпляры этих классов, либо использовали слабые ссылки.

Загрузите финальный проект.

Оригинал статьи.

Комментарии

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: