Proje ortasında React Native'den Flutter'a — dürüst hesap
Altı hafta, üç TestFlight beta, ödeme yapan altı merchant. Kasım 2024'te çalışan bir React Native uygulamasını Flutter'a yeniden yazdım — neler kırıldı, neye mal oldu, neyi tekrar yapardım.
Çalışan bir React Native uygulamamız vardı. Üç TestFlight beta, sekiz aylık emek, pilotta ödeme yapan altı merchant. Kasım 2024'te onu Flutter'a yeniden yazdım. Neden yaptığımı, neye mal olduğunu ve neyi tekrar yapacağımı anlatacağım — pişman olduğum kısımlar dahil.
Bu bir "Flutter mı, RN mi" yazısı değil. Çok okudum bu yazılardan; hepsi aynı dertten muzdarip: yazar size bir karar satıyor, gerçekte ne yaşandığını anlatmıyor. Aşağısı bizim kod tabanımızda, bizim kısıtlarımızla, bizim takvimimizde gerçekte yaşananlar.
Başladığımız yer
Stork, Türkçe bir kargo takip uygulaması. İlk sürüm React Native + Expo SDK 49 + TypeScript ile yazıldı. RN'i çoğu ekibin seçtiği sebepten seçtik: kurucu mühendis (ben) en iyi JavaScript biliyordu ve tek kod tabanından iOS-Android build çıkarmak, merchant'ların eline koyabileceğimiz bir TestFlight beta'ya giden en kısa yoldu.
Sekiz ay boyunca işini gördü. İlk üç TestFlight turu stabildi. Pilot merchant'lardan feedback topladık. Uygulama yapması gerekeni yapıyordu.
Sonra kırılmaya başladı.
Neler kırılmaya başladı
Üç somut acı noktası vardı, üçü de native modül kaynaklı:
Barkod tarayıcı. Paket kodlarını okumak için bir community camera kütüphanesi kullanıyorduk. Kütüphane çalışıyordu, ama her iOS minor güncellemesi kullandığımız deprecated API'lara dair bir release-note maddesi getiriyordu. Ekim 2024'e gelindiğinde iPhone 13 ve altında 200-300ms shutter gecikmesi vardı ve onu eleyemiyorduk. Bakıcı ilgiliydi ama yorgundu, fix sürekli "bir sonraki sürümü bekle" oluyordu.
Push notification yönlendirme. Uygulama foreground, background veya terminated durumdayken bildirimleri farklı yönlendiriyorduk. Expo notification kütüphanesi ilk iki durumu temiz halletti. Üçüncü durum — terminated app, bildirime dokun, doğru kargo detay ekranına in — her iki platformda da özel native kod istiyordu. Her Expo SDK upgrade'inde deeplink doğru yere düşecek mi diye nefesimi tutuyordum.
OS seviyesi background sync. Uygulama kapalıyken bile kargo durumlarının saatte bir yenilenmesini istiyorduk. iOS tarafında bu BGAppRefreshTask, Android tarafında WorkManager. İkisi de managed Expo'dan eject olmayı ve özel bir native modül beslemeyi gerektiriyordu. Dört ay erteledik; çünkü eject konuşması her seferinde açılıp tekrar kapanıyordu.
Problemler tek tek çözülebilirdi. Toplam problem, her sprint'in ciddi bir kısmının yak shave'lere gitmesiydi — modül güncellemeleri, versiyon pinleme, "bu Expo Go'da çalışıyor ama gerçek build'de çalışmıyor" hata ayıklaması. Çoğu ikişer gün sürüyordu. Hiçbiri yeni özellik üretmiyordu.
Devrilen hafta
21 Ekim 2024 haftası iOS 18.1 düştü. Beş gün içinde dört dependency'mizi kırdı. İkisi versiyon bump ile çözüldü. Biri fork gerektirdi. Dördüncüsü barkod kamera kütüphanesiydi ve gelen cevap "farkındayız, üzerinde çalışıyoruz" idi. Bu arada scanner'a bağımlı merchant'larımız vardı.
O hafta sonu scanner ekranını Flutter'da prototipledim. Login, scanner, kargo detay — üç ekran. Bir akşam aldı. Flutter mobile_scanner plugin'i ilk denemede shutter gecikmesi olmadan çalıştı. Laptopu kapattım, yürüyüşe çıktım. Eve döndüğümde karar verilmişti.
Neden spesifik olarak Flutter
Akıl yürütme konusunda spesifik olmak istiyorum, çünkü "Flutter daha iyi" başkasından kabul edeceğim bir argüman değil:
- Tek bir compilation target. Dart, AOT ile native'e derleniyor. JS bridge yok, Expo dev client yok, Hermes-vs-JSC tartışması yok. Geliştirmede çalışan production'da da çalışıyor. "Peki release build'de de çalışıyor mu?" konuşmaları sıfıra düştü.
- Aynı toolkit içinde hem Cupertino hem Material widget'ları. iOS uygulamasının iOS gibi, Android uygulamasının Android gibi hissetmesini istiyorduk. Flutter her iki widget ailesini de getiriyor. UI kütüphanesi vidalamak zorunda kalmadık.
- Release temposu ve ekosistem. Bu, moda olmayan kısım. Flutter'da daha az paket var ve çoğu Flutter ekibi ya da küçük, istikrarlı bir katkıcı grubu tarafından besleniyor. RN ekosisteminde daha fazla seçenek var ama daha fazla parçalanma da var. Biz seçenek istemiyorduk. Sıkıcı istiyorduk.
En sert savunacağım madde üçüncüsü. "Daha çok paket" bir avantaj gibi duruyor — taa ki bir Cuma akşamı, merchant'lar beklerken üç barkod scanner'dan birini seçmek zorunda kalana kadar.
Migrasyon gerçekte nasıl yürüdü
Kendime altı hafta verdim. Altı hafta sürdü.
Hafta 1 — spike ve ortam. Dev makineye Flutter kurulumu. iOS ve Android emülatörlerini ayağa kaldırma. Dikey bir dilim olarak login ve home ekranlarının port'lanması. Flutter karşılığı gereken dokuz RN paketini çıkardım ve riske göre sıraladım: barkod scanner, push bildirim, background sync, secure storage, file picker, image picker, deep link, biyometrik, in-app browser. İlk üçü beni endişelendirenlerdi.
Hafta 2-4 — ekran ekran yeniden yazım. Ekran sırasıyla değil, feature sırasıyla gittim. Önce authentication, sonra kargo listesi, sonra kargo detay, sonra scanner, sonra ayarlar. RN uygulamasını paralel çalıştırdım. Flutter versiyonunu, önemli yerlerde (CTA konumu, liste yoğunluğu) piksel piksel eşleştim; önemsiz yerlerde (geçiş eğrileri, splash süresi) eşlemedim.
// package.json — RN tarafı, acı veren kısımlar
{
"dependencies": {
"expo": "~49.0.0",
"expo-barcode-scanner": "~12.5.0",
"expo-notifications": "~0.20.0",
"expo-task-manager": "~11.3.0",
"@react-native-community/netinfo": "^9.3.10",
"react-native-reanimated": "~3.3.0",
"react-native-svg": "~13.9.0",
"react-native-mmkv": "^2.10.0"
}
}# pubspec.yaml — Flutter tarafı, karşılıkları
dependencies:
flutter:
sdk: flutter
mobile_scanner: ^5.2.3 # expo-barcode-scanner'ın 1:1 karşılığı
firebase_messaging: ^15.1.6 # expo-notifications routing'i yerine
workmanager: ^0.5.2 # expo-task-manager yerine
connectivity_plus: ^6.1.0 # @react-native-community/netinfo yerine
flutter_secure_storage: ^9.2.2 # react-native-mmkv yerineHafta 5 — zor paketler. Background sync ve push routing. Push routing beklediğimden kolay çıktı; Flutter firebase_messaging, terminated-app deeplink'lerini tek bir getInitialMessage() çağrısıyla hallediyor. RN karşılığı iOS ve Android için ayrı handler'lar gerektiriyordu.
En sık kırılan RN bridge modülü, kendi kargo sync background task'ımızdı. Kabaca şöyle görünüyordu:
// RN tarafı — sürekli kırılan bridge modülü
import { NativeModules, NativeEventEmitter } from "react-native";
const { ShipmentSync } = NativeModules;
const emitter = new NativeEventEmitter(ShipmentSync);
// Native taraftaki sync event'lerine abone ol. Sorun:
// her Expo SDK upgrade'i bir şeyi yeniden isimlendiriyor,
// ve event payload formatı sürümler arasında iki kez kaydı.
emitter.addListener("shipmentSyncDidFinish", (payload) => {
// payload.shipmentIds bazen array,
// bazen JSON string'di. Gerçek kod.
const ids = Array.isArray(payload.shipmentIds)
? payload.shipmentIds
: JSON.parse(payload.shipmentIds);
refreshLocalStore(ids);
});Flutter versiyonu, Workmanager().registerPeriodicTask(...)'a tek bir çağrı ve top-level bir callback'ten ibaretti. Bridge yok, payload kayması yok, event emitter yok:
// Flutter tarafı — 1:1 karşılık, daha temiz
@pragma("vm:entry-point")
void callbackDispatcher() {
Workmanager().executeTask((task, inputData) async {
// Background isolate'te çalışır. Sunucudan en güncel
// kargo durumlarını çeker, lokal DB'ye yazar.
final ids = await syncShipmentsFromServer();
await refreshLocalStore(ids);
return true;
});
}
void main() {
Workmanager().initialize(callbackDispatcher);
Workmanager().registerPeriodicTask(
"shipment-sync",
"syncShipments",
frequency: const Duration(hours: 1),
);
runApp(const StorkApp());
}Hafta 6 — App Store resubmission. Ayrı bir hikâye. Kısa versiyonu: Flutter binary ile review'dan geçmek, RN binary ile review'dan geçmekle yaklaşık aynı süreyi aldı. Apple toolkit'inizin ne olduğuyla ilgilenmiyor.
Migrasyon neye mal oldu
Saatte: altı hafta boyunca tek başına yaklaşık 240 saat. O haftaların bazılarında pilot merchant'lar için RN build'ini de besliyordum. Bu, her iki tarafı yavaşlattı.
Fırsat maliyetinde: shipleyebileceğim üç haftalık yeni özellik. Merchant pilot'un bir feature wishlist'i vardı; hiçbirini deliver etmeden migrasyonu deliver ettik.
Ekosistem maliyetinde: seçtiğim dokuz paketten ikisi hâlâ suboptimal. flutter_inappwebview çalışıyor ama bizim use case için aşırı ağır geliyor. Kullandığımız Türkçe adres autocomplete paketi tek bir kişi tarafından community-maintained, ki bu beni endişelendiriyor. İkisiyle de yaşıyorum.
Tekrar yapardım
Migrasyonun kendisi, evet. Spesifik olarak:
- Bu tür bir uygulama için — barkod scanner, push routing ve background sync içeren bir kargo takip UI'ı — yine Flutter'ı seçerdim.
- Yine solo yapardım. Bir rewrite'ta iki kişi tasarruf ettirdiğinden çok koordinasyon yükü üretiyor.
- Yine altı haftalık bütçe koyar ve şişirmezdim. Bütçe beni rewrite'ı over-engineer etmekten alıkoydu.
Pişman olduğum
Tek bir spesifik şey. Barkod scanner'ı haftaların ilk gününde — birinci haftanın birinci günü — Flutter'da prototiplemeliydim, üçüncü haftada değil. Sonunda çalıştı, ama focus ve shutter davranışını ayarlamak üçüncü haftada, daha az tamponum varken iki akşam aldı. Eğer scanner çalışmasaydı tüm migrasyon yanlış karar olurdu — ve bunu iki hafta geç öğrenirdim.
Bir dahaki sefere yazacağım genel kural şu: bir uygulamayı yeniden yazarken, kendini bağlamadan önce en riskli native akışı yeni toolkit'te prototiple. Sonra değil.
Sıkıcı nokta
İnsanlar hangisi daha iyi diye soruyor, RN mi Flutter mı, ve bu yanlış soru. Doğru soru şu: sizin spesifik uygulamanız için hangisi daha sıkıcı? "Sıkıcı" burada şu demek: hangi toolkit, toolkit'in kendini debug etmeye daha az, ürün shiplemeye daha çok zaman ayırmana izin veriyor.
Stork için, Kasım 2024'te, cevap Flutter'dı. Farklı bir ekip için — JS ağırlıklı, web-first, Next.js kod tabanında paylaşımlı logic'le — cevap hâlâ React Native olurdu.
Flutter versiyonunu pushladıktan üç gün sonra ekipten bir junior başını kaldırıp sordu: "Bir saniye, uygulama artık 1.2 saniyede mi yükleniyor?" Evet, dedim. Dart işte.
// madem buradasın
- 8 dk okuma
React Native, native driver ve nihayet hissedebildiğin jank
useNativeDriver sana ne zaman gerçekten bir şey kazandırır, ne zaman yalan söyler ve barkod ekranını yavaş hissettiren o düşen frame'i nasıl bulursun.
react-nativeperformancemobile - 8 dk okuma
Apple uygulamamı üç kez 'fazla Türkçe' diye reddetti
On bir gün, üç reject, bir lansman penceresi. Stork mobil uygulamasını App Store review'dan geçirme hikâyesi — ilk şikâyet 'dil İngilizce değil' idi.
mobileiosapp-storeinternationalization