Everlasting Loops – Telegram
Everlasting Loops
628 subscribers
7 photos
1 video
10 links
Some notes on mobile dev stuff

Android | Kotlin

Автор: @vla_dos
Download Telegram
Channel created
Наконец добрался до канала, зато с новостями

На досуге сделал плагин для Intellij, решил написать об этом статью, и ее опубликовали в ProAndroidDev! Claps or any feedback appreciated, а сам плагин можно посмотреть тут

В ближайшее время постараюсь сделать перевод для хабра
👍5
Channel photo updated
а вот, кстати, и перевод

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

на хабре гифка локализована; оригинал – в первом комментарии
🔥4
Kotlin function types

ответил в комментариях в Android Good Reads, но решил и тут написать, вдруг кому-то будет интересно

допустим, у нас есть такой Java-класс


public class SomeJavaClass {

interface Listener {
void onEvent(String event);
}

private HashSet<Listener> listeners = new HashSet<>();

void addListener(Listener listener) {
listeners.add(listener);
}

void removeListener(Listener listener) {
listeners.remove(listener);
}
}


и такой Kotlin-класс


class SomeKotlinClass {

private val eventListenerLambda = { eventId: String ->
println(eventId)
}

fun testLambdas(someJavaClass: SomeJavaClass) {
someJavaClass.addListener(eventListenerLambda)

someJavaClass.removeListener(eventListenerLambda)
}
}


чтобы передать лямбду типа (String) -> Unit в качестве параметра типа Listener, компилятор либо использует method reference, либо, когда Java 6, заворачивает в адаптер, и в обоих случаях инстанс итогового Listener каждый раз получается разный


public final class SomeKotlinClass {
@NotNull
private final Function1 eventListenerLambda = SomeKotlinClass::eventListenerLambda$lambda$0;
public static final int $stable;

public final void testLambdas(@NotNull SomeJavaClass someJavaClass) {
Intrinsics.checkNotNullParameter(someJavaClass, "someJavaClass");
Function1 var2 = this.eventListenerLambda;
someJavaClass.addListener(SomeKotlinClass::testLambdas$lambda$1);
var2 = this.eventListenerLambda;
someJavaClass.removeListener(SomeKotlinClass::testLambdas$lambda$2);
}

private static final Unit eventListenerLambda$lambda$0(String eventId) {
Intrinsics.checkNotNullParameter(eventId, "eventId");
System.out.println(eventId);
return Unit.INSTANCE;
}

private static final void testLambdas$lambda$1(Function1 $tmp0, String event) {
$tmp0.invoke(event);
}

private static final void testLambdas$lambda$2(Function1 $tmp0, String event) {
$tmp0.invoke(event);
}
}


и если запустить, можно увидеть, что после выполнения someJavaClass.removeListener(eventListenerLambda) содержимое listeners не изменится

напишите в комменты, кому это было очевидно!
🔥31
LinkageError

На днях готовил к релизу наш SDK для Unity. И всё должно было быть просто, потому что это обертка над нативными, самое сложное позади

А на нативном SDK для Android у нас есть UI-фича, которая сделана на Compose

И вот при взаимодействии с ней в Unity-проекте всё крэшилось с java.lang.NoClassDefFoundError, в рантайме почему-то не нашлось класса, который был при компиляции. Это частный случай java.lang.LinkageError – обычно такие ситуации возникают, когда dependencyA уже ранее была скомпилирована с dependencyB, но в рантайме оказалась другая версия dependencyB, с несовместимым API, либо её вовсе нет. А так как всё уже было скомпилировано ранее, ошибка происходит в рантайме

На нативном SDK при этом всё хорошо; другие пакеты в Unity не привносят конфликтующие androidx-зависимости

Если кто-то догадался, что произошло, пишите в комменты
2
Everlasting Loops
LinkageError На днях готовил к релизу наш SDK для Unity. И всё должно было быть просто, потому что это обертка над нативными, самое сложное позади А на нативном SDK для Android у нас есть UI-фича, которая сделана на Compose И вот при взаимодействии с ней…
Разгадка

Дело в том, что гугл не отстает и тоже переводит свои библиотеки на KMP – таким образом, у артефакта есть какой-то общий id (например, lifecycle-viewmodel), а для конкретного таргета уже выбирается зависимость с соответствующим суффиксом. В итоге некоторые транзитивные зависимости зарезолвились как-то так:


+--- androidx.compose.material3:material3-desktop:{strictly 1.3.0} -> 1.3.0 (c)
+--- androidx.lifecycle:lifecycle-viewmodel-compose-desktop:{strictly 2.8.0} -> 2.8.0 (c)
+--- androidx.compose.ui:ui-jvmstubs:{strictly 1.7.2} -> 1.7.2 (c)


Только суффикс должен был быть -android.

Напишите, кстати, в комменты, кто сталкивался с таким. Кажется, что это не очень тривиально – практически все пишут на котлине, и когда мы подключаем Kotlin Gradle plugin в модуль, зависимости резолвятся с ожидаемым суффиксом:


+--- androidx.compose.material3:material3-android:{strictly 1.3.0} -> 1.3.0 (c)
+--- androidx.lifecycle:lifecycle-viewmodel-compose-android:{strictly 2.8.3} -> 2.8.3 (c)
+--- androidx.compose.ui:ui-android:{strictly 1.7.2} -> 1.7.2 (c)


Бридж, который связывает слой нативного андроида и Unity, был написан на Java; Kotlin-плагина в проекте не было вообще.

Ну а дальше всё просто: каких-то сущностей не оказалось в -desktop/-jvmstubs–артефактах –> в рантайме был крэш с LinkageError.

P.S. Начиная с AGP 8.4 такой проблемы больше нет. На проекте был 8.3
👍2🔥1
кто бы мог подумать, что картинку спойлерить можно, а monospace и code block – нельзя
👍3
public class String

Вы могли видеть такую функцию:


public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}


Или такую:


public inline fun <T> mutableListOf(): MutableList<T> = ArrayList()


Что у них общего? Они обе на котлине, и обе объявлены с модификатором public

🤔 Зачем это нужно в лаконичном языке, где сущности и так public по умолчанию?

В Kotlin 1.4 появился опциональный флаг, который обязывает везде указывать модификатор доступа, а для публичного api еще и возвращаемый тип

Эта фича нужна разработчикам библиотек, чтобы лучше контролировать этот самый публичный api. Если вы нигде не можете пропустить модификатор (код попросту не скомпилируется), скорее всего вы укажете тот доступ, который и планировали. В результате будет гораздо меньше мест, где изменения могут что-то сломать у текущих клиентов

Кстати, про то, как никому ничего не сломать, на днях был доклад у @dolgo_polo_dev. И там на секции вопросов подняли тему, что делать, если у нас несколько модулей

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

У нас нет никакого модификатора, который был бы internal внутри library group, но есть специальная аннотация @RequiresOptIn. Она дает возможность сообщить пользователю, что api is not meant to be public. Например, так это выглядит в корутинах:


@Retention(value = AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS, AnnotationTarget.PROPERTY)
@RequiresOptIn(
level = RequiresOptIn.Level.ERROR, message = "This is an internal kotlinx.coroutines API that " +
"should not be used from outside of kotlinx.coroutines. No compatibility guarantees are provided. " +
"It is recommended to report your use-case of internal API to kotlinx.coroutines issue tracker, " +
"so stable API could be provided instead"
)
public annotation class InternalCoroutinesApi



@InternalCoroutinesApi
public fun tryResume(value: T, idempotent: Any? = null): Any?


Теперь, чтобы вызвать tryResume, нам нужно явно добавить либо @OptIn(InternalCoroutinesApi::class) (если мы хотим "проглотить" ошибку), либо @InternalCoroutinesApi (если хотим пробросить ее дальше). В обоих случаях мы по сути подписываемся, что согласны на нестабильное api – то есть, технически все эти сущности публичные, но есть нюанс

При этом кейворд public может быть полезен, даже когда вам не нужно предоставлять внешнее api

Но об этом уже в следующем посте

🎃 Everlasting Loops
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8🔥51
public final override

Помимо разработки библиотек, есть еще один кейс, где эсплицитный public несет дополнительную смысловую нагрузку


abstract class Animal {

protected abstract fun say(): String
}

abstract class Fox : Animal() {

public override fun say(): String {
return "Ring-ding-ding-ding-dingeringeding!"
}
}

class ArcticFox : Fox() {

// тут public уже не нужен
override fun say(): String {
return "Wa-pa-pa-pa-pa-pa-pow!"
}
}

class FennecFox : Fox() {

// тут тоже
override fun say(): String {
return "Chacha-chacha-chacha-chow!"
}
}


С точки зрения синтаксиса котлин такое позволяет, и тулинг не подсвечивает его как redundant

А еще в котлине есть ключевое слово final

Концептуально это очень похоже на предыдущий кейс, ибо и public, и final здесь выполняют одну и ту же функцию – нивелируют предыдущую эксплицитную декларацию: public можно использовать для того, чтобы перекрыть protected / internal, а final – для open / abstract


open class RedFox : Fox() {

final override fun say(): String {
return "A-hee-ahee ha-hee!"
}
}

class EuropeanRedFox : RedFox() {

// ошибка – “’say' in 'RedFox' is final and cannot be overridden”
override fun say(): String {
return "A-oo-oo-oo-ooo! Woo-oo-oo-ooo!"
}
}



@loops_everlasting
👍6🔥5🥱2🦄1