Opbygning af et responsivt kortgitter
Anvend Flexbox-ombrydning og procentbaserede bredder til at oprette et kortgitter med to kolonner, der ser korrekt ud på både små telefoner og store tablets.
Opbygning af et responsivt kortgitter er en gratis React Native Academy-lektion på CoddyKit. Dette er lektion 4 af 4. Du kan læse alle 3 lektioner i dette læringsspor gratis i deres fulde længde — derefter låser CoddyKit PRO alle lektioner op samt praktiske øvelser med en indbygget kodeeditor og en AI-underviser døgnet rundt. Den er en del af læringsforløbet i React Native Academy, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. React Native Academy-kurset indeholder 4 lektioner i alt.
Oversigt over kortgitterets arkitektur
Et kortgitter er et af de mest almindelige UI-mønstre i mobilapps — fra produktkataloger til fotogallerier og app-dashboard. I React Native opbygger du det med en kombination af flexDirection: 'row', flexWrap: 'wrap', procentværdier for width og egenskaben gap. Udfordringen er at få gitteret til at se korrekt ud på små iPhones (320–375 pt brede), store Android-telefoner (392–430 pt) og tablets (768 pt+). Ved at beregne kortdimensionerne ud fra skærmbredden tilpasser gitteret sig automatisk til alle enhedsstørrelser.
// The pattern: percentage-width cards in a wrapping row
import { View, useWindowDimensions } from 'react-native';
const COLUMNS = 2;
const GAP = 12;
const PADDING = 16;
export default function CardGrid({ items }) {
const { width } = useWindowDimensions();
const cardWidth = (width - PADDING * 2 - GAP * (COLUMNS - 1)) / COLUMNS;
return (
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: GAP, padding: PADDING }}>
{items.map(item => <Card key={item.id} width={cardWidth} data={item} />)}
</View>
);
}Præcis beregning af kortbredde
Hvis du vil have præcis N kolonner med ensartede mellemrum, skal du beregne kortbredden sådan: (containerWidth - horizontalPadding * 2 - gap * (N - 1)) / N. For to kolonner med 16 pt indvendig margen på hver side og 12 pt mellemrum: (screenWidth - 32 - 12) / 2. Denne formel sikrer, at kortene og mellemrummene tilsammen præcis svarer til containerens bredde, så du får et pixelperfekt gitter uden afrundingsfejl. Når du bruger useWindowDimensions, beregnes dette igen, når skærmbredden ændres (f.eks. ved rotation af enheden eller i iPads delt visning).
import { useWindowDimensions } from 'react-native';
function useGridLayout({ columns = 2, padding = 16, gap = 12 } = {}) {
const { width } = useWindowDimensions();
const cardWidth = (width - padding * 2 - gap * (columns - 1)) / columns;
return { cardWidth, numColumns: columns };
}
// Usage:
function MyGrid() {
const { cardWidth } = useGridLayout({ columns: 2, padding: 16, gap: 12 });
// cardWidth updates automatically on rotation
return <>{/* render cards with width: cardWidth */}</>;
}Opbygning af kortkomponenten
Hvert kort i gitteret er en selvstændig komponent med et billede, en titel og en valgfri undertitel. Brug borderRadius til afrundede hjørner, en skygge eller elevation for dybde og overflow: 'hidden' for at beskære billedet efter kortets form. Billedets højde angives med aspectRatio, så den skaleres proportionalt med kortets bredde. Modtag kortets bredde som en prop fra gitterets overordnede komponent — kortet behøver ikke kende skærmen; det udfylder blot den bredde, det får.
import { View, Text, Image, StyleSheet } from 'react-native';
export default function ProductCard({ item, width }) {
return (
<View style={[styles.card, { width }]}>
<Image
source={{ uri: item.imageUri }}
style={styles.image}
/>
<View style={styles.info}>
<Text style={styles.title} numberOfLines={2}>{item.name}</Text>
<Text style={styles.price}>${item.price.toFixed(2)}</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
card: { backgroundColor: '#fff', borderRadius: 14, overflow: 'hidden', elevation: 3, shadowColor: '#000', shadowOffset: {width:0,height:2}, shadowOpacity:0.08, shadowRadius:8 },
image: { width: '100%', aspectRatio: 1, backgroundColor: '#f0f0f0' },
info: { padding: 10 },
title: { fontSize: 13, fontWeight: '600', color: '#222', lineHeight: 18 },
price: { fontSize: 15, fontWeight: 'bold', color: '#4f86f7', marginTop: 4 },
});FlatList med numColumns til gitre
Til store datasæt skal du bruge FlatList med propen numColumns i stedet for en række med ombrydning. FlatList virtualiserer gengivelsen — kun kort, der er synlige på skærmen, ligger i hukommelsen, hvilket giver jævn rulning med tusindvis af elementer. Angiv numColumns={2}, og angiv en columnWrapperStyle for ensartet afstand mellem rækkerne. Propen key fra keyExtractor er afgørende for ydeevnen. Ulempen er, at ændring af numColumns under kørsel kræver, at listen afmonteres og monteres igen — brug det samme antal kolonner under hele sessionen.
import { FlatList, useWindowDimensions } from 'react-native';
import ProductCard from './ProductCard';
export default function ProductGrid({ products }) {
const { width } = useWindowDimensions();
const cardWidth = (width - 32 - 12) / 2;
return (
<FlatList
data={products}
numColumns={2}
keyExtractor={item => item.id.toString()}
renderItem={({ item }) => (
<ProductCard item={item} width={cardWidth} />
)}
columnWrapperStyle={{ gap: 12, paddingHorizontal: 16 }}
contentContainerStyle={{ paddingVertical: 16, gap: 12 }}
showsVerticalScrollIndicator={false}
/>
);
}Håndtering af ulige antal elementer (sidste række)
Når dit gitter har et ulige antal elementer, vil den sidste række have et kort uden et tilsvarende søskendeelement. Som standard strækker dette enlige kort sig, så det udfylder hele rækkens bredde (på grund af Flexbox-strækning). Hvis du vil holde det fritstående kort i samme bredde som de andre kort, skal du give hvert kort en fast, beregnet bredde i stedet for at stole på Flexbox-strækning. I FlatList kan du også tilføje et gennemsigtigt afstandselement for at udfylde den sidste række. En anden mulighed er ListFooterComponent med en fast højde, så det sidste kort ikke ser ud til at være strakt nær bunden af skærmen.
import { FlatList, View } from 'react-native';
import ProductCard from './ProductCard';
// Add a transparent placeholder if odd count
function padToEven(data) {
if (data.length % 2 !== 0) {
return [...data, { id: '__placeholder__', _placeholder: true }];
}
return data;
}
export default function EvenGrid({ products, cardWidth }) {
const paddedData = padToEven(products);
return (
<FlatList
data={paddedData}
numColumns={2}
keyExtractor={item => item.id}
renderItem={({ item }) => (
item._placeholder
? <View style={{ width: cardWidth }} />
: <ProductCard item={item} width={cardWidth} />
)}
columnWrapperStyle={{ gap: 12, paddingHorizontal: 16 }}
/>
);
}Kategorioverhoved over gitteret
Gitre i rigtige apps viser ofte et kategorioverhoved — en titel, et link med teksten 'Se alle' og eventuelt vandrette filterknapper — over produktkortene. Den mest fleksible tilgang er at bruge propen ListHeaderComponent i FlatList, som gengiver indholdet over det rullelige indhold og ruller sammen med det. Det holder overhovedet inde i den virtualiserede rulning uden behov for en separat ScrollView-wrapper. Til flere kategorier med deres egne gitre skal du bruge SectionList med sektionsoverskrifter og en tilpasset renderSectionHeader.
import { FlatList, View, Text, TouchableOpacity, StyleSheet } from 'react-native';
function GridHeader({ title, onSeeAll }) {
return (
<View style={styles.header}>
<Text style={styles.title}>{title}</Text>
<TouchableOpacity onPress={onSeeAll}>
<Text style={styles.seeAll}>See all</Text>
</TouchableOpacity>
</View>
);
}
// Use as:
<FlatList
ListHeaderComponent={<GridHeader title='Popular' onSeeAll={() => {}} />}
data={products}
numColumns={2}
// ...
/>
const styles = StyleSheet.create({
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 12 },
title: { fontSize: 20, fontWeight: 'bold', color: '#111' },
seeAll: { fontSize: 14, color: '#4f86f7' },
});Adaptive kolonner til tablets
Hvis du vil have et gitter, der viser 2 kolonner på telefoner og 3 eller 4 på tablets, skal du beregne antallet af kolonner dynamisk ud fra skærmbredden. Da numColumns i FlatList ikke kan ændres efter montering, er den sikreste tilgang at inkludere kolonneantallet i FlatList's prop key: key={numColumns.toString()}. Når antallet af kolonner ændres (f.eks. når en tablet drejes til liggende retning), afmonterer FlatList listen og monterer den igen med det nye layout. Dette er en kendt begrænsning — accepter den korte genmontering for at få et korrekt adaptivt gitter.
import { FlatList, useWindowDimensions } from 'react-native';
import ProductCard from './ProductCard';
export default function AdaptiveGrid({ products }) {
const { width } = useWindowDimensions();
const numColumns = width >= 768 ? 3 : 2;
const gap = 12, padding = 16;
const cardWidth = (width - padding * 2 - gap * (numColumns - 1)) / numColumns;
return (
<FlatList
key={numColumns.toString()} // remount on column change
data={products}
numColumns={numColumns}
keyExtractor={item => item.id}
renderItem={({ item }) => <ProductCard item={item} width={cardWidth} />}
columnWrapperStyle={{ gap, paddingHorizontal: padding }}
contentContainerStyle={{ paddingVertical: 16, gap }}
/>
);
}Indlæsningsgitter med skelet
Mens produktdata indlæses fra en API, skal du vise et skeletraster med pladsholderkort for at undgå en tom, hvid skærm. Brug et fast array med pladsholderelementer (f.eks. 6 elementer), og vis skeletkortene i det samme rasterlayout. Animer dem med pulserende opacitet ved hjælp af Animated API for at skabe en poleret shimmer-effekt. Når de rigtige data ankommer, skal du erstatte skelettet med de faktiske elementer. Overgangen fra skelet til rigtigt indhold skal ske øjeblikkeligt — uden fade — for at undgå visuel forvirring om, hvorvidt indholdet er blevet ændret.
import { View, StyleSheet } from 'react-native';
function SkeletonCard({ width }) {
return (
<View style={[styles.card, { width }]}>
<View style={styles.imgSkeleton} />
<View style={styles.info}>
<View style={styles.titleSkeleton} />
<View style={styles.priceSkeleton} />
</View>
</View>
);
}
const PLACEHOLDER_COUNT = 6;
export default function SkeletonGrid({ cardWidth }) {
return (
<View style={styles.grid}>
{Array.from({ length: PLACEHOLDER_COUNT }).map((_, i) => (
<SkeletonCard key={i} width={cardWidth} />
))}
</View>
);
}
const styles = StyleSheet.create({
grid: { flexDirection: 'row', flexWrap: 'wrap', gap: 12, padding: 16 },
card: { borderRadius: 14, overflow: 'hidden', backgroundColor: '#fff', elevation: 2 },
imgSkeleton: { width: '100%', aspectRatio: 1, backgroundColor: '#e0e0e0' },
info: { padding: 10, gap: 6 },
titleSkeleton: { height: 14, backgroundColor: '#e0e0e0', borderRadius: 4, width: '80%' },
priceSkeleton: { height: 14, backgroundColor: '#e0e0e0', borderRadius: 4, width: '40%' },
});Træk for at opdatere i rasteret
Tilføj træk for at opdatere i rasteret ved at sende en RefreshControl til FlatLists refreshControl-egenskab. Det viser platformens indbyggede opdateringsindikator (en spinner på iOS og en trækkeanimation på Android). Sæt egenskaben refreshing til en boolsk tilstandsvariabel og onRefresh til en funktion, der henter nye data og sætter refreshing tilbage til false, når den er færdig. Nulstil altid refreshing til false både ved succes og fejl i dit API-kald, så spinneren ikke bliver ved med at køre.
import { FlatList, RefreshControl } from 'react-native';
import { useState, useCallback } from 'react';
export default function RefreshableGrid({ initialProducts, fetchProducts }) {
const [products, setProducts] = useState(initialProducts);
const [refreshing, setRefreshing] = useState(false);
const onRefresh = useCallback(async () => {
setRefreshing(true);
try {
const fresh = await fetchProducts();
setProducts(fresh);
} finally {
setRefreshing(false); // always reset
}
}, [fetchProducts]);
return (
<FlatList
data={products}
numColumns={2}
keyExtractor={item => item.id}
renderItem={({ item }) => <>{/* ProductCard */}</>}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor='#4f86f7' />
}
/>
);
}Tom tilstand for rasteret
Når produktlisten er tom (ingen søgeresultater eller en tom kategori), skal du vise en tom tilstand i stedet for en blank skærm. Brug FlatLists ListEmptyComponent-egenskab til at vise en brugerdefineret komponent, når data-arrayet er tomt. En god tom tilstand indeholder et ikon eller en illustration, en tydelig overskrift ("Ingen resultater fundet") og eventuelt en handlingsknap ("Ryd filtre" eller "Gennemse alle"). Det forbedrer brugervenligheden markant sammenlignet med ikke at vise noget og efterlade brugeren i tvivl om, hvorvidt data indlæses eller mangler.
import { FlatList, View, Text, TouchableOpacity, StyleSheet } from 'react-native';
function EmptyState({ onReset }) {
return (
<View style={styles.empty}>
<Text style={styles.icon}>🔍</Text>
<Text style={styles.title}>No products found</Text>
<Text style={styles.subtitle}>Try different filters or search terms</Text>
<TouchableOpacity style={styles.btn} onPress={onReset}>
<Text style={styles.btnText}>Clear filters</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
empty: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 32 },
icon: { fontSize: 56, marginBottom: 16 },
title: { fontSize: 20, fontWeight: 'bold', color: '#333' },
subtitle: { fontSize: 14, color: '#999', textAlign: 'center', marginTop: 8 },
btn: { marginTop: 20, paddingHorizontal: 24, paddingVertical: 12, backgroundColor: '#4f86f7', borderRadius: 24 },
btnText: { color: '#fff', fontWeight: '600' },
});Handlinger ved langt tryk på rasterelementer
En kontekstmenu ved langt tryk på rasterkort gør dem mere nyttige — brugere kan holde fingeren på et kort for at se muligheder som "Føj til ønskeliste", "Del" eller "Fjern". Brug egenskaben onLongPress på kortets TouchableOpacity- eller Pressable-wrapper. Vis handlingsarket med React Natives indbyggede ActionSheetIOS på iOS eller en brugerdefineret modal med bundark, der fungerer på tværs af platforme. Send elementets data til håndteringen af langt tryk, så handlingsarket ved, hvilket element handlingen skal udføres på.
import { TouchableOpacity, ActionSheetIOS, Platform, Alert } from 'react-native';
function GridCard({ item, width, onDelete, onShare }) {
function handleLongPress() {
if (Platform.OS === 'ios') {
ActionSheetIOS.showActionSheetWithOptions(
{
options: ['Cancel', 'Share', 'Remove'],
cancelButtonIndex: 0,
destructiveButtonIndex: 2,
},
(index) => {
if (index === 1) onShare(item);
if (index === 2) onDelete(item.id);
}
);
} else {
Alert.alert(item.name, 'Choose an action', [
{ text: 'Share', onPress: () => onShare(item) },
{ text: 'Remove', style: 'destructive', onPress: () => onDelete(item.id) },
{ text: 'Cancel', style: 'cancel' },
]);
}
}
return (
<TouchableOpacity
onPress={() => {/* navigate to detail */}}
onLongPress={handleLongPress}
delayLongPress={400}
style={{ width }}
>
{/* card content */}
</TouchableOpacity>
);
}Hurtigt tjek
Test din forståelse af begreberne inden for React Native Mobile Development fra denne lektion.
Opsummering af lektionen
I denne lektion har du lært, at FlatList med numColumns virtualiserer rastervisning for store datasæt, at beregning af cardWidth sikrer en perfekt tilpasning på alle skærmstørrelser, og at ListEmptyComponent og ListHeaderComponent giver et mere gennemarbejdet resultat med tomme tilstande og kategorioverskrifter. Dernæst ser vi på, hvordan du sender data mellem komponenter med props.
Lær JavaScript med en AI-underviser — gratis
Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.
- Kurser
- 30
- Lektioner
- 120
Ofte stillede spørgsmål
Er lektionen “Opbygning af et responsivt kortgitter” gratis?
Ja — alle 3 lektioner i læringssporet React Native Academy, inklusive “Opbygning af et responsivt kortgitter”, kan læses gratis i deres fulde længde her på webstedet. Derefter låser CoddyKit PRO alle lektioner op samt interaktive øvelser med en indbygget kodeeditor og en AI-underviser døgnet rundt. React Native Academy-kurset indeholder 4 lektioner i alt.
Hvad lærer jeg i “Opbygning af et responsivt kortgitter”?
Anvend Flexbox-ombrydning og procentbaserede bredder til at oprette et kortgitter med to kolonner, der ser korrekt ud på både små telefoner og store tablets. Du øver dig i React Native Academy med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.
Skal jeg have erfaring for at begynde på React Native Academy?
Der kræves ingen tidligere erfaring. React Native Academy på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 4 af 4.
Hvor lang tid tager lektionen “Opbygning af et responsivt kortgitter”?
De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.
Kan jeg skrive og køre kode i denne React Native Academy-lektion?
Ja. Alle React Native Academy-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.
Alle lektioner i dette kursus
- StyleSheet.create og inline-typografier
- Flexbox-retning, justifyContent og alignItems
- flex, flexGrow og responsiv størrelsestilpasning
- Opbygning af et responsivt kortgitter