Irbisa · Шпаргалка
Shared-модуль KMP на практике
Ktor, kotlinx.serialization, SQLDelight, Koin, source set-ы, Gradle, multiplatform-тесты
commonMainотказывается компилироваться сjava.util.Dateиjava.io.File. Чем заменить JDK-типы, к которым тянется рука по привычке?import kotlin.time.Clockimport kotlin.time.TimeSourceimport kotlinx.coroutines.sync.Muteximport kotlinx.coroutines.sync.withLockimport kotlinx.datetime.TimeZoneimport kotlinx.datetime.toLocalDateTime…Что не так с этим shared-репозиторием и как это исправить?
class WeatherRepository { suspend fun forecast(city: String): Forecast { val client = HttpClient { install(ContentNegotiation) { json() } } return client.get("https://api.example.com/forecast") { parameter("city", city) }.body() }}Общий Ktor-клиент собирается, но на iOS падает, как только клиент начинают использовать. Что
HttpClientтребует от каждого таргета?// shared/build.gradle.kts: core in common, an engine in every leafkotlin { sourceSets { commonMain.dependencies { implementation(libs.ktor.client.core) implementation(libs.ktor.client.contentNegotiation)…Шлюз отдаёт HTML с кодом 502, и общий клиент падает внутри
body<User>(). Как в Ktor подключается JSON и где место этой ошибке?val appJson = Json { ignoreUnknownKeys = true } // the backend will add fieldssealed class ApiError : Exception() { data object Unauthorized : ApiError() data class Http(val status: Int) : ApiError() data object BadPayload : ApiError() data class Network(val reason: Throwable) : ApiError()…Найдите баг: после переезда API на
/v2лента по-прежнему грузится, а каждый запрос профиля получает 404.val client = HttpClient { install(ContentNegotiation) { json(appJson) } defaultRequest { url("https://api.example.com/v2/") contentType(ContentType.Application.Json) }} class AccountApi(private val client: HttpClient) { suspend fun feed(page: Int): List<Post> = client.get("feed") { parameter("page", page) }.body() suspend fun profile(): User = client.get("/users/me").body()}Что выведет этот код и почему бэкенд может отвергнуть такой payload?
@Serializabledata class Price(val amount: Long, val currency: String = "USD") { var note: String = "" val isFree: Boolean get() = amount == 0L} fun main() { val price = Price(5).apply { note = "sale" } println(Json.encodeToString(price))}Как в общем Ktor-клиенте подставлять bearer-токен и обновлять его по 401, не засветив его в логах и на чужих хостах?
// SessionStore: suspend read(): TokenPair?, write(TokenPair), clear()fun apiClient(engine: HttpClientEngine, store: SessionStore, apiHost: String) = HttpClient(engine) { install(ContentNegotiation) { json(appJson) } install(Logging) { level = LogLevel.HEADERS…В ленте перемешаны объекты post, ad и banner под одним ключом
type. Как разобрать это через kotlinx.serialization в общем коде?// Closed polymorphism: sealed + @Serializable, subclasses registered by the plugin// @JsonClassDiscriminator("kind") (experimental) changes the key for this hierarchy@Serializablesealed interface FeedItem { @Serializable @SerialName("post") data class Post(val id: Long, val body: String) : FeedItem @Serializable @SerialName("ad") data class Ad(val id: Long, val campaign: String) : FeedItem…Запрос SQLDelight мгновенный в JVM-тесте и роняет кадры на устройстве. Что делает драйвер каждой платформы и где выполняется сам запрос?
// commonMainexpect class DriverFactory { fun create(): SqlDriver } // androidMainactual class DriverFactory(private val context: Context) { actual fun create(): SqlDriver =…Вы добавили колонку в уже выпущенный
.sq-файл, и установленные приложения падают при старте. Как на самом деле работают миграции SQLDelight?// src/commonMain/sqldelight/com/example/db/Player.sq: the DESTINATION schema// CREATE TABLE player (// id INTEGER NOT NULL PRIMARY KEY,// name TEXT NOT NULL,// team_id INTEGER -- added in v2// );…Список на запросе SQLDelight не обновляется после записи. На что подписывается
asFlow()и что заставляет его переизлучать?class PlayerRepo( private val db: AppDatabase, private val io: CoroutineDispatcher = Dispatchers.IO,) { // asFlow() emits Query objects; mapToList runs them on the context you hand it. // Dispatchers.Main there would execute SQL on the UI thread.…Koin-модули лежат в
commonMain. Как iOS-приложение запускает этот граф и как Swift достаёт из него репозиторий?// commonMain: one graph, platform pieces behind an expectval sharedModule = module { single { httpClient() } single<FeedRepo> { FeedRepoImpl(get(), get()) } factory { FeedViewModel(get()) }}…Весь shared-модуль собирается в одном объекте с фабричными функциями, и коллега предлагает подключить Koin. Что реально даёт контейнер?
Где в KMP-приложении хранить auth-токен: в multiplatform-settings или за собственным интерфейсом поверх Keychain и Keystore?
Объявлений
expectуже больше десятка, и каждый новый target означает ещё одну папку сactual. Когда expect/actual становится неправильным инструментом?// wrong: everything as expect/actual (untestable, and a new target means new files everywhere)// commonMainexpect class AnalyticsTracker() { fun track(event: String, params: Map<String, String>)}// androidMain / iosMain: actual class AnalyticsTracker { ... }…Вы добавили target
watchosArm64, и код вappleMain, который вчера собирался, перестал компилироваться. Откуда вообще берутся source set-ы вродеiosMainиappleMain?kotlin { androidTarget() iosArm64() iosSimulatorArm64() watchosArm64() // <- adding this widens appleMain for everyone…Как доказать, что shared-репозиторий делает три попытки с backoff после 503, но тест при этом не висит семь настоящих секунд?
class RetryTest { @Test fun retriesThreeTimesWithBackoff() = runTest { var calls = 0 val waited = mutableListOf<Long>()…Найдите баг: во время инцидента на бэкенде у части клиентов за один заказ списали деньги два-три раза.
val client = HttpClient { install(ContentNegotiation) { json(appJson) } install(HttpRequestRetry) { retryOnExceptionOrServerErrors(maxRetries = 3) exponentialDelay() } defaultRequest { url("https://api.example.com/v2/") }} class PaymentApi(private val client: HttpClient) { suspend fun pay(orderId: String, amountCents: Long): Receipt = client.post("payments") { contentType(ContentType.Application.Json) setBody(PayRequest(orderId, amountCents)) }.body()}Падение внутри общего Kotlin-кода на Android читается как нормальный stack trace, а в iOS-отчёте это стена из hex. Что должно быть настроено на каждой стороне?
// commonMaininterface CrashReporter { fun log(message: String) fun setKey(key: String, value: String) fun record(error: Throwable) // non-fatal}…Две другие команды хотят зависеть от вашего shared-модуля. Что на самом деле кладёт в репозиторий
./gradlew publishи во что обойдётся ломающее изменение каждому потребителю?Добавленная библиотека прекрасно собирается под Android и падает на резолве для
iosArm64. Как поймать JVM-only зависимость заранее и какие есть варианты?