Привет, хабр! В данной небольшой статье я бы хотел поделится опытом работы со слабыми ссылками при разработке приложения для 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/
Добавить комментарий