feeds-reader تطبيق عملي: تطبيق قارئ الخلاصات - الجزء الثاني


حسام برهان

سنتناول في هذا الدرس من سلسلة تعلّم برمجة تطبيقات أندرويد باستخدام Xamarin.Forms الجزء الثاني من تطبيق قارئ الخلاصات الخاص بموقع أكاديميّة حسوب.

main2.png


لقد بنينا في الجزء الأوّل تطبيق أساسي أسميناه BasicFeedReader وكانت وظيفته تنحصر في عرض خلاصات قسم مقالات البرمجة. سنطوّر هذه الفكرة من خلال إنشاء تطبيق جديد يعمل على إتاحة الإمكانية للمستخدم بأن يستعرض الخلاصات المتاحة من جميع أقسام الموقع. سيكون ذلك من خلال إضافة صفحة جديدة لعرض أقسام الموقع على شكل مجموعات. لنبدأ فورًا في بناء هذا التطبيق التي سيحتوي على بعض من المكوّنات المتطابقة مع سلفه.

بناء تطبيق قارئ الخلاصات المحسّن

أنشئ مشروعًا جديدًا من النوع Blank App (Xamarin.Forms Portable) وسمّه EnhancedFeedReader ثم أبق فقط على المشروعين EnhancedFeedReader (Portable) و EnhancedFeedReader.Droid كما وسبق أن فعلنا في هذا الدرس.
رغم الشبه الكبير بين هذا التطبيق وسلفه إلًّا أنّني آثرت إعادة إدراج الشيفرة البرمجيّة للأجزاء المتشابهة، وذلك لكي يكون هذا التطبيق متكاملًا قائمًا بحد ذاته.
أضف المجلّدين التاليين إلى المشروع EnhancedFeedReader (Portable) (عن طريق النقر بزر الفأرة الأيمن ثم اختيار Add ثم New Folder): Pages و Entities.

الأصناف ضمن المجلّد Entities

أضف الأصناف التالي إلى المجلّد Entities: FeedItem و Section و GroupSection.
بالنسبة للصنف FeedItem فلقد تحدثنا عنه في الجزء الأوّل، وإليك الشيفرة البرمجيّة الخاصّة به:

namespace EnhancedFeedReader.Entities
{
    public class FeedItem
    {
        public string Link { get; set; }
        public string Title { get; set; }
        public string Description { get; set; }

        public override string ToString()
        {
            return Title;
        }
    }
}

الصنف Section هو صنف جديد تمامًا وهو يمثّل قسم من أقسام المحتوى الخاصّة بموقع أكاديميّة حسّوب. الشيفرة البرمجيّة للملف Section.cs هي:

using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Linq;

namespace EnhancedFeedReader.Entities
{
    public class Section
    {
        public string Name { get; set; }

        public string ResourceUrl { get; set; }

        public async Task<ObservableCollection<FeedItem>> GetFeedItems()
        {
            return await Task.Factory.StartNew(() =>
            {
                XDocument doc = XDocument.Load(ResourceUrl);

                var feeds = from newsItem in doc.Descendants("item")
                            select new FeedItem
                            {
                                Title = newsItem.Element("title").Value,
                                Description = newsItem.Element("description").Value,
                                Link = newsItem.Element("link").Value
                            };

                return new ObservableCollection<FeedItem>(feeds);
            });
        }

        public override string ToString()
        {
            return Name;
        }
    }
}

يحتوي هذا الصنف على خاصيّتين: الاسم Name وعنوان المصدر ResourceUrl الذي سيتم جلب الخلاصات منه. يوجد أيضًا التابع GetFeedItems الذي سيجلب الخلاصات التابعة للقسم الحالي. هذا التابع هو البديل للتابع LoadFeeds الذي كان موجودًا في الصنف SectionFeedsPage وهو يقوم بنفس وظيفته مع ميزة استخدام البرمجة غير المتزامنة. وأخيرًا التابع ToString للحصول على تمثيل نصّي لكائن من النوع Section.
أمّا بالنسبة للصنف GroupSection فهو يمثّل تصنيف لمجموعة من الأقسام. في الحقيقة يعمل موقع أكاديميّة حسوب على تقسيم الخلاصات ضمن مجموعات كل منها يحتوي على قسمين. انظر إلى الشيفرة الخاصّ بهذا الصنف:

using System.Collections.ObjectModel;

namespace EnhancedFeedReader.Entities
{
    public class GroupSection : ObservableCollection<Section>
    {
        public string Name { get; set; }
    }
}

لاحظ كم هي بسيطة هذه الشيفرة. يحتوي هذا الصنف على خاصيّة واحدة هي خاصيّة الاسم Name له. مع الانتباه إلى أنّه يرث من الصنف العمومي ObservableCollection<Section>. وهذا إشارة إلى أنّه يمثّل مجموعة من الأقسام Section. سنرى سبب عمليّة الوراثة هذه بعد قليل.

الواجهات ضمن المجلّد Pages

يوجد لدينا ضمن هذا المجلّد ثلاث واجهات تتبع لثلاثة أصناف. صنفان منهما قديمان وهما: SectionFeedsPage و FeedDetailsPage وهما يمثّلان على الترتيب: الواجهة المسؤولة عن عرض خلاصات قسم محدّد من الأقسام المتوفّرة، ومحتوى الخلاصة التي اختارها المستخدم من الواجهة السابقة.
أمّا الصنف الثالث فهو SectionsPage وهو يمثّل الواجهة الرئيسيّة الخاصة بعرض جميع الأقسام المتوفّرة ضمن موقع الأكاديميّة.
انتقل إلى ملف الرماز SectionFeedsPage.xaml واحرص على أن تكون محتوياته على الشكل التالي:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="EnhancedFeedReader.Pages.SectionFeedsPage">
  <StackLayout>
    <ListView x:Name="lsvFeeds"
              ItemTapped="lsvFeeds_ItemTapped"/>
  </StackLayout>

</ContentPage>

ثم انتقل إلى ملف الشيفرة الموافق له وهو SectionFeedsPage.xaml.cs واحرص على أن تكون محتوياته على الشكل التالي:

using EnhancedFeedReader.Entities;
using Xamarin.Forms;

namespace EnhancedFeedReader.Pages
{
    public partial class SectionFeedsPage : ContentPage
    {
        public SectionFeedsPage(Section section)
        {
            InitializeComponent();

            Title = section.Name;

            PopulateFeedsListView(section);
        }

        private async void PopulateFeedsListView(Section section)
        {
            lsvFeeds.ItemsSource = await section.GetFeedItems();
        }

        private async void lsvFeeds_ItemTapped(object sender, ItemTappedEventArgs e)
        {
            FeedItem selectedFeed = (FeedItem)e.Item;
            FeedDetailsPage feedDetailsPage = new
                FeedDetailsPage(selectedFeed);

            await Navigation.PushAsync(feedDetailsPage);
        }
    }
}

الجديد في هذا الصنف هو التابع PopulateFeedsListView الذي يُمرّر إليه وسيط واحد من النوع Section حيث يتم الحصول على خلاصاته ومن ثمّ إسنادها إلى القائمة lsvFeeds لعرضها. لاحظ كيف يتمّ ذلك باستخدام البرمجة غير المتزامنة.

انتقل بعد ذلك إلى ملف الرماز FeedDetailsPage.xaml واحرص على أن تكون محتوياته على الشكل التالي:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="EnhancedFeedReader.Pages.FeedDetailsPage">

  <StackLayout Orientation="Vertical">

    <WebView x:Name="wvDescription"
             VerticalOptions="FillAndExpand"
             HorizontalOptions="FillAndExpand"/>
  </StackLayout>

</ContentPage>

ثم انتقل إلى ملف الشيفرة البرمجيّة الموافق له وهو FeedDetailsPage.xaml.cs:

using Xamarin.Forms;
using EnhancedFeedReader.Entities;

namespace EnhancedFeedReader.Pages
{
    public partial class FeedDetailsPage : ContentPage
    {
        public FeedDetailsPage(FeedItem feedItem)
        {
            InitializeComponent();

            this.Title = feedItem.Title;

            var descriptionHtmlSource = new HtmlWebViewSource();
            descriptionHtmlSource.Html = @"<html dir='rtl'><body>"
                                        + feedItem.Description
                                        + "</body></html>";

            wvDescription.Source = descriptionHtmlSource;
        }
    }
}

لم يطرأ في الحقيقة أيّ تعديل على هذا الصنف.
انتقل أخيرًا إلى ملف الرماز SectionsPage.xaml الذي يمثّل واجهة العرض الأساسيّة، التي ستعرض جميع أقسام الخلاصات المتاحة في الأكاديميّة واحرص على أن تكون محتوياته كما يلي:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="EnhancedFeedReader.Pages.SectionsPage">

  <ListView x:Name="lsvSections"
              ItemTapped="lsvSections_ItemTapped"
              IsGroupingEnabled="True"
              GroupDisplayBinding="{Binding Name}" />

</ContentPage>

واضح أنّ محتوياته بسيطة، فهي لا تتعدى عنصر القائمة الذي أسميته lsvSections. الأمر الملفت للنظر هنا هو وجود الخاصيتين IsGroupingEnabled و GroupDisplayBinding لعنصر القائمة. أسندنا القيمة True للخاصيّة IsGroupingEnabled وذلك للإشارة إلى وجوب أن تدعم هذه القائمة ميزة التجميع Grouping. أمّا الخاصيّة GroupDisplayBinding فقد أسندت لها القيمة {Binding Name} للإشارة إلى أنّني أرغب بأن يتم ربطها مع قيمة الخاصيّة Name للكائن GroupSection فهي تُحدّد النص المراد إظهاره كعنوان لكل مجموعة. لكي تتوضّح الأمور بشكل أفضل انظر إلى الواجهة في حالة العمل:

fig01.png

انتقل الآن إلى ملف الشيفرة البرمجيّة الموافق وهو SectionsPage.xaml.cs لكي نتعرّف على طريقة الاستفادة من هذه الميزة، واحرص على أن تكون محتوياته على الشكل التالي:

using System.Collections.Generic;
using System.Collections.ObjectModel;
using Xamarin.Forms;
using EnhancedFeedReader.Entities;

namespace EnhancedFeedReader.Pages
{
    public partial class SectionsPage : ContentPage
    {
        public SectionsPage()
        {
            InitializeComponent();

            Title = "قارئ خلاصات أكاديمية حسوب";

            LoadGroupSections();
        }

        private void LoadGroupSections()
        {
            int spacePos = 0;
            string tmp;
            GroupSection gs = null;
            Dictionary<string, string[]> groupSectionDic = 
                new Dictionary<string, string[]>()
            {
                { "خلاصات ريادة الأعمال",new string[] { "https://academy.hsoub.com/entrepreneurship/?rss=1", "https://academy.hsoub.com/questions/rss/entrepreneurship-5.xml" } },
                { "خلاصات العمل الحر",new string[] { "https://academy.hsoub.com/freelance/?rss=1", "https://academy.hsoub.com/questions/rss/freelance-8.xml" } },
                { "خلاصات التسويق والمبيعات", new string[] { "https://academy.hsoub.com/marketing/?rss=1", "https://academy.hsoub.com/questions/rss/marketing-7.xml" } },
                { "خلاصات البرمجة", new string[] { "https://academy.hsoub.com/programming/?rss=1", "https://academy.hsoub.com/questions/rss/programming-3.xml" } },
                { "خلاصات التصميم", new string[] { "https://academy.hsoub.com/design/?rss=1", "https://academy.hsoub.com/questions/rss/design-4.xml" } },
                { "خلاصات DevOps",new string[] { "https://academy.hsoub.com/devops/?rss=1", "https://academy.hsoub.com/questions/rss/devops-6.xml" } },
                { "خلاصات البرامج والتطبيقات",new string[] { "https://academy.hsoub.com/apps/?rss=1", "https://academy.hsoub.com/questions/rss/apps-9.xml" } },
                { "خلاصات الشهادات المتخصصة",new string[] { "https://academy.hsoub.com/certificates/?rss=1", "https://academy.hsoub.com/questions/rss/certificates-10.xml" } }
            };

            ObservableCollection<GroupSection> groupSections =
                new ObservableCollection<GroupSection>();

            foreach (var grp in groupSectionDic)
            {
                gs = new GroupSection() { Name = grp.Key };

                spacePos = grp.Key.IndexOf(" ");
                tmp = grp.Key.Substring(spacePos + 1);

                gs.Add(
                    new Section
                    {
                        Name = "مقالات " +  tmp ,
                        ResourceUrl = grp.Value[0]
                    }
                );

                gs.Add(
                    new Section
                    {
                        Name = "أسئلة " + tmp,
                        ResourceUrl = grp.Value[1]
                    }
                );

                groupSections.Add(gs);
            }

            lsvSections.ItemsSource = groupSections;
        }

        private async void lsvSections_ItemTapped(object sender, ItemTappedEventArgs e)
        {
            Section selectedSection = (Section)e.Item;

            SectionFeedsPage sectionFeedsPage = new
                SectionFeedsPage(selectedSection);

            await Navigation.PushAsync(sectionFeedsPage);
        }
    }
}

هناك الكثير من المتعة في الشيفرة السابقة! تتمحور معظم الشيفرة حول التابع LoadGroupSections ويُستَخدم لتجهيز الأقسام المختلفة لمصادر الخلاصات في الأكاديمية ووضعها ضمن مجموعات منفصلة ضمن عنصر القائمة lsvSections. هذه الأقسام (كعناوين) موجودة في وضع عدم اتصال وقد نسختها يدويًا من موقع الأكاديميّة، وهي ضمن المتغير groupSectionDic الذي صرحنا عنه من نوع القاموس Dictionary

ملف التطبيق App.cs

انتقل إلى ملف التطبيق App.cs واحرص على أن تكون بانية الصنف App على الشكل التالي:

public App()
{
    // The root page of your application
    MainPage = new NavigationPage(new SectionsPage());
}

احرص على استخدام فضاء الاسم Pages لكي تستطيع الوصول إلى الصفحة SectionsPage كما يلي:

using EnhancedFeedReader.Pages;

ستضع السطر السابق أوّل الملف App.cs كما هو معلوم. نفّذ البرنامج وتنقّل في أقسامه المختلفة.

الخلاصة

تناولنا في هذا الدرس الجزء الثاني من تطبيق قارئ الخلاصات الخاص بموقع أكاديميّة حسّوب. لقد أجرينا تحسينات مهمّة على هذا التطبيق من خلال السماح للمستخدم بتصفّح الخلاصات من جميع أقسام الموقع. حيث استخدمنا ميزة التجميع grouping لعنصر القائمة ListView وذلك لتجميع العناوين التابعة لنفس القسم معًا. نكون بذلك قد انتهينا من هذا التطبيق.





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن