كيفية بناء تطبيق أخبار أندرويد باستخدام React Native وNative Base
مقدمة: لماذا تبني تطبيق أخبار باستخدام React Native؟
نعيش اليوم في عالم سريع التغيّر، حيث تتجدد الأخبار والمعلومات على مدار الساعة. لذلك، أصبح وجود تطبيق أخبار عملي وسريع أمراً مهماً للمستخدم الذي يريد متابعة المستجدات من مصادر متعددة وبحسب التصنيف الذي يفضله. في هذا الدليل العملي، سنبني تطبيق أخبار يعمل على أندرويد بالاعتماد على React Native، مع استخدام Native Base لتحسين الواجهة، وReact Navigation للتنقل بين الأقسام، بالإضافة إلى ربط التطبيق مع خدمة أخبار خارجية عبر API.
الهدف من هذا المشروع ليس فقط إنشاء واجهة تعرض الأخبار، بل أيضاً فهم كيفية تنظيم المشروع، إنشاء شاشات متعددة، استدعاء البيانات من الإنترنت، تحسين تجربة المستخدم أثناء التحميل، وتنسيق النتائج بشكل احترافي.

في نهاية هذا الشرح، سيكون لديك تطبيق يعرض الأخبار الرئيسية من عدة فئات مثل الأخبار العامة، الأعمال، الصحة، الرياضة، والتقنية.

تثبيت Expo وبدء المشروع
يُعد Expo من أسرع الأدوات لتشغيل مشاريع React Native وتطويرها دون تعقيد كبير في الإعدادات الأولية. فهو يوفّر بيئة جاهزة لتجربة التطبيق وبنائه على أكثر من منصة.
تثبيت أداة Expo CLI
ابدأ بتثبيت أداة سطر الأوامر الخاصة بـ Expo عبر تنفيذ الأمر التالي:
npm install --global expo-cli
استخدام الخيار --global يعني تثبيت الأداة بشكل عام على الجهاز، حتى تتمكن من استخدامها في أي مشروع لاحقاً.
إنشاء مشروع جديد
بعد اكتمال التثبيت، أنشئ مشروعك الجديد بالأمر التالي:
expo init News-Application
سيطرح عليك المثبّت بعض الأسئلة، مثل اسم المشروع ونوع القالب المطلوب. اختر القالب الفارغ blank حتى تبدأ ببنية نظيفة وسهلة التخصيص.
بعد تنزيل الحزم والاعتماديات، انتقل إلى مجلد المشروع ثم شغّل التطبيق باستخدام:
expo start
سيفتح ذلك أدوات المطور الخاصة بـ Expo داخل المتصفح.

يمكنك اختيار التشغيل على الهاتف أو المحاكي أو المتصفح. ولأغراض الشرح السريع، يمكن تجربة التطبيق على المتصفح.
ملف App.js الافتراضي
بعد إنشاء المشروع، ستحصل على ملف App.js افتراضي مشابه لما يلي:
import { StatusBar } from 'expo-status-bar' ;
import React from 'react' ;
import { StyleSheet, Text, View } from 'react-native' ;
export default function App() {
return (
<View style={styles.container}>
<Text>Open up App.js to start working on your app!</Text>
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
هذا هو القالب الافتراضي، وسنبدأ منه في بناء التطبيق الفعلي.

إنشاء شاشات التطبيق باستخدام React Navigation
تطبيق الأخبار يحتاج إلى أكثر من شاشة أو تبويب لعرض التصنيفات المختلفة، مثل الأخبار العامة والاقتصاد والصحة والتقنية. لهذا سنستخدم مكتبة React Navigation.
تثبيت المكتبة الأساسية
npm install @react-navigation/native
expo install react-native-screens react-native-safe-area-context
بعد ذلك سنستخدم شريط تنقل سفلي من نوع Bottom Tabs لأنه مناسب لتطبيقات الأخبار ويمنح وصولاً سريعاً إلى الأقسام.

تثبيت Bottom Tabs
npm install @react-navigation/bottom-tabs
استيراد وبناء التنقل السفلي
أضف الاستيرادات الأساسية داخل ملف App.js:
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs' ;
import { NavigationContainer } from '@react-navigation/native' ;
ثم أنشئ التبويبات:
const Tab = createBottomTabNavigator();
function MyTabs() {
return (
<Tab.Navigator>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Settings" component={SettingsScreen} />
</Tab.Navigator>
);
}
لكن في مشروعنا سنستخدم التصنيفات التالية:
- الأخبار العامة
- أخبار الأعمال
- الأخبار الصحية
- الأخبار الرياضية
- أخبار التقنية
وبالتالي ستكون التبويبات الفعلية على النحو التالي:
<Tab.Navigator>
<Tab.Screen name="All" component={All} />
<Tab.Screen name="Business" component={Business} />
<Tab.Screen name="Health" component={HealthScreen} />
<Tab.Screen name="Sports" component={SportsScreen} />
<Tab.Screen name="Tech" component={TechScreen} />
</Tab.Navigator>
يجب أيضاً تغليف هذا المكوّن داخل NavigationContainer حتى يعمل نظام التنقل بشكل صحيح:
<NavigationContainer>
<Tab.Navigator>
<Tab.Screen name="All" component={All} />
<Tab.Screen name="Business" component={Business} />
<Tab.Screen name="Health" component={HealthScreen} />
<Tab.Screen name="Sports" component={SportsScreen} />
<Tab.Screen name="Tech" component={TechScreen} />
</Tab.Navigator>
</NavigationContainer>
ولا تنس استيراد جميع الشاشات في أعلى الملف:
import All from './screens/All' ;
import Business from './screens/Business' ;
import HealthScreen from './screens/Health' ;
import SportsScreen from './screens/Sports' ;
import TechScreen from './screens/Tech' ;
النسخة الكاملة من ملف App.js ستكون كالتالي:
import React from 'react' ;
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs' ;
import { NavigationContainer } from '@react-navigation/native' ;
import All from './screens/All' ;
import Business from './screens/Business' ;
import HealthScreen from './screens/Health' ;
import SportsScreen from './screens/Sports' ;
import TechScreen from './screens/Tech' ;
const Tab = createBottomTabNavigator();
export default function App() {
return (
<NavigationContainer>
<Tab.Navigator>
<Tab.Screen name="All" component={All} />
<Tab.Screen name="Business" component={Business} />
<Tab.Screen name="Health" component={HealthScreen} />
<Tab.Screen name="Sports" component={SportsScreen} />
<Tab.Screen name="Tech" component={TechScreen} />
</Tab.Navigator>
</NavigationContainer>
);
}

إضافة أيقونات لشريط التنقل السفلي
استخدام الأيقونات يجعل التبويبات أوضح وأكثر أناقة، ويساعد المستخدم على التمييز السريع بين الأقسام.
تثبيت مكتبة الأيقونات
npm install react-native-elements
توفر هذه المكتبة عدداً كبيراً من الأيقونات الجاهزة.

على سبيل المثال، يمكن إضافة أيقونة الصفحة الرئيسية إلى تبويب الأخبار العامة كما يلي:
<Tab.Screen
name="All"
component={All}
options={{
tabBarIcon: (props) => (
<Icon type='feather' name='home' color={props.color} />
),
}}
/>

وبنفس الفكرة يمكن تعريف الأيقونات لبقية الشاشات:
<Tab.Navigator>
<Tab.Screen
name="All"
component={All}
options={{
tabBarIcon: (props) => (
<Icon type='feather' name='home' color={props.color} />
),
}}
/>
<Tab.Screen
name="Business"
component={Business}
options={{
tabBarIcon: (props) => (
<Icon type='feather' name='dollar-sign' color={props.color} />
),
}}
/>
<Tab.Screen
name="Health"
component={HealthScreen}
options={{
tabBarIcon: (props) => (
<Icon type='feather' name='heart' color={props.color} />
),
}}
/>
<Tab.Screen
name="Sports"
component={SportsScreen}
options={{
tabBarIcon: (props) => (
<Icon type='ionicon' name='tennisball-outline' color={props.color} />
),
}}
/>
<Tab.Screen
name="Tech"
component={TechScreen}
options={{
tabBarIcon: (props) => (
<Icon type='ionicon' name='hardware-chip-outline' color={props.color} />
),
}}
/>
</Tab.Navigator>

ربط التطبيق مع News API
بعد تجهيز بنية التطبيق، ننتقل إلى الجزء الأهم: جلب الأخبار من مصدر خارجي. سنستخدم خدمة News API التي تتيح الوصول إلى العناوين والأوصاف والصور وتاريخ النشر.

بعد التسجيل في الموقع، ستحصل على مفتاح وصول API Key. ننصح بحفظ هذا المفتاح في ملف إعدادات منفصل.
إنشاء ملف الإعدادات
export const API_KEY = `` ;
export const endpoint = `https://newsapi.org/v2/top-headlines` ;
export const country = 'in'
هذا الملف يحتوي على:
API_KEYلمصادقة الطلبات.endpointوهو الرابط الأساسي لجلب الأخبار.countryلتحديد الدولة المستهدفة في النتائج.
إنشاء خدمة لجلب البيانات
أنشئ ملفاً باسم services.js واستخدم فيه الشيفرة التالية:
import { API_KEY, endpoint, country } from '../config/config' ;
export async function services(category = 'general') {
let articles = await fetch(`${endpoint}?country=${country}&category=${category}`, {
headers: {
'X-API-KEY': API_KEY
}
});
let result = await articles.json();
articles = null;
return result.articles;
}
هذه الدالة تستقبل تصنيفاً افتراضياً قيمته general، ثم ترسل طلب GET إلى واجهة الأخبار، وتُرجع المقالات فقط من النتيجة النهائية.
الفكرة هنا بسيطة:
- بناء رابط الطلب باستخدام الدولة والتصنيف.
- إرسال المفتاح داخل الترويسات
headers. - تحويل الاستجابة إلى صيغة
JSON. - إرجاع مصفوفة المقالات عبر
return.
استخدام useEffect وuseState داخل شاشة الأخبار
لنبدأ بشاشة الأخبار العامة All.js. سنحتاج إلى حالتين أساسيتين:
useStateلتخزين الأخبار المسترجعة.useEffectلتنفيذ جلب البيانات بمجرد تحميل الشاشة.
import React, { useEffect, useState } from 'react'
import { View } from 'react-native' ;
import { services } from '../services/services' ;
export default function All() {
const [newsData, setNewsData] = useState([])
useEffect(() => {
services('general')
.then(data => {
setNewsData(data)
})
.catch(error => {
alert(error)
})
}, [])
return (
<View></View>
)
}
في هذا المثال، يتم استدعاء الدالة services() مرة واحدة فقط عند فتح الشاشة، ثم تُخزن النتيجة في المتغير newsData.
تثبيت Native Base وبناء واجهة عرض الأخبار
حتى تكون الواجهة أنيقة وعملية، سنستخدم مكتبة Native Base التي توفر مكوّنات جاهزة مثل FlatList وSpinner وDivider وImage.
تثبيت الحزم المطلوبة
yarn add native-base styled-components styled-system
expo install react-native-svg react-native-safe-area-context
استيراد المكوّنات
import React, { useEffect, useState } from 'react'
import { View, Text } from 'react-native' ;
import { NativeBaseProvider, FlatList, ScrollView, Divider, Image, Spinner } from 'native-base' ;
import { services } from '../services/services' ;
إضافة الحاوية الرئيسية
return (
<NativeBaseProvider>
</NativeBaseProvider>
)
ثم نضيف ScrollView حتى يتمكن المستخدم من التمرير عند وجود عدد كبير من الأخبار:
<NativeBaseProvider>
<ScrollView height={850}>
</ScrollView>
</NativeBaseProvider>
عرض الأخبار باستخدام FlatList
<NativeBaseProvider>
<ScrollView height={850}>
<FlatList
data={newsData}
renderItem={({ item }) => (
<View></View>
)}
keyExtractor={(item) => item.id}
/>
</ScrollView>
</NativeBaseProvider>
يعرض المكوّن FlatList كل عنصر داخل المصفوفة newsData، وهو خيار أفضل من التكرار اليدوي عند التعامل مع البيانات الكبيرة.
يمكننا الآن إضافة المحتوى الفعلي لكل خبر:
<NativeBaseProvider>
<ScrollView height={850}>
<FlatList
data={newsData}
renderItem={({ item }) => (
<View>
<View>
<Text>{item.title}</Text>
<Text>{item.publishedAt}</Text>
<Text>{item.description}</Text>
</View>
</View>
)}
keyExtractor={(item) => item.id}
/>
</ScrollView>
</NativeBaseProvider>

تنسيق واجهة التطبيق وتحسين قابلية القراءة
الواجهة الأولية تؤدي الغرض، لكنها تحتاج إلى تحسين بصري حتى تصبح أكثر راحة للمستخدم. لهذا سنعتمد على StyleSheet.
استيراد StyleSheet
import { View, Text, StyleSheet } from 'react-native' ;
تطبيق الأنماط على عناصر الخبر
<View>
<View style={styles.newsContainer}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.date}>{item.publishedAt}</Text>
<Text style={styles.newsDescription}>{item.description}</Text>
</View>
</View>
ثم أضف الأنماط التالية أسفل الملف:
const styles = StyleSheet.create({
newsContainer: {
padding: 10
},
title: {
fontSize: 18,
marginTop: 10,
fontWeight: '600'
},
newsDescription: {
fontSize: 16,
marginTop: 10
},
date: {
fontSize: 14
},
});

تنسيق التاريخ باستخدام moment
تواريخ الأخبار القادمة من الواجهة البرمجية تكون غالباً بصيغة غير مريحة للقراءة مثل 2021-08-21T11:00:40Z. ولتحويلها إلى صيغة أوضح، يمكن استخدام مكتبة moment.
تثبيت المكتبة
npm install moment --save
استخدامها داخل الشاشة
<Text style={styles.date}>
{moment(item.publishedAt).format('LLL')}
</Text>

الصيغة LLL تمنح تاريخاً ووقتاً أكثر قرباً للقراءة البشرية، وهذا يحسن تجربة الاستخدام بشكل واضح.

إضافة فاصل بصري بين الأخبار
عند عرض قائمة طويلة من المقالات، تصبح الفواصل البصرية مهمة لتجنب تداخل العناصر وتشويش القارئ. يوفر Native Base مكوّن Divider لهذه المهمة.
<View>
<View style={styles.newsContainer}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.date}>{moment(item.publishedAt).format('LLL')}</Text>
<Text style={styles.newsDescription}>{item.description}</Text>
</View>
<Divider my={2} bg="#e0e0e0" />
</View>

عرض صور الأخبار داخل التطبيق
من مزايا News API أن كثيراً من الأخبار تأتي مع صورة مرافقة. عرض الصورة يجعل الواجهة أكثر جاذبية ويزيد من فرص تفاعل المستخدم مع المحتوى.
<View>
<View style={styles.newsContainer}>
<Image
width={550}
height={250}
resizeMode={'cover'}
source={{ uri: item.urlToImage }}
alt="صورة الخبر"
/>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.date}>{moment(item.publishedAt).format('LLL')}</Text>
<Text style={styles.newsDescription}>{item.description}</Text>
</View>
<Divider my={2} bg="#e0e0e0" />
</View>
يعتمد عرض الصورة هنا على الحقل urlToImage القادم من البيانات.

إضافة مؤشر تحميل Spinner لتحسين تجربة المستخدم
من غير المناسب أن يرى المستخدم شاشة فارغة أثناء انتظار وصول البيانات. لذلك من الأفضل عرض مؤشر تحميل واضح.
سنفحص طول المصفوفة newsData، فإذا كانت تحتوي على عناصر نعرض القائمة، وإلا نعرض Spinner.
{newsData.length > 1 ? (
<FlatList
data={newsData}
renderItem={({ item }) => (
<View>
<View style={styles.newsContainer}>
<Image
width={550}
height={250}
resizeMode={'cover'}
source={{ uri: item.urlToImage }}
alt="صورة الخبر"
/>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.date}>{moment(item.publishedAt).format('LLL')}</Text>
<Text style={styles.newsDescription}>{item.description}</Text>
</View>
<Divider my={2} bg="#e0e0e0" />
</View>
)}
keyExtractor={(item) => item.id}
/>
) : (
<View style={styles.spinner}>
<Spinner color="danger.400" />
</View>
)}
وأضف التنسيق التالي:
spinner: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: 400
}
النسخة الكاملة من شاشة All.js
import React, { useEffect, useState } from 'react'
import { View, Text, StyleSheet } from 'react-native' ;
import { NativeBaseProvider, FlatList, ScrollView, Divider, Image, Spinner } from 'native-base' ;
import { services } from '../services/services' ;
import moment from 'moment'
export default function All() {
const [newsData, setNewsData] = useState([])
useEffect(() => {
services('general')
.then(data => {
setNewsData(data)
})
.catch(error => {
alert(error)
})
}, [])
return (
<NativeBaseProvider>
<ScrollView height={850}>
{newsData.length > 1 ? (
<FlatList
data={newsData}
renderItem={({ item }) => (
<View>
<View style={styles.newsContainer}>
<Image
width={550}
height={250}
resizeMode={'cover'}
source={{ uri: item.urlToImage }}
alt="صورة الخبر"
/>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.date}>{moment(item.publishedAt).format('LLL')}</Text>
<Text style={styles.newsDescription}>{item.description}</Text>
</View>
<Divider my={2} bg="#e0e0e0" />
</View>
)}
keyExtractor={(item) => item.id}
/>
) : (
<View style={styles.spinner}>
<Spinner color="danger.400" />
</View>
)}
</ScrollView>
</NativeBaseProvider>
)
}
const styles = StyleSheet.create({
newsContainer: {
padding: 10
},
title: {
fontSize: 18,
marginTop: 10,
fontWeight: '600'
},
newsDescription: {
fontSize: 16,
marginTop: 10
},
date: {
fontSize: 14
},
spinner: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: 400
}
});
إنشاء بقية الشاشات للتصنيفات الأخرى
بعد إنهاء شاشة All.js، يمكنك إعادة استخدام البنية نفسها في بقية الملفات مثل Business.js وHealth.js وSports.js وTech.js. الفرق الوحيد هو قيمة التصنيف المرسلة إلى الدالة services().
مثلاً في شاشة الأعمال:
useEffect(() => {
services('business')
.then(data => {
setNewsData(data)
})
.catch(error => {
alert(error)
})
}, [])

وللتصنيفات الأخرى استخدم القيم التالية:
useEffect(() => {
services('business')
.then(data => {
setNewsData(data)
})
.catch(error => {
alert(error)
})
}, [])
useEffect(() => {
services('health')
.then(data => {
setNewsData(data)
})
.catch(error => {
alert(error)
})
}, [])
useEffect(() => {
services('sports')
.then(data => {
setNewsData(data)
})
.catch(error => {
alert(error)
})
}, [])
useEffect(() => {
services('technology')
.then(data => {
setNewsData(data)
})
.catch(error => {
alert(error)
})
}, [])
نصائح عملية لتحسين المشروع قبل النشر
إذا كنت ترغب في تحويل هذا النموذج التعليمي إلى تطبيق أفضل من ناحية الجودة والاعتمادية، فهذه بعض التحسينات المقترحة:
- إضافة معالجة للأخطاء عند فشل الاتصال بالإنترنت.
- عرض رسالة بديلة عند غياب صورة الخبر بدلاً من ترك مساحة فارغة.
- تحسين
keyExtractorباستخدام قيمة أكثر موثوقية منitem.idإذا لم تكن متوفرة دائماً. - إنشاء مكوّن موحّد لبطاقة الخبر بدلاً من تكرار نفس الشيفرة في كل شاشة.
- استخدام متغيرات بيئية لإخفاء
API Keyوعدم وضعه مباشرة في ملفات المشروع. - إضافة شاشة تفاصيل عند الضغط على الخبر لعرض المحتوى كاملاً أو فتح الرابط الأصلي.
الخلاصة التقنية
بناء تطبيق أخبار باستخدام React Native وNative Base يُعد مشروعاً ممتازاً لفهم أساسيات تطوير التطبيقات متعددة الشاشات وربطها بمصادر بيانات خارجية. هذا النوع من المشاريع يجمع بين إدارة الحالة، جلب البيانات، تصميم الواجهة، وتحسين تجربة المستخدم أثناء التحميل والتنقل. ومن الناحية التقنية، أفضل خطوة لاحقة بعد هذا النموذج هي تقليل التكرار عبر إنشاء مكوّنات مشتركة واستخدام بنية أكثر تنظيماً للخدمات والواجهات، لأن ذلك يجعل التطبيق أسهل في التوسعة والصيانة لاحقاً.