Kontekst wyboru – czym w ogóle jest „współdzielony kod” w mobile
Cross‑platform, multiplatform i shared code – trzy podobne, ale różne podejścia
Określenia cross‑platform, multiplatform i współdzielony kod często są wrzucane do jednego worka. W praktyce opisują różne strategie budowania aplikacji mobilnych, które mają działać na Androidzie, iOS i często także na innych platformach.
Cross‑platform (w ujęciu praktycznym) to najczęściej technologie takie jak Flutter czy React Native, gdzie dąży się do maksymalizacji wspólnego kodu – szczególnie w warstwie UI. Programista pisze jeden kod interfejsu i logiki, a framework uruchamia to na wielu platformach. Natywny kod sprowadza się głównie do wąskich mostków integracyjnych.
Multiplatform w wydaniu Kotlin Multiplatform (KMP) oznacza coś innego: wspólny jest rdzeń aplikacji (logika biznesowa, dane, modele, część infrastruktury), natomiast UI i część integracji pozostaje natywna – oddzielna dla Androida i iOS. Tutaj akcent pada na współdzielenie tego, co nie zależy od platformy, bez rezygnacji z natywnego wyglądu i zachowania.
Sam termin współdzielony kod jest szerszy. Oznacza każdą sytuację, w której logika aplikacji jest używana na więcej niż jednej platformie: od prostych modułów SDK, przez wspólne biblioteki domenowe, aż po pełne warstwy domain + data, z których korzystają oddzielne aplikacje Android i iOS.
Co faktycznie można współdzielić między Androidem a iOS
Najbardziej opłacalne do współdzielenia są obszary, które:
- zmieniają się często (logika biznesowa, feature flags, reguły walidacji),
- są skomplikowane (mechanizmy płatności, system reguł, silnik rekomendacji),
- są identyczne na każdej platformie (model danych, mapowanie JSON, komunikacja z backendem).
W praktyce w podejściu multiplatformowym zwykle współdzieli się:
- warstwę domenową (domain) – przypadki użycia, logikę biznesową, reguły, kalkulacje, walidacje,
- warstwę danych (data) – API klienckie, repozytoria, cachowanie, obsługę błędów,
- modele – DTO, encje, obiekty domenowe, struktury do serializacji/deserializacji,
- część infrastruktury – logowanie, konfiguracje, feature toggles, lokalne bazy danych.
Takie podejście stosuje Kotlin Multiplatform Mobile (KMM): logika biznesowa i dane są w jednym, wspólnym module, który kompiluje się na Androida i iOS. Wyżej znajduje się cienka, natywna warstwa prezentacji – Android korzysta z Jetpack Compose lub XML, iOS z UIKit/SwiftUI.
Elementy, które najczęściej zostają natywne
Nie wszystko opłaca się dzielić między platformami. Istnieją obszary, gdzie specyfika systemu operacyjnego jest na tyle istotna, że próba całkowitej abstrakcji tylko komplikuje projekt. Najczęściej natywne pozostają:
- UI i UX – style, animacje, nawigacja, zachowanie kontrolek, interakcje gestami,
- integracje sprzętowe – kamera, Bluetooth, NFC, czujniki, biometria,
- specyficzne integracje systemowe – powiadomienia push, tło, usługi lokalizacyjne, skróty i widżety systemowe,
- komponenty mocno związane z OS – płatności (Apple Pay / Google Pay), AppClips, Android App Links, dynamiczne moduły.
Flutter próbuje zunifikować warstwę UI, ale i tak w wielu obszarach (np. płatności, notyfikacje, deep linki) posługuje się natywnymi pluginami, więc z punktu widzenia projektu całkowita „ucieczka” od natywnego kodu wciąż jest iluzją.
Dlaczego firmy w ogóle szukają współdzielonego kodu
Najczęstsze motywacje przy wyborze między Kotlin Multiplatform a Flutterem są bardzo pragmatyczne:
- redukcja kosztów – jedna logika biznesowa zamiast dwóch niezależnych implementacji,
- spójność funkcjonalna – Android i iOS dostają te same reguły biznesowe, te same edge case’y, te same walidacje,
- krótszy time‑to‑market – nowy feature implementowany raz i szybko dostępny w obu aplikacjach,
- łatwiejsze utrzymanie – mniej rozjazdów wersji, mniej nieskoordynowanych hotfixów,
- re‑use na innych platformach – wykorzystanie tego samego modułu na backendzie, w webie lub w aplikacjach desktopowych.
Różnica między Kotlin Multiplatform a Flutterem sprowadza się głównie do tego, w której warstwie chcemy oszczędzać: KMP w warstwie domenowo‑danych, Flutter dodatkowo w warstwie UI.
Krótka charakterystyka Kotlin Multiplatform (KMP / KMM)
Idea: współdzielony core, natywne UI i integracje
Kotlin Multiplatform jest narzędziem firmy JetBrains, które pozwala pisać wspólne moduły w Kotlinie i kompilować je na różne platformy: JVM, Native (w tym iOS), JavaScript i inne. W obszarze mobile mówimy najczęściej o Kotlin Multiplatform Mobile (KMM), które łączy Androida (JVM) i iOS (Kotlin/Native).
Filozofia KMP jest prosta: wspólny core, natywne UI. Współdzielona jest warstwa logiki biznesowej, modelu danych, komunikacji z API i części infrastruktury. Android i iOS mają osobne projekty UI, ale korzystają z tego samego „silnika” aplikacji. Dzięki temu:
- Android nadal jest klasyczną aplikacją w Kotlinie (często Compose),
- iOS pozostaje standardowym projektem Xcode (Swift/Objective‑C + SwiftUI/UIKit),
- wspólny moduł KMM kompiluje się do .aar (Android) i .framework (iOS) i jest używany jak zwykła biblioteka.
Dla zespołu oznacza to, że nie trzeba wyrzucać obecnego kodu natywnego – można dorzucać współdzielone moduły stopniowo, np. zacząć od warstwy API i modeli, później przenieść logikę biznesową, a UI i integracje zostawić po stronie natywnej.
Jak działa kompilacja Kotlin Multiplatform
KMP jest kompilatorem wieloplatformowym. Z jednego kodu Kotlina powstają różne „artefakty”:
- JVM – bytecode dla Androida, backendu, desktopu,
- Kotlin/Native – binaria natywne dla iOS, macOS, Linux, Windows,
- JS – JavaScript do użycia w przeglądarce lub Node.js.
Kod multiplatformowy dzieli się na części:
- common – kod niezależny od platformy, używa tylko API dostępnego wszędzie,
- platform‑specific – używa dyrektyw expect/actual, czyli deklaruje abstrakcję w warstwie wspólnej (expect) i dostarcza konkretne implementacje na każdej platformie (actual).
Przykładowo: wspólny moduł może deklarować interfejs DeviceInfo z metodą deviceName() jako expect class DeviceInfo, a na Androidzie i iOS istnieją odpowiednie actual class DeviceInfo korzystające z natywnych API. Dzięki temu logika biznesowa nie zna szczegółów platformy, a jednak korzysta z natywnych możliwości.
Ekosystem Kotlin Multiplatform i najpopularniejsze biblioteki
Ekosystem Kotlin Multiplatform dynamicznie się rozwija. Coraz więcej bibliotek jest dostępnych jako multiplatform, co pozwala używać ich zarówno na Androidzie, jak i iOS w jednym wspólnym module. Najważniejsze przykłady:
- Ktor – multiplatformowy klient HTTP (oraz framework serwerowy),
- SQLDelight – obsługa baz danych z generowaniem typowanego Kodlina z SQL,
- kotlinx.serialization – serializacja JSON/CBOR/Protobuf w kodzie współdzielonym,
- kotlinx.coroutines – współbieżność oparta o coroutines, z integracją na JVM i Native,
- multiplatform settings – wspólny Key‑Value storage,
- różne biblioteki do DI, MVI/MVVM, logowania (np. Koin KMP, Napier).
Dzięki temu w praktycznym projekcie KMM można mieć jeden zestaw narzędzi dla API, storage’u, serializacji, logowania, zamiast dwóch równoległych implementacji (Kotlin/Java + Swift/Obj‑C).
Kiedy Kotlin Multiplatform sprawdza się najlepiej
Kotlin Multiplatform Mobile szczególnie dobrze pasuje do scenariuszy:
- istniejące aplikacje natywne – firma ma dojrzałe projekty Android i iOS i chce ograniczyć duplikację logiki, ale nie planuje rewolucji w UI,
- bogata domena biznesowa – mnogość reguł, procesów, workflow, która wymaga spójności i dobrego testowania,
- produkt „Android‑first” – silny zespół Android, który może pociągnąć wspólny core, a iOS „podpina się” pod gotowy moduł,
- projekt z planem na inne platformy – ten sam współdzielony moduł ma być używany na backendzie, desktopie czy w aplikacji webowej.
Przykładowy, realny wzorzec: zespół rozwijał aplikację tylko na Androida, z Mocną bazą w Kotlinie. Po decyzji o wejściu na iOS wyodrębniono logikę do modułu KMM, a iOS dostał natywne UI i bardzo cienką warstwę integracji z wspólnym core. Dzięki temu czas wejścia na iOS drastycznie się skrócił, a cała logika płatności, autoryzacji, reguł ofert była dzielona 1:1.

Krótka charakterystyka Fluttera
Architektura Fluttera: Dart i własny silnik renderujący
Flutter to framework od Google oparty na języku Dart, którego wyróżnikiem jest własny silnik renderujący (Skia). Zamiast korzystać z natywnych kontrolek Androida i iOS, Flutter rysuje UI samodzielnie – piksel po pikselu – w jednym, spójnym pipeline’ie.
Architektura Fluttera dzieli się na dwie główne warstwy:
- warstwa frameworka – komponenty UI (widżety), nawigacja, layout, animacje,
- silnik (engine) – napisany głównie w C++ i odpowiedzialny za renderowanie, obsługę wejścia, tekst, grafiki.
Z punktu widzenia programisty kluczowe jest to, że UI jest deklaratywny – definicje ekranów to drzewo widżetów w Dart, podobnie jak w React czy Jetpack Compose. Stan aplikacji determinuje wygląd, a framework dba o „różnicowanie” i aktualizację.
Write once, run anywhere – jedno UI i logika dla wielu platform
Flutter realizuje klasyczną obietnicę „write once, run anywhere” w warstwie UI i logiki. W jednym projekcie Dart powstaje aplikacja, którą można skompilować na:
- Android i iOS,
- web (Flutter Web),
- desktop (Windows, macOS, Linux).
Interfejs użytkownika, routingi, zarządzanie stanem, większość logiki biznesowej – wszystko to znajduje się we wspólnym kodzie Dart. Natywny kod ogranicza się do pluginów i cienkiej warstwy integracji, np. z API platformy (płatności, powiadomienia, geolokalizacja).
Dbanie o spójność między Androidem i iOS jest tu znacznie prostsze niż w podejściu z dwoma natywnymi UI – cała logika i ekrany istnieją tylko raz. Jednocześnie to właśnie jest główna różnica w filozofii względem Kotlin Multiplatform.
Najważniejsze atuty Fluttera z perspektywy projektu
W praktyce biznesowej Flutter wybierany jest z kilku powtarzających się powodów:
- szybki development – hot reload i hot restart umożliwiają natychmiastowe sprawdzanie zmian w UI i logice,
- spójny wygląd między platformami – jeden zestaw widżetów, ten sam toolchain, brak rozjazdów w drobnych detalach UI,
- bogaty zestaw komponentów – Material, Cupertino i mnóstwo gotowych widżetów na pub.dev,
- łatwość budowania skomplikowanych animacji – rozbudowane API animacji, hero transitions, custom painters,
- jeden stack technologiczny – zespół nie musi utrzymywać oddzielnych kompetencji Android i iOS.
Na poziomie produktu sprawdza się to szczególnie przy nowych projektach (greenfield), MVP, POC oraz aplikacjach rich‑UI, gdzie animacje, karty, listy i customowe komponenty są kluczowe dla doświadczenia użytkownika.
Typowe scenariusze użycia Fluttera
Flutter często wygrywa w sytuacjach, gdy:
- trzeba szybko zweryfikować produkt (MVP) i liczy się szybka iteracja z biznesem,
- projekt ma ograniczony budżet, a jednocześnie musi być dostępny na Androidzie i iOS,
- fundamentalne jest wrażenie wizualne – custom UI, animacje, płynne przejścia,
- planowany jest target szerszy niż mobile – ten sam kod ma trafić także na web lub desktop,
- zespół nie ma mocnych kompetencji natywnych i chce skupić się na jednym języku oraz jednym środowisku.
Dobrym przykładem są produkty B2B: panel administracyjny w formie weba + aplikacja mobilna dla pracowników terenowych. Flutter pozwala tu stworzyć wspólną bazę komponentów i logiki, a następnie wystawić różne „powierzchnie” (mobile, web) bez utrzymywania osobnych projektów pod każdą platformę. Podobnie w startupach – mały zespół 2–3 devów jest w stanie utrzymać pełen zestaw aplikacji mobilnych bez osobnych specjalistów od Androida i iOS.
Na osi „pełna kontrola nad natywnym UI” kontra „szybka wieloplatformowość” Kotlin Multiplatform i Flutter stoją po przeciwnych stronach. KMM faworyzuje sytuacje, gdy aplikacje natywne już istnieją lub muszą mocno respektować wytyczne platform (np. bankowość, rządowe e‑usługi), a najważniejsze jest ujednolicenie logiki w tle. Flutter lepiej służy tam, gdzie UI może być spójne i wspólne, a przewagą ma być tempo dostarczania funkcji i niższy koszt utrzymania jednego kodu produktowego.
W praktyce wybór między Kotlin Multiplatform a Flutterem często sprowadza się do tego, co jest już na pokładzie: zespół Android + istniejące aplikacje – zwykle wygrywa KMM; czysta kartka, mało ludzi, nacisk na szybki time‑to‑market – częściej Flutter. Dobrze przeprowadzona analiza potrzeb (jak duży udział ma logika biznesowa, ile pracy pochłonie UI, jakie są plany na web/desktop) pozwala dość szybko wskazać, które podejście będzie bardziej opłacalne w danym produkcie.
Jakość natywnej integracji: pluginy Fluttera kontra mosty KMM
Oba podejścia rozwiązują ten sam problem – dostęp do natywnych API systemu – ale robią to w zupełnie inny sposób. Przy projekcie wieloplatformowym szybko staje się to jednym z kluczowych kryteriów.
Jak Flutter komunikuje się z natywnym kodem
Flutter opiera się na platform channels, czyli kanałach komunikacyjnych między Dartem a natywnym kodem (Kotlin/Java na Androidzie, Swift/Obj‑C na iOS). Logika jest prosta: Dart wysyła komunikat (metoda + argumenty), a po drugiej stronie plugin wywołuje odpowiednie API i odsyła wynik.
W praktyce sprowadza się to do:
- definicji kanału po stronie Darta (np.
MethodChannel("com.example/device")), - implementacji handlera po stronie Android/iOS,
- ustalenia kontraktu (nazwy metod, typy danych, błędy) między obiema stronami.
Dla typowych funkcji – aparat, galeria, geolokalizacja, notyfikacje, płatności – zazwyczaj korzysta się z gotowych pluginów z pub.dev. Tworzenie customowego pluginu ma sens, gdy aplikacja ma specyficzne wymagania (np. integracja z wewnętrznym SDK banku, niestandardowy hardware).
Jak KMM korzysta z natywnych możliwości platform
Kotlin Multiplatform nie wymaga osobnego „mostu” – natywny kod jest częścią projektu. Wspólny moduł KMM może mieć expect deklaracje, a warstwy Android i iOS dostarczają actual implementacje korzystające bezpośrednio z natywnych frameworków. Na Androidzie są to klasy i biblioteki JVM, na iOS – API z frameworków iOS widziane jako wygenerowane interfejsy w Kotlin/Native.
Klasyczny schemat wygląda tak:
- w module wspólnym:
expect interface BiometricAuth { suspend fun authenticate(): Boolean }, - w module Android:
actual class BiometricAuthImplużywającyBiometricPrompt, - w module iOS:
actual class BiometricAuthImplowinięty wokółLAContext.
Z perspektywy logiki biznesowej wywoływana jest po prostu metoda interfejsu, bez świadomości, że pod spodem stoją dwa niezależne zestawy API.
Konsekwencje dla złożonych integracji
Przy prostych integracjach Flutter wygrywa prędkością – gotowy plugin, kilka linijek konfiguracji i temat zamknięty. Problemy pojawiają się, gdy:
- plugin jest niedojrzały lub przestaje być utrzymywany,
- API platformy zmienia się szybciej niż community pluginów,
- trzeba połączyć kilka bibliotek natywnych w jeden spójny moduł (np. monitoring + crash reporting + własne logowanie).
Wtedy zespół Flutterowy musi wejść w natywny kod i utrzymywać plugin samodzielnie. W małym projekcie to do przełknięcia, w rozbudowanym – staje się równoległym torem prac, bardzo podobnym do utrzymywania „prawdziwych” aplikacji natywnych.
W KMM te integracje i tak trzeba napisać osobno dla Androida i iOS, ale kod znajduje się bliżej biznesowej logiki, a nie w izolowanych pluginach. Zazwyczaj łatwiej zadbać o spójne kontrakty (interfejsy expect) i testy na poziomie modułu shared, nawet jeśli implementation różni się 180 stopni między platformami.
Doświadczenie użytkownika: płynność, wygląd, zachowanie
Różnica „własny silnik renderujący vs. natywne kontrolek” przekłada się bezpośrednio na UX. Dla jednych produktów jest to kosmetyka, dla innych – kwestia krytyczna.
Responsywność i wydajność animacji
Flutter od początku projektowano z myślą o płynnych animacjach w 60–120 FPS, dlatego:
- UI rysowany jest w jednym pipeline’ie Skia,
- wszystko jest przewidywalne, bo to ten sam silnik na Androidzie i iOS,
- narzędzia typu
Flutter DevToolspozwalają debugować jank, przepływ klatek, użycie pamięci.
Dobrze napisane aplikacje Flutterowe działają bardzo płynnie, szczególnie tam, gdzie dużo się dzieje na ekranie – karty, parallax, gesty, niebanalne przejścia. Ryzyko „poszatkowanego” UI pojawia się raczej wtedy, gdy logika ciężko obciąża główny wątek lub nadmiernie korzysta się z bridge’a do natywnego kodu.
W podejściu KMM płynność UI zależy wprost od tego, jak napisane są aplikacje natywne: na Androidzie – czy Compose/Views są poprawnie zbudowane, na iOS – czy layout i animacje w UIKit/SwiftUI nie blokują głównego wątku. KMM nie wprowadza tu własnych ograniczeń ani narzutu renderowania; to nadal „czysty” Android i iOS.
Look & feel: spójność vs. natywność
Flutter – z natury – faworyzuje spójność między platformami. Domyślnie aplikacja wygląda bardzo podobnie na Androidzie i iOS, chyba że celowo korzysta się z Cupertino widgets i odmiennych motywów. Dla wielu produktów to plus: jedna biblioteka design systemu, jeden zestaw komponentów, minimalizowanie niespodzianek.
Z drugiej strony aplikacje systemowe, bankowe, urzędowe czy nastawione na starszych użytkowników często muszą respektować przyzwyczajenia platformy: specyficzne gesty na iOS, wzorce nawigacji z Material Design na Androidzie, natywne pola formularzy, zachowanie przy cofnięciu. Lepiej odwzorowuje to UI natywny, pisany w Kotlinie/Java z Compose/Views i Swift/Obj‑C/SwiftUI.
Różnica jest odczuwalna także w detalach, jak:
- tekst i kerning (systemowy renderer vs. Skia),
- scroll z „odbiciem” na iOS,
- effekty overscroll i ripple na Androidzie,
- przejścia systemowe między ekranami, integracja z gestami systemu (np. „swipe to go back”).
W prostszych produktach B2C i aplikacjach konsumenckich użytkownicy zwykle nie zwracają na to uwagi. W aplikacjach, gdzie UI powinien „zlewać się” z systemem lub przechodzi audyty UX‑owe platform, rozbieżności stają się wyraźniejsze.

Produkt i organizacja: jak podejście wpływa na zespół
Decyzja „KMM czy Flutter” w praktyce jest decyzją o kształcie zespołu i procesów rozwojowych. Ten sam produkt może być tanio utrzymywany w Flutterze lub wygodniej rozwijany w KMM – zależnie od tego, kto ma siedzieć po drugiej stronie klawiatury.
Struktura zespołu i kompetencje
Flutter naturalnie promuje model „full‑stack mobile”: jeden zespół Dart/Flutter, który dostarcza aplikacje na Androida, iOS, ewentualnie web i desktop. Minimalizuje to zależności między zespołami i ułatwia rotację ludzi między projektami. Dla mniejszych firm oznacza to często „jedna rekrutacja zamiast dwóch”.
W organizacjach, które już posiadają silne działy Android i iOS, Flutter bywa trudniejszy do wpasowania. Nagle pojawia się trzeci stos technologiczny, a eksperci natywni wchodzą w rolę „dostawców pluginów” lub osób do zadań specjalnych – co rzadko jest satysfakcjonujące.
KMM lepiej wpisuje się w strukturę z dwoma zespołami natywnymi. Pojawia się dodatkowa warstwa – współdzielony moduł – której rozwój może prowadzić np. zespół Android (częsty scenariusz), a iOS aktywnie uczestniczy w projektowaniu API i dostarcza natywne implementacje actual. Oba zespoły dalej używają swoich narzędzi, ale pracują nad wspólną logiką domenową.
Cykl wydawniczy i koordynacja releasów
W Flutterze release mobile jest z zasady „jeden” – to ten sam kod, budowany na dwie platformy. Koordynacja jest prostsza, bo nie ma typowego dla projektów natywnych problemu, że Android wyprzedza iOS o dwie wersje (lub odwrotnie). To szczególnie istotne, gdy produkt wymaga równoczesnego wdrażania funkcji, np. przy kampaniach marketingowych.
W KMM można osiągnąć zbliżony efekt, ale wymaga to dyscypliny. Moduł współdzielony ma swoje wersje, aplikacje Android i iOS swoje. Gdy Android przechodzi na shared:1.5.0, iOS może nadal korzystać z 1.4.2. Z jednej strony daje to elastyczność (platformy nie muszą być identyczne co do dnia), z drugiej – komplikuje trackowanie regresji i planowanie wspólnych releasów.
Organizacyjnie wygląda to tak:
- Flutter: jedna tablica zadań, jedno repo (często monorepo), jeden pipeline CI/CD dla obu platform,
- KMM: przynajmniej dwa repo (moduł shared + aplikacje), kilka pipeline’ów CI/CD, osobne cykle releasowe, ale wspólna biblioteka domenowa.
Przy mniejszym zespole i ograniczonym budżecie prościej jest utrzymać setup Flutterowy. Przy większej organizacji z dojrzałym procesem QA na natywnych aplikacjach przewagę ma KMM, który nie przewraca całego ekosystemu narzędzi do góry nogami.
Doświadczenie deweloperskie: narzędzia, testy, debugowanie
Oba rozwiązania oferują dojrzałe środowiska pracy, ale różnice w codziennym workflow są odczuwalne, zwłaszcza dla zespołów przyzwyczajonych do jednego z nich.
IDE i narzędzia deweloperskie
Flutter gra głównie z Android Studio / IntelliJ IDEA i VS Code. Typowe benefity to:
- hot reload / hot restart,
- inspektory widżetów i layoutu,
- profilery wydajności i pamięci,
- bogaty ekosystem pluginów, w tym generatory kodu dla routingu, DI, modeli.
Deweloper, który raz się przyzwyczai do bardzo szybkiej iteracji UI, często nie chce wracać do tradycyjnego przebudowywania aplikacji po każdej zmianie w natywnym UI.
KMM korzysta głównie z IntelliJ IDEA / Android Studio po stronie współdzielonego modułu i z Xcode po stronie iOS. Powstaje więc naturalne rozdzielenie: Android dev najczęściej żyje w JetBrainsowym IDE, iOS dev – w Xcode. Dla czystego modułu KMM najwygodniejsza jest IDEA Ultimate z wsparciem dla multiplatform, choć KMM Gradle Plugin stopniowo poprawia doświadczenie także w innych wariantach.
Testowanie i debugowanie logiki
W Flutterze logika biznesowa jest pisaną w Darcie i testowaną testami unit i widget tests. Wiele frameworków architektonicznych (BLoC, Riverpod, MobX) ułatwia izolowanie logiki od UI oraz pisanie testów „bez ekranu”. Debugowanie to jeden proces – wkładka DevTools i stacktrace w Darcie.
W KMM testy logiki biznesowej można uruchamiać:
- na JVM – szybko i wygodnie, co pokrywa większość przypadków,
- na simulatorach/emulatorach iOS/Android, gdy testują też natywne integracje,
- z użyciem wspólnych bibliotek (kotlinx.coroutines, serialization), więc asercje dotyczą tego samego zachowania na obu platformach.
Debugowanie bywa podzielone: błędy w warstwie współdzielonej szuka się w JetBrainsowym IDE, błędy UI iOS – w Xcode, Android – w Android Studio. Z jednej strony zwiększa to złożoność, z drugiej – daje pełną moc natywnych narzędzi (profilery Instruments, Android Profiler, narzędzia do analizy pamięci specyficzne dla platform).
Architektura aplikacji: jak wygląda podział warstw
Architektura projektu różni się już na etapie pierwszego szkicu diagramu. Ten sam produkt narysowany „po Flutterowemu” i „po KMM” daje dwie różne mapy.
Typowa warstwa logiki w Flutterze
W Flutterze granica między UI a logiką jest stosunkowo cienka. Często stosuje się podejścia:
- BLoC / Cubit – strumienie zdarzeń i stanów zarządzane przez komponenty logiki,
- Provider lub Riverpod – iniekcja zależności i zarządzanie stanem przy pomocy providerów,
- Redux/MobX – globalny store lub reaktywne obserwowalne obiekty.
Warstwa API, cache, storage, reguły biznesowe zazwyczaj pozostają w tym samym repo, pisane w Darcie. Z punktu widzenia architekta jest to klasyczny monolit modularny z warstwami (presentation, domain, data), ale cały w jednym języku i na jednej platformie wykonawczej.
Layering w projekcie Kotlin Multiplatform
W KMM najczęściej wydziela się kilka poziomów:
- shared-domain – reguły biznesowe, use case’y, modele domeny,
- shared-data – klienci API (Ktor), repozytoria, serializacja, persystencja (np. SQLDelight),
- platform‑specific UI – osobne moduły aplikacji Android i iOS odpowiadające za ekran, nawigację, prezentację.
Między shared a UI często znajduje się cienka warstwa prezentacyjna, także wspólna – np. multiplatformowy MVI lub MVVM z wykorzystaniem StateFlow. Android i iOS wiążą się wtedy z tym samym „źródłem prawdy” stanu ekranu. Alternatywnie każdy zespół pisze własne ViewModel w natywnym stylu, opierając się jedynie na współdzielonej warstwie domain/data.
Dla rozbudowanych systemów (np. z backendem w Kotlinie, webem w Kotlin/JS) można rozszerzyć moduł shared na inne platformy, co jeszcze mocniej promuje architekturę „clean” z jasno wydzielonym rdzeniem domenowym.
Gdy taki rdzeń jest już obecny, dużo łatwiej utrzymać spójność reguł w różnych interfejsach: mobilnym, webowym, czasem nawet w narzędziach wewnętrznych. Konsekwencją jest też inne miejsce, w którym „mieszka” złożoność – w Flutterze duża część trudnych decyzji ląduje w warstwie prezentacji (stan, nawigacja, kompozycja widoków), w KMM znacznie większy ciężar przerzuca się do modułu współdzielonego i kontraktów między warstwami.
Dobrze widać tę różnicę przy zmianach w logice promocji czy koszyka. W Flutterze zaktualizowany jest kod Dartowy, a potem ewentualnie dostosowuje się layouty pod konkretne platformy. W KMM zmiana zaczyna się w module domain/data, a Android i iOS dostają nową wersję „silnika”, który podpinają pod swoje ekrany. Dla zespołów z mocną kulturą domenową i wymagającą analityką taki model bywa naturalniejszy, bo wymusza jasne granice i testy na poziomie use case’ów.
Inaczej rozkłada się też ciężar refactoringu. W Flutterze większa przebudowa architektury (np. przejście z BLoC na Riverpod, zmiana podejścia do routingu) dotyka od razu całości aplikacji, ale odbywa się w jednym repo i jednym języku. W KMM refaktoryzacja wspólnego modułu może być przeprowadzona centralnie i od razu przynosi zyski na obu platformach, natomiast każde większe przesunięcie granic odpowiedzialności między shared a UI wymaga zsynchronizowanego wysiłku zespołów Android i iOS.
Wreszcie, inaczej podchodzi się do eksperymentów. W Flutterze szybkie prototypowanie nowych flow produktowych – także na produkcji w formie testów A/B – jest prostsze, bo cała logika i UI żyją w jednym świecie. W KMM eksperymenty w domenie można wdrożyć równolegle na Androidzie i iOS, ale jeśli wymagają odmiennych doświadczeń wizualnych, trzeba je osobno zaprojektować i zaimplementować po obu stronach, korzystając z tych samych reguł w module współdzielonym.
Ostateczny wybór między Kotlin Multiplatform a Flutterem rzadko sprowadza się do pojedynczego argumentu technicznego. Zwykle wygrywa to podejście, które lepiej wpisuje się w istniejący ekosystem firmy: skład zespołu, historię technologii, oczekiwany czas życia produktu i gotowość do kompromisów między natywnością a tempem rozwoju. Dobrze przeprowadzona analiza tych czynników często mówi więcej niż porównywanie benchmarków czy listy funkcji frameworka.
Najczęściej zadawane pytania (FAQ)
Kotlin Multiplatform czy Flutter – co lepiej wybrać do aplikacji mobilnej?
Jeśli chcesz współdzielić głównie logikę biznesową i warstwę danych, a zostawić natywne UI na Androidzie i iOS, lepszym wyborem zwykle jest Kotlin Multiplatform (KMM). Sprawdza się szczególnie tam, gdzie istnieją już projekty natywne i chcesz ograniczyć duplikację kodu bez przepisywania całego frontu.
Flutter ma przewagę, gdy zależy ci na maksymalnym współdzieleniu – łącznie z interfejsem użytkownika. Piszesz jedną aplikację z jednym UI i jednym zestawem logiki, a framework generuje wersje na Androida i iOS. Ceną jest jednak mniejsza „natywność” wrażenia z używania aplikacji oraz dodatkowa warstwa technologiczna w projekcie.
Co konkretnie można współdzielić między Androidem a iOS w Kotlin Multiplatform?
W Kotlin Multiplatform najczęściej współdzieli się to, co jest identyczne lub bardzo zbliżone na obu platformach: logikę biznesową, modele danych i komunikację z backendem. Do jednego modułu trafiają przypadki użycia, walidacje, reguły biznesowe, obsługa błędów, repozytoria, cache oraz serializacja JSON.
UI, nawigacja ekranów, animacje czy integracje sprzętowe (kamera, NFC, biometria) pozostają zwykle natywne. Dzięki temu Android może używać Jetpack Compose, a iOS SwiftUI lub UIKit, ale oba systemy korzystają z tego samego „silnika” domenowo–danych.
Czy Flutter całkowicie eliminuje potrzebę pisania natywnego kodu?
Nie. Flutter minimalizuje natywny kod w warstwie UI, ale wciąż opiera się na pluginach, które pod spodem korzystają z natywnych API. Każda bardziej zaawansowana integracja z systemem (płatności, notyfikacje, deep linki, integracje z OS) wymaga mostków do Androida i iOS.
W prostych aplikacjach biznesowych da się długo funkcjonować bez dotykania natywnego kodu. W produktach korzystających intensywnie z możliwości urządzenia (kamera, BLE, geolokalizacja w tle, widgety) prędzej czy później trzeba wejść w natywne warstwy – samodzielnie lub przez dopracowane pluginy.
Kiedy Kotlin Multiplatform ma większy sens niż Flutter?
Kotlin Multiplatform jest szczególnie sensowny, gdy:
- masz już działające aplikacje natywne na Androida i iOS i chcesz zmniejszyć dług technologiczny bez ich przepisywania,
- produkt ma rozbudowaną domenę (dużo reguł, workflow, integracji z backendem), a UI jest stosunkowo prosty i mocno osadzony w standardach każdej platformy,
- zespół Android jest silny, a po stronie iOS brakuje mocy – łatwiej wtedy „wyciągnąć” logikę do wspólnego modułu, niż narzucać nowy framework wszystkim.
Przykładowo: bankowość mobilna, aplikacje finansowe, systemy rezerwacyjne – tam, gdzie zmieniają się głównie reguły biznesowe, a nie sam wygląd ekranów.
Czy Flutter lepiej nadaje się dla małych zespołów i startupów?
W wielu przypadkach tak. Flutter pozwala małemu zespołowi (a nawet jednej osobie) szybko stworzyć aplikację na Androida i iOS z jednym kodem UI i logiki. Time‑to‑market jest często krótszy niż przy równoległym rozwijaniu dwóch aplikacji natywnych lub budowaniu od zera architektury KMM.
Minusem jest to, że cała inwestycja idzie w technologię, która nie jest natywnym standardem żadnej z platform. Jeśli po kilku latach zdecydujesz się na natywne aplikacje, migracja będzie oznaczała przepisywanie frontu. W KMM przeżywa przynajmniej wspólna warstwa domenowo‑danych.
Jakie są główne różnice w architekturze aplikacji: Flutter vs Kotlin Multiplatform?
W Flutterze zwykle mamy jedną aplikację, w której UI, logika prezentacji i logika biznesowa są pisane w Dart. Projekt ma jedną bazę kodu i jeden pipeline CI/CD, a pod spodem powstają binaria na różne platformy.
W Kotlin Multiplatform architektura jest rozdzielona: wspólny moduł (domain + data + modele) plus dwie aplikacje natywne (Android i iOS) korzystające z tego modułu jak z biblioteki. To bardziej zbliżone do klasycznego świata natywnego, tylko z dodatkową warstwą współdzielonego core’u.
Czy można używać Kotlin Multiplatform także poza mobile, np. w backendzie lub webie?
Tak. Kotlin Multiplatform kompiluje się nie tylko na Androida i iOS, ale również na JVM (backend, desktop) i JavaScript (przeglądarka, Node.js). Ten sam moduł z logiką domenową może być użyty jednocześnie w aplikacji mobilnej, serwerze i panelu webowym.
To podejście jest szczególnie atrakcyjne, gdy backend jest już w Kotlinie – można dzielić modele, reguły walidacji czy obsługę protokołów między serwerem a klientem mobilnym, co znacząco redukuje liczbę rozjazdów między platformami.
Najważniejsze wnioski
- „Współdzielony kod” można rozumieć na kilka sposobów: cross‑platform (Flutter, React Native) maksymalizuje wspólny kod łącznie z UI, multiplatform (Kotlin Multiplatform) dzieli głównie logikę i dane, a pojęcie shared code obejmuje każdy scenariusz, w którym ta sama logika działa na wielu platformach.
- Kotlin Multiplatform (KMM) stawia na wspólny core (domain + data + część infrastruktury), pozostawiając interfejs i integracje sprzętowo‑systemowe po stronie natywnej; Flutter próbuje unifikować także warstwę prezentacji, ale i tak opiera się na natywnych pluginach dla wielu funkcji.
- Największy zwrot z inwestycji we współdzielony kod daje przeniesienie elementów częstych w zmianach, złożonych lub identycznych na każdej platformie: logiki biznesowej, modeli danych, komunikacji z backendem, cachowania, obsługi błędów czy feature toggles.
- UI, zaawansowane integracje z OS (powiadomienia, tło, lokalizacja, widżety) oraz funkcje mocno zależne od platformy (płatności systemowe, AppClips, Android App Links) zwykle opłaca się zostawić jako natywne – próba pełnej abstrakcji najczęściej podnosi koszt i złożoność projektu.
- Główna różnica strategiczna: KMP oszczędza głównie w warstwie domenowo‑danych, zachowując „prawdziwe” aplikacje natywne na Androida i iOS, natomiast Flutter celuje w dodatkową oszczędność na UI, kosztem odejścia od natywnego wyglądu i zachowania kontrolek.
Źródła informacji
- Kotlin Multiplatform Mobile Documentation. JetBrains – Oficjalny opis KMP/KMM, architektura, targety JVM/Native/JS
- Kotlin Multiplatform Overview. JetBrains – Filozofia wspólnego core, expect/actual, podział common vs platform
- SQLDelight Documentation. Cash App – Biblioteka SQLDelight dla baz danych w Kotlin Multiplatform
- Flutter Documentation: Introduction to Flutter. Google – Opis Fluttera jako frameworka cross‑platform z jednym kodem UI
- SwiftUI Essentials. Apple – Oficjalny opis SwiftUI jako natywnego frameworka UI dla iOS





