Styling & Flexbox di Mobile — Mobile

Styling di React Native React Native tidak pakai CSS file. Semua styling melalui JavaScript objects via StyleSheet.create(). Propertinya mirip CSS tapi pakai…

Styling di React Native ditulis sebagai object JavaScript, bukan file CSS, dan layout-nya selalu flexbox. Bayangin papan nota di dinding toko Bu Rini. Nota ditempel dari atas ke bawah, satu per satu. Kalau mau beberapa nota berjajar ke samping, kamu harus bilang dulu "jajarkan". Di React Native, tiap View adalah papan seperti itu: anak-anaknya ditumpuk ke bawah (column), kecuali kamu menulis flexDirection: 'row'.

🧪 Tebakan cepat: Di baris order (OrderRow), nama di kiri dan badge status di kanan, dalam flexDirection: 'row'. Nama pelanggannya panjang, dan HP-nya selebar 360. Tanpa flex: 1 di bagian nama, apa yang terjadi dengan badge status? Jawabannya ada di bagian "Flexbox: default yang beda dari web".

StyleSheet: style sebagai object

const styles = StyleSheet.create({
  status: { fontSize: 12, lineHeight: 16, fontWeight: '600' },
});

<Text style={[styles.status, { color: '#1d4ed8' }]}>Disetrika</Text>

Satuan: dp, bukan piksel

Semua ukuran di React Native adalah dp (density-independent pixel), di iOS disebut point. 1 dp kira-kira sama besar di layar mana pun. HP Android dengan layar 720 × 1.600 piksel dan density 2 dilaporkan sebagai 360 × 800 dp. iPhone 15 punya layar 1.179 × 2.556 piksel dengan scale 3, jadi lebarnya 393 point.

Kamu menulis HP density 2 iPhone 15 (scale 3)
padding: 16 32 piksel fisik 48 piksel fisik
width: 96 192 piksel fisik 288 piksel fisik

PixelRatio.getPixelSizeForLayoutSize(16) menghitungnya untukmu: 48 di iPhone 15. Ukuran huruf juga ikut pengaturan teks di HP pengguna (fontScale), karena allowFontScaling di Text bawaannya true. Persen juga boleh untuk ukuran: width: '50%'.

Flexbox: default yang beda dari web

React Native menghitung layout dengan Yoga, mesin flexbox buatan Meta. Aturannya flexbox, tapi beberapa nilai awalnya berbeda:

Properti Web (CSS) React Native
display block, inline, dan lain-lain semua View sudah flex
flexDirection row column
flexShrink 1 (boleh mengecil) 0 (gak mengecil)
alignContent normal flex-start
position static relative
ukuran kotak content-box border-box (padding ikut dihitung dalam width)

flex: 1 di React Native artinya: tumbuh mengisi sisa ruang di sumbu utama induknya, dengan basis 0. Itu cara membuat elemen memenuhi ruang yang tersedia, misalnya layar { flex: 1 } di bawah header. height: '100%' mengambil tinggi penuh induknya tanpa peduli saudaranya, jadi bersama header dia kelebihan dan meluber.

Sekarang baris order di HP 360 dp. Padding kiri kanan 16, jarak (gap) 12, badge 96. Nama panjang kalau ditulis satu baris lebarnya, misalnya, 260 dp. Hasil hitungan Yoga:

Bagian nama Lebar nama Badge mulai di Badge berakhir di
tanpa flex 260 16 + 260 + 12 = 288 384, lewat 24 dp dari tepi layar
flex: 1 360 - 32 - 12 - 96 = 220 248 344, pas 16 dari tepi

Tanpa flex: 1, bagian nama gak mau mengecil (flexShrink 0), jadi lebarnya tetap 260 dan badge status terdorong keluar layar sejauh 24 dp. Itu jawaban tebakannya. Dengan flex: 1, bagian nama mengambil sisa ruang, 220 dp, dan namanya turun ke baris kedua. flexShrink: 1 juga menyelesaikannya.

OrderRow, angka per angka

// src/app/(tabs)/index.tsx
import { router } from 'expo-router';
import { FlatList, Pressable, StyleSheet, Text, View, useColorScheme, useWindowDimensions } from 'react-native';
import { HARGA_PER_KG, ORDERS, rupiah, type Order } from '@/data/orders';

const WARNA = {
  light: { latar: '#ffffff', teks: '#111827', redup: '#4b5563', garis: '#e5e7eb', badge: '#dbeafe', badgeTeks: '#1d4ed8' },
  dark: { latar: '#111827', teks: '#f9fafb', redup: '#9ca3af', garis: '#374151', badge: '#1e3a8a', badgeTeks: '#bfdbfe' },
};

function OrderRow({ order }: { order: Order }) {
  const w = WARNA[useColorScheme() === 'dark' ? 'dark' : 'light'];
  return (
    <Pressable
      style={[styles.row, { backgroundColor: w.latar, borderBottomColor: w.garis }]}
      onPress={() => router.push(`/order/${order.id}`)}>
      <View style={styles.info}>
        <Text style={[styles.nota, { color: w.teks }]}>#{order.id} {order.nama}</Text>
        <Text style={[styles.detail, { color: w.redup }]}>
          {order.kg} kg · {rupiah(order.kg * HARGA_PER_KG)}
        </Text>
      </View>
      <View style={[styles.badge, { backgroundColor: w.badge }]}>
        <Text style={[styles.status, { color: w.badgeTeks }]}>{order.status}</Text>
      </View>
    </Pressable>
  );
}

export default function Pesanan() {
  const { width } = useWindowDimensions();
  const kolom = width >= 600 ? 2 : 1;
  return (
    <FlatList
      key={kolom}
      numColumns={kolom}
      data={ORDERS}
      keyExtractor={(order) => order.id}
      renderItem={({ item }) => <OrderRow order={item} />}
    />
  );
}

const styles = StyleSheet.create({
  row: { flex: 1, flexDirection: 'row', alignItems: 'center', gap: 12, paddingHorizontal: 16, paddingVertical: 12, borderBottomWidth: 1 },
  info: { flex: 1 },
  nota: { fontSize: 16, lineHeight: 22, fontWeight: '600' },
  detail: { fontSize: 14, lineHeight: 20, marginTop: 2 },
  badge: { width: 96, paddingVertical: 4, borderRadius: 12, alignItems: 'center' },
  status: { fontSize: 12, lineHeight: 16, fontWeight: '600' },
});

Lebar layar: useWindowDimensions

useWindowDimensions() memberi { width, height, scale, fontScale } dan ikut berubah saat HP diputar atau jendela diubah ukurannya. Di tablet selebar 768, kolom jadi 2: tiap baris 384 dp, bagian namanya 244. Dimensions.get('window') yang dipanggil sekali di luar komponen gak ikut berubah.

numColumns gak boleh berubah di FlatList yang sama. Tanpa key={kolom}, saat HP diputar muncul error Changing numColumns on the fly is not supported. Change the key prop on FlatList when changing the number of columns to force a fresh render of the component. Dengan key, FlatList dibuat ulang.

Safe area: poni, Dynamic Island, dan garis home

Safe area adalah bagian layar yang gak tertutup status bar, poni atau Dynamic Island, dan garis home. Di Android, aplikasi Expo SDK 57 selalu digambar sampai tepi layar (edge-to-edge), jadi status bar juga menimpa aplikasimu. Header dari Stack dan Tabs sudah mengurusnya. Masalah muncul saat header disembunyikan:

// src/app/(tabs)/akun.tsx (tab ini memakai headerShown: false)
import { Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

export default function Akun() {
  const insets = useSafeAreaInsets();
  return (
    <View style={{ flex: 1, paddingTop: insets.top + 16, paddingHorizontal: 16 }}>
      <Text style={{ fontSize: 24, fontWeight: '700' }}>Halo, Mbak Wulan</Text>
    </View>
  );
}

Di iPhone dengan Dynamic Island, insets.top bernilai 59 dan insets.bottom 34, jadi sapaannya mulai di 75. react-native-safe-area-context versi 5.7.0 sudah ikut di Expo SDK 57, dan Expo Router sudah memasang SafeAreaProvider-nya. SafeAreaView dari react-native sudah deprecated dan memunculkan peringatan.

Mode gelap

⚠️ Jebakan yang sering ketemu

TL;DR: Style ditulis dengan StyleSheet.create: camelCase, angka tanpa satuan, gabungan [a, b] (yang belakang menang), tanpa cascade. Satuannya dp: padding: 16 jadi 32 piksel di HP density 2 dan 48 di iPhone 15. Flexbox React Native beda dari web: flexDirection bawaannya column, flexShrink 0, alignContent flex-start, position relative. flex: 1 membuat elemen mengisi ruang yang tersedia. Di OrderRow selebar 360, flex: 1 di bagian nama memberinya 220 dp; tanpa itu nama panjang mendorong badge 24 dp keluar layar. useWindowDimensions() untuk lebar yang bisa berubah (dua kolom mulai 600, dengan key), useSafeAreaInsets() saat header disembunyikan (59 di atas iPhone dengan Dynamic Island), dan useColorScheme() plus userInterfaceStyle: "automatic" untuk mode gelap.

Yang akan kamu pelajari