Kotlin для Android: упрощаем работу со слабыми ссылками

от автора

Привет, хабр! В данной небольшой статье я бы хотел поделится опытом работы со слабыми ссылками при разработке приложения для Android с использованием активно развивающегося сейчас языка Kotlin.

Я уже довольно долго пишу iOS приложения с использованием Swift. А еще раньше писал Android приложения на Pure Java 6. И желания возвращаться у меня не возникало ни на секунду. Но по долгу службы появилась необходимость вернуться. В это время замечательная компания JetBrains уже сделали релиз jvm-компилируемого языка Kotlin (в момент написания статьи — версии 1.1.1). И тогда я твердо решил, что мой проект будет не на Java.

Java

Рассмотрим стандартная ситуация, когда вы прототипируете приложение и делаете запросы прямо из компонента UI (в данном случае, Activity):

// MainActivity.java    void loadComments() {         api.getComments(new Api.ApiHandler<Api.Comment>() {             @Override             public void onResult(@Nullable List<Api.Comment> comments, @Nullable Exception exception) {                 if (comments != null) {                     updateUI(comments);                 } else {                     displayError(exception);                 }             }         });     }

Криминал в данном случае очевиден. Анонимный класс хендлера держит сильную ссылку на компонент (неявное свойство this$0 в дебаггере), что не очень хорошо, если пользователь решит завершить Activity.

Решить данную проблему можно, если использовать слабую ссылку на наше Activity:

// MainActivity.java  void loadCommentsWithWeakReferenceToThis() {         final WeakReference<MainActivity> weakThis = new WeakReference<>(this);         api.getComments(new Api.SimpleApiHandler<Api.Comment>() {             @Override             public void onResult(@Nullable List<Api.Comment> comments, @Nullable Exception exception) {                 MainActivity strongThis = weakThis.get();                 if (strongThis != null)                      if (comments != null)                          strongThis.updateUI(comments);                      else                          strongThis.displayError(exception);             }         });     }

Конечно, это не сработает. Как упоминалось ранее, анонимный класс держит сильную ссылку на объект, в котором был создан.

Единственным решением остается передавать слабую ссылку (или создавать внутри) в другой объект, который не подвержен жизненному циклу компонента (в нашем случае объект класса Api):

// MainActivity.java  public class MainActivity extends AppCompatActivity implements Api.ApiHandler<Api.Comment> {  void loadCommentsWithWeakApi() {         api.getCommentsWeak(this);     }      @Override     public void onResult(@Nullable List<Api.Comment> comments, @Nullable Exception exception) {         if (comments != null)             updateUI(comments);         else             displayError(exception);     } 

// Api.java  class Api {     void getCommentsWeak(ApiHandler<Comment> handler) {         final WeakReference<ApiHandler<Comment>> weakHandler = new WeakReference<>(handler);         new Thread(new Runnable() {             @Override             public void run() {                 … // getting comments                 ApiHandler<Comment> strongHandler = weakHandler.get();                 if (strongHandler != null) {                     strongHandler.onResult(new ArrayList<Comment>(), null);                 }             }         }).start();     }     … }

В итоге мы совсем избавились от анонимного класса, наше Activity теперь реализует интерфейс хендлера Api и получает результат в отдельный метод. Громоздко. Зато больше нет удержания ссылки на Activity.

Как бы я сделал в Swift:

// ViewController.swift  func loadComments() {     api.getComments {[weak self] comments, error in        guard let `self` = self else { return }          if let comments = comments {              self.updateUI(comments)          } else {              self.displayError(error)          }     } } 

В данном случае объект за идентификатором self (значение примерно такое же, как this в Java) передается в лямбду как слабая ссылка.

И на Pure Java мне такое поведение вряд ли удасться реализовать.

Пробуем Kotlin

Перепишем наш функционал на Kotlin:

// MainActivity.kt    internal fun loadComments() {         api.getComments { list, exception ->             if (list != null) {                 updateUI(list)             } else {                 displayError(exception!!)             }         }     } 

Лямбды в Kotlin более умные и захватывают в себя аргументы только если они используются в нем самом. К сожалению, нельзя указать правила захвата (как в C++ или в Swift), поэтому ссылка на Activity захватывается как сильная:

(тут можно заметить, как лямбда является объектом, реализующем интерфейс Function2<T,V>)

Однако что нам мешает передавать слабую ссылку в лямбду:

// MainActivity.kt  internal fun loadCommentsWeak() {         val thisRef = WeakReference(this)         api.getComments { list, exception ->             val `this` = thisRef.get()             if (`this` != null)                  if (list != null) {                     `this`.updateUI(list)                 } else {                     `this`.displayError(exception!!)                 }         }     }

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

Однако сахар Kotlin позволит мне еще больше приблизится к синтаксису Swift:

// MainActivity.kt  internal fun loadCommentsWithMagic() {         val weakThis by weak(this)         api.getComments { list, exception ->             val `this` = weakThis?.let { it } ?: return@getComments             if (list != null)                 `this`.updateUI(list)              else                 `this`.displayError(exception!!)         }     }

Конструкция val A by B является назначением переменной A объект-делегат B, через которого будут устанавливаться и получаться значение переменной A.
weak(this) — упрощенная функция-конструктор специального класса WeakRef

// WeakRef.kt  class WeakRef<T>(obj: T? = null): ReadWriteProperty<Any?, T?> {      private var wref : WeakReference<T>?      init {         this.wref = obj?.let { WeakReference(it) }     }      override fun getValue(thisRef:Any? , property: KProperty<*>): T? {         return wref?.get()     }      override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {         wref = value?.let { WeakReference(value) }     } }  // Та самая функция-конструктор fun <T> weak(obj: T? = null) = WeakRef(obj) 

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

Добавим еще сахарку

// MainActivity.kt      internal fun loadCommentsWithSugar() {         val weakThis by weak(this)         api.getComments { list, exception -> weakThis?.run {             if (list != null)                 updateUI(list)             else                 displayError(exception!!)         }}     }

В определенной части кода мы начинаем вызывать функции нашего Activity даже без указания какого-то конкретного объекта, как будто ссылаемся на наше исходное activity, что автоматически захватывает его в хендлер. А мы от этого так долго пытались избавится.

Как видно из дебаггера, этого не происходит.

Замечательно свойство лямбд в Kotlin — возможность устанавливать его владельца (как в Javascript). Таким образом this в лямбде после weakThis?.run принимает значение объекта Activity, причем сама лямбда выполнится только тогда, когда данный объект еще находится в памяти. Функция run() является расширением любого типа и позволяет создать лямбду с владельцем объекта, у которого оно вызвано (а еще есть другие магические функции типа let(), apply(), also() и тп).

В дебаггере свойство this у лямбды указывается как $receiver.

Подробнее про лямбды в Kotlin можно найти на сайте языка.

Под конец можно еще уменьшить количество строк (но не скобок)

// MainActivity.kt  internal fun loadCommentsWithDoubleSugar() = this.weaked().run { api.getComments { list, exception -> this.get()?.run {             if (list != null)                 updateUI(list)             else                 displayError(exception!!)     }}}  // weakref.kt  fun <T>T.weaked() = WeakReference(this) 

Выводы

Kotlin — отличная альтернатива Java для разработки приложений для JVM-приложений. Описанные выше приемы — меньшая часть возможностей языка.
Один из привлекательных фактов для Swift-разработчиков — довольно похожий cинтаксис.
По своему опыту работы с ним могу утверждать, что при правильном использовании он позволяет существенно сократить количество кода бизнес-логики по сравнению с Java (не берусь утверждать за утилитный код).

Для ознакомления со всеми фичами языка можно почитать документацию.
Как интергрировать Kotlin в Android проект можно узнать здесь.
Исходники проекта на git.

ссылка на оригинал статьи https://habrahabr.ru/post/325966/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *