📜 ⬆️ ⬇️

Kotlin for Android: simplify working with weak links in asynchronous calls

Write in Java and wait for asynchronous API calls directly in the fragment or Activity? Using anonymous classes? In this article I will tell you how Kotlin allows you to do this without harming the GC and possible IllegalStateException.


image


This article shows the work with weak links on the example of waiting for asynchronous calls from the components of the Android application. However, this experience is applicable to other situations where you need to use weak links.


Ps. I’ve been writing Swift for quite some time. And before that, I wrote Android applications in Java 6. And I didn’t have a desire to return to it for a second. But on duty, I still needed to develop an Android application. By that time, JetBrains had already released the Kotlin jvm-compiled language (at the time of writing this article, version 1.1.1). After looking at the documentation on it, I firmly decided that my project would not be in Java.


First, I will give an example of handling asynchronous calls using Java, the standard Android development tool.


Java (<8)


Consider the standard situation when you prototype an application and make requests directly from the UI component (in this case, the 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); } } }); } 

The crime in this case is obvious. The anonymous handler class holds a strong reference to the component (the implicit property this $ 0 in the debugger), which is not very good if the user decides to terminate the Activity.


image


This problem can be solved by using a weak link to our 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); } }); } 

Of course it won't work. As mentioned earlier, the anonymous class holds a strong reference to the object in which it was created.


image


The only solution is to transfer a weak link (or create inside) to another object that is not subject to the component life cycle (in our case, an Api class object):


 // 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(); } … } 

As a result, we completely got rid of the anonymous class, our Activity now implements the Api handler interface and gets the result in a separate method. Cumbersome. Not functional. But then there is no longer holding the link to the Activity.


How would I do in 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) } } } 

In this case, the object behind the self identifier (the value is about the same as this in Java) is passed to the lambda as a weak link.


And on Pure Java, this behavior is unlikely to be implemented.


Kotlin


Let's rewrite our functionality on Kotlin:


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

Lambda in Kotlin (as in Java 8) are smarter than anonymous classes, and capture arguments only if they are used in it. Unfortunately, you cannot specify capture rules (as in C ++ or in Swift), so the link to the Activity is captured as strong:


image
(here you can see how lambda is an object that implements the interface Function2<T,V> )


However, what prevents us from transmitting a weak link to the lambda:


 // 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


As you can see from the debugger, our handler no longer has a direct link to the Activity, which was what was required. We have a secure handler for an asynchronous call response, written in a functional style.


However, Kotlin sugar will allow me to get even closer to the Swift syntax:


 // 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!!) } } 

The construction val A by B is the assignment of the variable A to the delegate object B, through which the value of the variable A will be established and obtained.


weak(this) is a simplified constructor function of the special class 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 is the WeakRef decorator, allowing it to be used as a delegate. You can read more about delegating to Kotlin on the language site .


Now the construction val A by weak(B) makes it possible to declare weak variables and properties. In Swift, for example, this feature is supported at the language level:


 weak var A = B 

Add more sugar


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

In a certain part of the code, we begin to call the functions of our Activity even without specifying any particular object, as if we refer to our original activity, which automatically captures it into the handler. And we tried to get rid of it for so long.


image


As you can see from the debugger, this does not happen.


The remarkable feature of lambda in Kotlin is the ability to set its owner (as in Javascript). Thus this lambda after weakThis?.run accepts the value of the Activity object, and the lambda itself will be executed only when the given object is still in memory. The run() function is an extension of any type and allows you to create a lambda with the owner of the object for which it is called (There are other magic functions like let() , apply() , also() ).


image


In the debugger, the lambda owner is specified as the $receiver property.


More information about lambda in Kotlin can be found on the language website .


Finally, some more sugar:


 // 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


Lambda in Java 8 also does not capture the object where they were created:


 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 does not yet have full support for Java 8, but some features are already supported. Prior to Android Studio 2.4 Preview 4, you will need to use the Jack toolchain .


findings


In this article, I gave an example of how using Kotlin can solve the problem of safely waiting for asynchronous calls from the components of the life cycle of an Android application, and also compared it with a solution that offers Java (<8).


Kotlin allowed to write the code in a functional style without sacrificing safety for the life cycle, which is undoubtedly a plus.


→ To familiarize yourself with all the features of the language, you can read the documentation .
→ How to integrate Kotlin into Android project can be found here .
→ Project sources on git .


Update : Added about Java 8


')

Source: https://habr.com/ru/post/325966/


All Articles