كيفية بناء تطبيق أخبار أندرويد باستخدام React Native وNative Base

دقائق القراءة: 10

مقدمة: لماذا تبني تطبيق أخبار باستخدام React Native؟

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

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

واجهة توضيحية لمشروع تطبيق أخبار أندرويد باستخدام React Native وNative Base

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

الشكل النهائي المتوقع لتطبيق الأخبار على أندرويد

تثبيت Expo وبدء المشروع

يُعد Expo من أسرع الأدوات لتشغيل مشاريع React Native وتطويرها دون تعقيد كبير في الإعدادات الأولية. فهو يوفّر بيئة جاهزة لتجربة التطبيق وبنائه على أكثر من منصة.

تثبيت أداة Expo CLI

ابدأ بتثبيت أداة سطر الأوامر الخاصة بـ Expo عبر تنفيذ الأمر التالي:

npm install --global expo-cli

استخدام الخيار --global يعني تثبيت الأداة بشكل عام على الجهاز، حتى تتمكن من استخدامها في أي مشروع لاحقاً.

إنشاء مشروع جديد

بعد اكتمال التثبيت، أنشئ مشروعك الجديد بالأمر التالي:

expo init News-Application

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

بعد تنزيل الحزم والاعتماديات، انتقل إلى مجلد المشروع ثم شغّل التطبيق باستخدام:

expo start

سيفتح ذلك أدوات المطور الخاصة بـ Expo داخل المتصفح.

أدوات المطور الخاصة بمنصة 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 Native بعد الإنشاء

إنشاء شاشات التطبيق باستخدام React Navigation

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

تثبيت المكتبة الأساسية

npm install @react-navigation/native
expo install react-native-screens react-native-safe-area-context

بعد ذلك سنستخدم شريط تنقل سفلي من نوع Bottom Tabs لأنه مناسب لتطبيقات الأخبار ويمنح وصولاً سريعاً إلى الأقسام.

صفحة توثيق React Navigation الخاصة بمكوّن 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

توفر هذه المكتبة عدداً كبيراً من الأيقونات الجاهزة.

مجموعة الأيقونات المتاحة في مكتبة 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 التي تتيح الوصول إلى العناوين والأوصاف والصور وتاريخ النشر.

موقع News API المستخدم للحصول على مفتاح 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 إلى واجهة الأخبار، وتُرجع المقالات فقط من النتيجة النهائية.

الفكرة هنا بسيطة:

  1. بناء رابط الطلب باستخدام الدولة والتصنيف.
  2. إرسال المفتاح داخل الترويسات headers.
  3. تحويل الاستجابة إلى صيغة JSON.
  4. إرجاع مصفوفة المقالات عبر 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>

خيارات تنسيق التاريخ والوقت في مكتبة moment

الصيغة 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 يُعد مشروعاً ممتازاً لفهم أساسيات تطوير التطبيقات متعددة الشاشات وربطها بمصادر بيانات خارجية. هذا النوع من المشاريع يجمع بين إدارة الحالة، جلب البيانات، تصميم الواجهة، وتحسين تجربة المستخدم أثناء التحميل والتنقل. ومن الناحية التقنية، أفضل خطوة لاحقة بعد هذا النموذج هي تقليل التكرار عبر إنشاء مكوّنات مشتركة واستخدام بنية أكثر تنظيماً للخدمات والواجهات، لأن ذلك يجعل التطبيق أسهل في التوسعة والصيانة لاحقاً.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *