Strona główna / Artykuły / Czysty React Native, Expo czy Flutter: dopasowanie stacku mobilnego do Twojego zespołu

Czysty React Native, Expo czy Flutter: dopasowanie stacku mobilnego do Twojego zespołu

Praktyczne porównanie surowego React Native, Expo i Flutter: jak wyglądają w kodzie, gdzie każdy z nich ma słabe strony oraz pięciozadaniowy framework do wyboru między nimi.

2627 słów

Wybór uniwersalnego stacku mobilnego kiedyś polegał na znalezieniu kompromisu, z którym można było żyć. Dziś React Native, Expo i Flutter potrafią tworzyć aplikacje o wrażeniu natywnych, działające płynnie i skalujące się do bardzo dużych grup odbiorców, więc sama szybkość rzadko decyduje o wyborze. Decydujące jest dopasowanie: umiejętności już posiadane przez zespół, kod, który już się ma, oraz szybkość, z jaką trzeba dotrzeć do sklepów z aplikacjami. Ten artykuł traktuje Expo jako odrębną opcję, a nie jedynie uzupełnienie React Native, przedstawia praktyczne aspekty każdej z opcji i kończy się krótkim zestawem pytań, które można wykorzystać do dokonania wyboru.

Co zmieniło się we wszystkich trzech stackach

Kilka strukturalnych słabości, które wpływały na wcześniejsze porównania, zostało już wyeliminowanych:

  • Nowa architektura React Native jest domyślna. Odpowiada ona odstąpieniu od starszego, asynchronicznego mostu komunikacyjnego, dzięki czemu JavaScript i kod natywny mogą komunikować się bezpośrednio. W rezultacie wywołania do modułów natywnych są znacznie tańsze.
  • Wersja mobilna Fluttera używa domyślnie renderera Impeller zamiast Skii. Impeller kompiluje swoje shadery z góry, co eliminuje problem spowodowany kompilacją shaderów podczas pierwszego uruchomienia i zapewnia bardziej stałą szybkość odświeżania obrazu.
  • Expo stało się standardowym narzędziem do tworzenia aplikacji React Native. Nie jest już tylko środowiskiem testowym dla początkujących, lecz pełnowartościowym zestawem narzędzi używanym przez duże firmy.

Opublikowane testy wydajności zazwyczaj pokazują, że Flutter ma niewielką przewagę pod względem szybkości renderowania w interfejsach bogatych w animacje, natomiast React Native przewyższa go pod względem czasu uruchamiania, zużycia pamięci oraz operacji wejścia/wyjścia w środowisku natywnym. Trzeba podchodzić z ostrożnością do konkretnych liczb, ponieważ znacznie różnią się one w zależności od aplikacji i urządzenia. W przypadku większości produktów ta różnica już nie decyduje o ostatecznym wyniku. Jeśli chcesz poznać punkt widzenia zespołów, które od dłuższego czasu pracują z tymi rozwiązaniami, nasz artykuł na temat decyzji dotyczących Fluttera i React Native, które ujawniają się dopiero w środowisku produkcyjnym omawia długoterminowe aspekty.

Czysty React Native: maksymalna kontrola, maksymalna odpowiedzialność

React Native umożliwia pisanie interfejsu w JavaScript lub TypeScript z użyciem React, podczas gdy framework renderuje prawdziwe komponenty platformowe na iOS i Android. Nie ma WebView ani dostosowanego canvas: View staje się natywnym elementem wyświetlania, a Text natywnym komponentem tekstu.

Licznik poniżej pokazuje podstawową strukturę aplikacji w ramach Nowej Architektury z rendererem Fabric. Stan aplikacji jest przechowywany w hooku useState, przycisk jest elementem typu Pressable, a style są deklarowane raz za pomocą StyleSheet.create, aby można je było zweryfikować i ponownie wykorzystać.

// App.tsx — React Native (New Architecture, Fabric)
import React, { useState } from 'react';
import { View, Text, Pressable, StyleSheet } from 'react-native';

export default function App() {
  const [count, setCount] = useState(0);

  return (
    <View style={styles.container}>
      <Text style={styles.counter}>{count}</Text>
      <Pressable style={styles.button} onPress={() => setCount(c => c + 1)}>
        <Text style={styles.buttonText}>Tap me</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
  counter: { fontSize: 48, fontWeight: '700', marginBottom: 24 },
  button: { backgroundColor: '#2563eb', paddingHorizontal: 24, paddingVertical: 12, borderRadius: 12 },
  buttonText: { color: 'white', fontSize: 16, fontWeight: '600' },
});

W tym fragmencie nie ma nic specyficznego dla Nowej Architektury; ten sam kod komponentu działa w obu architekturach. Zmiana architektury ma miejsce w tle, w sposobie, w jaki renderer i moduły natywne komunikują się z JavaScriptem.

Zalety

  • Prawdziwe elementy interfejsu natywne. Otrzymujesz własne kontrolki platformy, zachowanie dostępności oraz renderowanie tekstu w oryginalnej formie, a nie ich aproksymację.
  • Największy zasób talentów. Umiejętności w JavaScript, TypeScript i React są znacznie powszechniejsze niż te potrzebne do pracy z innymi opcjami.
  • Pełny dostęp do środowiska natywnego. Masz pełną kontrolę nad katalogami ios/ i android/, więc możesz korzystać z Swift, Kotlin, Objective-C lub Java w dowolnym momencie, gdy to konieczne.
  • Ogromny ekosystem. npm oferuje więcej pakietów od stron trzecich niż pub.dev, a większość mobilnych SDK do płatności, map i analiz dostarcza oficjalne otoczenia typu React Native.
  • Szybsze wywołania do kodu natywnego. Fabric, TurboModules i JSI eliminują stary problem związany z asynchronicznym mostkiem komunikacyjnym, dzięki czemu wywołania do kodu natywnego są niemal natychmiastowe.
  • Dzielenie się kodem z przeglądarką. Dzięki React Native Web zespół posiadający już aplikację webową napisaną w React może ponownie wykorzystać dużą część jej logiki, a nawet niektóre komponenty.
  • Słabości

    • Jesteś odpowiedzialny za narzędzia do budowania aplikacji natywnych. Niespójności w wersjach Xcode, Gradle i CocoaPods oraz konflikty zależności natywnych nadal stanowią częsty problem.
    • Zachowanie spójności między platformami wymaga wysiłku. Ponieważ framework celowo mapuje się na elementy natywne, iOS i Android będą wyglądać inaczej, chyba że zaprojektujesz rozwiązanie zapewniające spójność.
    • Infrastruktura dostarczania aplikacji leży po twojej stronie. Budowa systemów CI/CD, podpisywania kodu oraz aktualizacji bezpośrednio w urządzeniu to zadanie nierzadko trudne, a właśnie tę lukę wypełnia Expo.
    • Nierówna jakość modułów od stron trzecich. Jakość konserwacji modułów natywnych waha się od doskonałej po całkowicie zaniedbaną.

    Praktyczne porady

    • react-native-reanimated i react-native-gesture-handler do interakcji wymagających wysokiej wydajności. One uruchamiają animacje i gesty na wątku UI, dzięki czemu zajęty wątek JavaScript nie powoduje utraty klatek.

    Expo: React Native z wbudowaną infrastrukturą

    Expo to framework oraz zestaw usług stworzony na bazie React Native. Jego dawna reputacja dotyczyła ograniczonego środowiska bez dostępu do kodu natywnego. To już nie jest prawdą: dzięki funkcji prebuild, zwanej również ciągłą generacją kodu natywnego (CNG), Expo może korzystać z całego ekosystemu modułów natywnych.

    Ekran poniżej wykorzystuje Expo Router, który mapuje pliki w katalogu app/ na trasy w taki sam sposób, jak to robi Next.js dla środowiska webowego. app/index.tsx to trasa domowa, a router.push('/profile') przenosi użytkownika do pliku definiującego trasę /profile. Głębokie linki oraz adresy URL z tego samego źródła.

    // app/index.tsx — Expo Router (file-based routing)
    import { View, Text, Pressable } from 'react-native';
    import { useRouter } from 'expo-router';
    
    export default function HomeScreen() {
      const router = useRouter();
    
      return (
        <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', gap: 16 }}>
          <Text style={{ fontSize: 24, fontWeight: '600' }}>Welcome</Text>
          <Pressable onPress={() => router.push('/profile')}>
            <Text style={{ color: '#2563eb' }}>Go to Profile</Text>
          </Pressable>
        </View>
      );
    }
    

    Stworzenie i uruchomienie projektu wymaga trzech poleceń. Pierwsze tworzy szkielet aplikacji, a npx expo start uruchamia serwer rozwojowy, dzięki czemu można otworzyć aplikację na urządzeniu lub symulatorze.

    # Spin up a new project in under a minute
    npx create-expo-app@latest my-app
    cd my-app
    npx expo start
    

    Zalety

    • Brak narzędzi natywnych do rozpoczęcia pracy. Przez większość czasu rozwoju nie potrzebujesz Xcode ani Android Studio; możesz testować na urządzeniu fizycznym za pomocą Expo Go lub dedykowanego klienta do rozwoju.
    • EAS Build. Budowa aplikacji dla iOS i Android odbywa się w chmurze z dowolnego systemu operacyjnego, więc możesz stworzyć wersję dla iOS na Windows lub Linux.
    • EAS Update. Aktualizacje przez sieć przesyłają zmiany w kodzie JavaScript i plikach użytkownikom natychmiast, bez konieczności czekania na weryfikację w sklepie, o ile nie ma zmian w kodzie natywnym.
    • Expo Router. Routing oparty na plikach z możliwością głębokiego łączenia oraz uniwersalną nawigacją dla aplikacji internetowych i natywnych jest dostępny od razu po instalacji.
  • Rozległe, starannie przygotowane SDK. Pakiety takie jak expo-camera, expo-notifications, expo-location i expo-image są zintegrowane, udokumentowane oraz dopasowane pod względem wersji, dzięki czemu mogą współpracować ze sobą.
  • Plugi konfiguracyjne. Można deklaratywnie zmieniać pliki projektu natively, takie jak Info.plist i AndroidManifest.xml, bezpośrednio z pliku app.json, zamiast edytować je ręcznie, co zapewnia powtarzalność procesów budowania w środowisku CI.
  • W głębi nadal jest to React Native. Wykonanie polecenia npx expo prebuild generuje foldery natively za każdym razem, gdy potrzebny jest pełny kontrola nad komponentami natively.
  • Słabości

    • Niszowe SDK wymagają dodatkowej pracy. Niektóre specjalistyczne, natywne SDK wciąż wymagają stworzenia własnego pluginu konfiguracyjnego lub modułu natywnego, co jest nieco bardziej pracochłonne niż w przypadku zwykłego React Native, chociaż ta różnica stale się zmniejsza.
    • Pułapka Expo Go. Zbyt duże poleganie na Expo Go może ukrywać fakt, że niestandardowy moduł natywny nie będzie działał, dopóki nie stworzysz wersji rozwojowej. To regularnie zaskakuje początkujących.
    • Koszty usług. EAS Build i Update oferują wersje bezpłatne, ale profesjonalne zespoły produkcyjne zazwyczaj przechodzą na płatne plany, co jest kosztem, którego unika zwykły React Native z samodzielnie zarządzanym CI.
    • Dziedziczone ograniczenia. Jako warstwa nad React Native, Expo zachowuje swoje wady: różnice w interfejsie użytkownika pomiędzy platformami oraz wątek JavaScript, który może stać się wąskim gardłem przy intensywnych obliczeniach.

    Praktyczne porady

    • Zainstaluj npx expo prebuild, gdy potrzebujesz modułu natywnego, którego nie obsługuje Expo. Narzędzie to tworzy foldery ios/ i android/ w momencie potrzeby, dzięki czemu kod natywny jest zawsze dostępny. Nasz artykuł pod adresem treating native folders as build output with Expo prebuild and CNG szczegółowo opisuje ten proces.
    • Wykorzystuj EAS Update do napraw błędów, a nie do dodawania funkcji zmieniających zachowanie aplikacji w wersji natywnej. Zasady sklepów z aplikacjami ograniczają to, co może zostać zmienione w pobranych plikach kodu, a wprowadzanie nowych funkcji pod pozorem aktualizacji OTA niesie ryzyko odrzucenia aplikacji; przed ich użyciem przeczytaj aktualne zasady Apple i Google.
    • Od samego początku nowego projektu używaj Expo Router. Modyfikacja istniejącej struktury nawigacji poprzez dodanie routingu opartego na plikach jest skomplikowana i trudna.
  • Uruchom expo doctor przed każdym budowaniem wersji. Dzięki temu wykrywa niespójności w zależnościach i wersjach, które w przeciwnym razie objawiałyby się jako tajemnicze błędy podczas budowania aplikacji w języku natywnym.
  • Flutter: pełna kontrola nad każdym pikselem

    Flutter obiera zupełnie inny sposób działania. Zamiast mapować elementy na widgety platformowe, sam rysuje całą interfejs za pomocą swojego silnika Impeller, a kod w języku Dart jest kompilowany z góry na kod maszynowy w formacie ARM lub x86.

    Licznik w języku Dart pokazany poniżej odpowiada przykładowi z React Native. MyApp otacza aplikację warstwą MaterialApp, CounterScreen jest obiektem typu StatefulWidget, którego klasa stanu przechowuje wartość _count; naciśnięcie przycisku wywołuje funkcję setState, która informuje Flutter o konieczności ponownego zbudowania tego poddrzewa z nową wartością.

    // main.dart
    import 'package:flutter/material.dart';
    
    void main() => runApp(const MyApp());
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return const MaterialApp(home: CounterScreen());
      }
    }
    
    class CounterScreen extends StatefulWidget {
      const CounterScreen({super.key});
    
      @override
      State<CounterScreen> createState() => _CounterScreenState();
    }
    
    class _CounterScreenState extends State<CounterScreen> {
      int _count = 0;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('$_count', style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold)),
                const SizedBox(height: 24),
                ElevatedButton(
                  onPressed: () => setState(() => _count++),
                  child: const Text('Tap me'),
                ),
              ],
            ),
          ),
        );
      }
    }
    

    Proces pracy w linii poleceń obejmuje cały cykl życia: tworzenie projektu, jego uruchamianie z funkcją hot reload podczas rozwoju oraz tworzenie plików do publikacji na Google Play (pakieta aplikacji) oraz w App Store (pliku IPA).

    flutter create my_app
    cd my_app
    flutter run          # hot reload in under a second
    flutter build appbundle --release   # Android
    flutter build ipa --release         # iOS
    

    Mocne strony

    • Tożsama interfejs użytkownika wszędzie. Ponieważ Flutter sam renderuje każdy widget, zachowanie i wygląd są identyczne na iOS, Android, w przeglądarce i na komputerze, bez specyficznych cech poszczególnych platform.
    • Dobra wydajność animacji. Dzięki wcześnie skompilowanym shaderom oraz bezpośredniemu dostępowi do GPU za pośrednictwem Impeller, Flutter zazwyczaj przewyższa inne rozwiązania pod względem szybkości odświeżania klatek w złożonych interfejsach bogatych w animacje.
    • Sześć celów z jednego kodu źródłowego. iOS, Android, przeglądarka, Windows, macOS i Linux – wszystkie mogą być budowane na podstawie tego samego kodu w języku Dart.
  • Wyśmienite narzędzia. DevTools, funkcja hot reload oraz spójny, dobrze udokumentowany katalog widgetów sprawiają, że codzienna praca nad oprogramowaniem jest przyjemna.
  • Brak interpretera w środowisku uruchomienia. Kompilacja z wyprzedzeniem do kodu maszynowego oznacza, że w czasie uruchomienia nie ma silnika JavaScript ani żadnego mostu komunikacyjnego.
  • Sprawdzone w dużych skali. Google je wspiera, a duże aplikacje produkcyjne takie jak Google Pay, aplikacje BMW oraz Xianyu od Alibaba je wykorzystują.
  • Słabości

    • Ekosystem Dart jest mniejszy. Znalezienie pracowników jest trudniejsze niż w przypadku JavaScript lub TypeScript, a nawet doświadczeni programiści zazwyczaj potrzebują kilku tygodni, aby stać się produktywni.
    • Może wydawać się nieco obcy. Ponieważ nie wykorzystuje widgetów platformy, uważni użytkownicy mogą zauważyć niewielkie różnice w zachowaniu, chociaż różnice te znacznie się zmniejszyły.
    • Większe pliki binarne. Silnik Flutter jest wbudowany we wszystkie aplikacje, więc pliki z tym silnikiem są zazwyczaj większe niż odpowiednie aplikacje React Native.
    • Mniej specjalistycznych pakietów. pub.dev jest dobry, ale mniej rozbudowany niż npm, szczególnie jeśli chodzi o narzędzia do obsługi specjalistycznych, lokalnych SDK.
    • Organizacja posiadająca już rozwiniętą bazę kodu React dla sieci nie ma nic, co mogłaby wykorzystać w tym środowisku.

    Praktyczne porady

    • Od pierwszego dnia włącz flutter analyze z rygorystycznymi regułami lintingu. Bezpieczeństwo typów null w Dart stanowi prawdziwą przewagę, ale tylko pod warunkiem unikania jego osłabiania poprzez użycie typów dynamic wszędzie.
    • Dla czegokolwiek więcej niż tylko prototypu wybierz metodę zarządzania stanem, taką jak Riverpod lub Bloc; sama funkcja setState nie nadaje się do skalowania rzeczywistej aplikacji.
    • Zanim stwierdzisz, że masz problem z wolnym działaniem aplikacji, sprawdź profil w widoku Performance narzędzi DevTools. Impeller już usunął większość problemów z kompilacją shaderów w przeszłości.
    • Jeśli strona internetowa ma duże znaczenie, przetestuj wcześnie wersję Flutter Web. Jego mechanizmy renderowania różnią się od tych w wersji mobilnej, a rozmiar pliku może być zaskakujący. W chwili pisania tego tekstu Flutter koncentruje się na rendererach opartych na CanvasKit, więc sprawdź aktualną dokumentację, jakie opcje są nadal obsługiwane.

    Pięć pytań, które pomogą podjąć decyzję

    Rozważaj je w kolejności. Zazwyczaj pierwsze pytanie z jasną odpowiedzią decyduje o wyborze.

    • Czy wasz zespół już pracuje z React i JavaScriptem? Jeśli tak, pozostań w ekosystemie React Native i używaj domyślnie Expo. Jeśli nie i możecie swobodnie wybierać, zarówno Flutter, jak i Expo są dobrymi opcjami; wybierz język, który zespół chciałby się nauczyć.
    • Czy potrzebujesz projektu identycznego piksel po pikselu lub zaawansowanych animacji dostosowanych pod konkretne potrzeby, jak w grach, narzędziach kreatywnych lub aplikacjach wymagających intensywnej wizualizacji? Flutter jest lepszym wyborem domyślnym.
    • Czy chcesz udostępnić kod lub komponenty istniejącej aplikacji webowej opartej na React? React Native, dzięki React Native Web, ma rzeczywłą przewagę; Flutter musi być implementowany od nowa w środowisku webowym.
    • Czy musisz wprowadzać poprawki oparte wyłącznie na JavaScript bez konieczności przeglądania przez sklep, lub tworzyć aplikacje iOS bez posiadania Maca? EAS Update i EAS Build od Expo rozwiązują obie te problemy bezpośrednio.
    • Czy wdrażasz ekrany wieloplatformowe w dużej istniejącej aplikacji natively zbudowanej? Czysty React Native lub funkcja dodawania elementów do aplikacji w Flutter lepiej pasują do takiego przypadku niż nowy projekt Expo.

    Zalecenia

    • Dla zespołu pracującego z React lub JavaScript, który tworzy nowe aplikacje, wybierz Expo. Eliminuje ono większość problemów związanych z React Native w zakresie narzędzi typu native, procesów CI/CD oraz aktualizacji OTA, jednocześnie pozwalając na korzystanie z pełnej mocy narzędzi typu native w momencie potrzeby.
    • Wybierz Flutter, gdy ważniejsza jest doskonałość interfejsu, wydajność animacji oraz dostępność na urządzeniach mobilnych, komputerach stacjonarnych i w przeglądarce niż możliwość ponownego wykorzystania ekosystemu JavaScript, albo gdy zespół nie ma wyraźnej preferencji językowej i rozpoczyna pracę od zera.
    • Zarezerwuj czysty React Native dla sytuacji, w których konieczne jest bezpośrednie zarządzanie projektami typu native – zazwyczaj przy dużym istniejącym kodzie natively lub nietypowych wymaganiach dotyczących integracji z tymi narzędziami.

    Podsumowanie

    Te trzy rozwiązania osiągnęły już wystarczająco wysoki poziom wydajności i dojrzałości, dzięki czemu możliwości frameworka rzadko stanowią wąskie gardło. Decydującymi czynnikami są osoby w twoim zespole, kod, który już posiadasz, oraz szybkość, z jaką musisz wprowadzić aplikację na rynek. Traktuj Expo jako standardowy sposób korzystania z React Native, zachowaj czysty React Native na przypadki, gdy kluczowe jest zarządzanie projektami natively, a wybierz Flutter, gdy kontrola nad renderowaniem i dostępność na wielu platformach przeważają nad zaletami pozostania przy JavaScript. Niezależnie od wyboru, sprawdź najbardziej ryzykowną część swojej aplikacji – czy to SDK natively, złożoną animację, czy wersję dla przeglądarki – już w pierwszych tygodniach, a nie w momencie wydania.