Пишите на Java и ждёте асинхронные вызовы API прямо во фрагменте или Activity? Используя анонимные классы? В этой статье я расскажу, как Kotlin позволяет это сделать без вреда для GC и возможных IllegalStateException.
В данной статье приведёна работа со слабыми ссылками на примере ожидания асинхронных вызовов из компонентов Android приложения. Однако данный опыт применим и для других ситуаций, когда требуется использовать слабые ссылки.
PS. Я уже довольно давно пишу на Swift. А еще раньше писал Android приложения на Java 6. И желания возвращаться к ней у меня не возникало ни на секунду. Но по долгу службы мне все же потребовалось разработать Android приложение. К тому моменту компания JetBrains уже сделала релиз jvm-компилируемого языка Kotlin (в момент написания статьи — версии 1.1.1). Посмотрев документацию по нему, я твердо решил, что мой проект будет не на Java.
Сначала я приведу пример обработки асинхронных вызовов с использованием Java — стандартного инструмента для разработки под Android.
Java (<8)
Рассмотрим стандартная ситуация, когда вы прототипируете приложение и делаете запросы прямо из компонента 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 // слабый захват self
guard let `self` = self else { return } // если self нет, то выходим
if let comments = comments {
self.updateUI(comments)
} else {
self.displayError(error)
}
}
}
В данном случае объект за идентификатором self
(значение примерно такое же, как this
в Java) передается в лямбду как слабая ссылка.
И на Pure Java мне такое поведение вряд ли удасться реализовать.
Kotlin
Перепишем наш функционал на Kotlin:
// MainActivity.kt
fun loadComments() {
api.getComments { list, exception ->
if (list != null) {
updateUI(list)
} else {
displayError(exception!!)
}
}
}
Лямбды в Kotlin (как и в Java 8) более умные, чем анонимные классы, и захватывают в себя аргументы только если они используются в нем самом. К сожалению, нельзя указать правила захвата (как в C++ или в Swift), поэтому ссылка на Activity захватывается как сильная:
(тут можно заметить, как лямбда является объектом, реализующем интерфейс Function2<T,V>
)
Однако что нам мешает передавать слабую ссылку в лямбду:
// MainActivity.kt
fun loadCommentsWeak() {
val thisRef = WeakReference(this) // слабая ссылка на Activity
api.getComments { list, exception ->
val `this` = thisRef.get() // получаем Activity или null
if (`this` != null)
if (list != null) {
`this`.updateUI(list)
} else {
`this`.displayError(exception!!)
}
}
}
Как видно из дебаггера, у нашего хендлера больше нет прямой ссылки на Activity, что и требовалось добиться. У нас получился безопасный обработчик ответа асинхронного вызова, написанный в функциональном стиле.
Однако сахар Kotlin позволит мне еще больше приблизится к синтаксису Swift:
// MainActivity.kt
fun loadCommentsWithMagic() {
val weakThis by weak(this) // искусственная weak-переменная
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(it) }
}
}
// Та самая функция-конструктор
fun <T> weak(obj: T? = null) = WeakRef(obj)
WeakRef
является декоратором WeakReference, позволяющего использовать его как делегат. Подробнее про делегировние в Kotlin можно прочитать на сайте языка.
Теперь конструкция val A by weak(B)
даёт возможность декларировать слабые переменные и свойства. В Swift, например, данная фича это поддерживается на уровне языка:
weak var A = B
Добавим еще сахарку
// MainActivity.kt
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()
).
В дебаггере владелец лямбды указывается как свойство $receiver
.
Подробнее про лямбды в Kotlin можно найти на сайте языка.
Напоследок еще немного сахара:
// MainActivity.kt
fun loadCommentsWithDoubleSugar() = this.weak().run {
// здесь this уже WeakReference<Activity>
api.getComments { list, exception -> this.get()?.run {
// здесь this уже Activity
if (list != null)
updateUI(list)
else
displayError(exception!!)
}}}
// weakref.kt
// добавляем во все классы функцию weak()
fun <T>T.weak() = WeakReference(this)
Update: Java 8
Лямбды в Java 8 также не захватывают объект, где были созданы:
void loadCommentsWithLambdaAndWeakReferenceToThis() {
final WeakReference<MainActivity> weakThis = new WeakReference<>(this);
api.getComments((comments, exception) -> {
MainActivity strongThis = weakThis.get();
if (strongThis != null)
if (comments != null)
strongThis.updateUI(comments);
else
strongThis.displayError(exception);
});
}
У Android пока еще нет полной поддержки Java 8, но некоторые фичи уже поддерживаются. До Android Studio 2.4 Preview 4 потребуется использовать Jack toolchain.
Выводы
В данной статье я привёл пример того, как с помощью Kotlin можно решить проблему безопасного ожидания асинхронных вызовов из копонентов жизненного цикла приложения Android, а так же сравнил его с решением, которое предлагает Java (<8).
Kotlin позволил написать код функциональном стиле без ущерба безопасности для жизненного цикла, что, несомненно, является плюсом.
Для ознакомления со всеми фичами языка можно почитать документацию.
Как интергрировать Kotlin в Android проект можно узнать здесь.
Исходники проекта на git.
Update: Добавил про Java 8
Автор: Ivan Brazhnikov