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

  • Tutorial

Пишите на Java и ждёте асинхронные вызовы API прямо во фрагменте или Activity? Используя анонимные классы? В этой статье я расскажу, как Kotlin позволяет это сделать без вреда для GC и возможных IllegalStateException.


image


В данной статье приведёна работа со слабыми ссылками на примере ожидания асинхронных вызовов из компонентов 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.


image


Решить данную проблему можно, если использовать слабую ссылку на наше 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);
            }
        });
 }

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


image


Единственным решением остается передавать слабую ссылку (или создавать внутри) в другой объект, который не подвержен жизненному циклу компонента (в нашем случае объект класса 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 захватывается как сильная:


image
(тут можно заметить, как лямбда является объектом, реализующем интерфейс 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!!)
                }
        }
}

image


Как видно из дебаггера, у нашего хендлера больше нет прямой ссылки на 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, что автоматически захватывает его в хендлер. А мы от этого так долго пытались избавится.


image


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


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


image


В дебаггере владелец лямбды указывается как свойство $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);
        });
    }

image


У Android пока еще нет полной поддержки Java 8, но некоторые фичи уже поддерживаются. До Android Studio 2.4 Preview 4 потребуется использовать Jack toolchain.


Выводы


В данной статье я привёл пример того, как с помощью Kotlin можно решить проблему безопасного ожидания асинхронных вызовов из копонентов жизненного цикла приложения Android, а так же сравнил его с решением, которое предлагает Java (<8).


Kotlin позволил написать код функциональном стиле без ущерба безопасности для жизненного цикла, что, несомненно, является плюсом.


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


Update: Добавил про Java 8

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 13

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

    Да, другой класс должен быть не anonymous, но разве его нельзя сделать static inner?


    А если использовать lambda из Java 8, последние версии android studio вроде имеют
    неплохую поддержку java 8?

      0
      Смысл в том, чтобы обрабатывать асинхронные вызовы в одном месте кода. Не вижу решения проблемы через static inner (или просто static) классы.

      Поизучал лямбды в Java 8. Они действительно ведут себя лучше, чем анонимные классы, т.е. захватывают контекст только по потребности. Обновлю статью.
      +1
      У меня возникло несколько вопросов:
      1) А причем тут Kotlin?
      -Обертки вокруг сильных ссылок появились в Java с 1.2 версии и вы просто сделали «те же яйца но на котлиновских референсах». Просто тема уже изжеванная.

      2) Зачем пытаться изобрести велосипед и изначально выбирать не правильное решение по способу загрузки. Простейший MVP решит все ваши проблемы с уничтожением View.

      3) Если я не ошибаюсь, то очистка WeakRef происходит с вызовом GC.

      val thisRef = WeakReference(this) // слабая ссылка на Activity
              api.getComments { list, exception ->
                  val `this` = thisRef.get() // получаем Activity или null

      Что то может пойти не так…

      4) Extention functions
      fun <T>T.weak() = WeakReference(this)
      

      Предположим у меня небольшой проект с очень качественным single resposibility. Как скажется данный подход на вашем DexCount?

      btw, я не являюсь адептом java или kotlin. Использую и то и другое в продакшене. Последний проект был целиком написан на kotlin. Но все же это просто инструмент (с более низким порогом вхождения после swift).
        +1

        1) Возможно, стоило озаглавить "… в Java и Kotlin". Конечно, получилось вроде "безопасная обработке асинхронных вызовов в той же части кода, в которой этот вызов был совершен", но это немного жирно для заголовка. Да и тут представлен именно подход.


        2) MVP — это еще одно равноправное решение данной проблемы. В своем проекте я использую Clean Architecture (VIP). Очень удобно декларировать слабую ссылку того же презентора на view через делегирование:


        var view by weak<View>()

        В Swift я бы написал


        weak var view: View?

        3) thisRef в лямбду захватывается сильной ссылкой. Очистится ли сама ссылка? Cложно поверить, что GC очистит слабую ссылку на валидный объект.


        4) По-хорошему количество новых методов соответствовать количеству разных классов, на объектах которого вызывается данный метод.


        все же это просто инструмент

        Согласен.

        +2

        Удивлен что не было упомянуто решение "в Kotlin стиле" — завернуть все эту логику с ссылками в одну функцию:


        api.getCommentsWeakRef { list, exception ->
            if (list != null)
                updateUI(list)    // the methods are called on a weak reference
            else
                 displayError(exception!!)
        }
          +1

          Сделаю по принципу Брежнева — кругом хвалят — надо попробовать Kotlin

            +1

            Уже два года как люди используют RxJava. Отписываешься в onDestroy и порядок. Да, разбиение на маленькие классы помогает от утечек памяти в том числе.

              +1
              К теме статьи относится мало, но вдруг Вам будет интересно.
              От Jack'а отказались.
                +2
                С weak-ом такое вот решение еще можно, если уже котлин использовать:
                class WeakContext<T>(self: T) {
                
                    private val reference = WeakReference(self)
                
                    val self: T?
                        get() = reference.get()
                
                    inline fun <R> ifSelfDefined(body: T.()->R): R? = self?.let(body)
                
                }
                
                inline fun <T, R> T.weakable(body: WeakContext<T>.()->R): R = WeakContext(this).body()
                
                class Test {
                
                    fun someMethod() = weakable {
                        api.getComments { list, exception ->
                            ifSelfDefined<Unit> {
                                if (list != null)
                                    updateUI(list)
                                else
                                    displayError(exception!!)
                            }
                        }
                
                    }
                
                }
                
                  +1
                  понял, что контекст не нужен, т. е. еще проще:
                  inline fun <T, R> T.weakable(body: WeakReference<T>.() -> R): R = WeakReference(this).body()
                  
                  inline fun <T, R> WeakReference<T>.ifSelfDefined(body: T.() -> R): R? = get()?.let(body)
                  
                  class Test {
                  
                      fun someMethod() = weakable {
                          api.getComments { list, exception ->
                              ifSelfDefined {
                                  if (list != null)
                                      updateUI(list)
                                  else
                                      displayError(exception!!)
                              }
                          }
                  
                      }
                  
                  }
                  

                  и что важно, совершенно бесплатная абстракция выходит, как если бы мы сохраняли ссылку перед вызовом
                  0
                  Подход хороший, только вот пример совсем неудачный, не надо так работать с активити :)
                    0
                    Автор, спасибо за статью.
                    Однако весьма любопытно, на каком android-api проводились ваши эксперименты?

                    А не ведут ли Kotlin-лямбды себя как RetroLamda:
                    (на устройствах < Nougat — конверт в обычный аноним)???

                    Если на уровне байт-кода, например на каком-нибудь Kit-Kat котлиновская лямда превращается в старый-добрый аноним-класс (что скорее всего, для обратной совместимости c < java 8) — то боюсь… все ваши старания по искоренению утечек в этом случае бесполезны.

                    Видится мне, пока мы не доживём до min API24 — единственное честное решение — остается как и прежде:
                    Внутренний Статик Класс со слабой ссылкой.

                    Либо другой путь — забить на утечки, потому как во всю в моде RX с ретролямбдой — и… народ как-то особо не парится)))
                      0
                      Интересные примеры работы со слабыми ссылками, но нужно отметить принципиально неправильный подход в реализации и использовании API. Activity должна отменять любые (сетевые) операции после своего завершения. Если следовать этому правилу, то не будет необходимости в использовании слабых ссылок.
                      Правда при таком подходе будут отменятся также операции при изменении конфигурации, но это уже другая тема.

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

                      Самое читаемое