Android Webservices

Android Webservices بالعربية – Retrofit

فى الدرس السابق Android Webservices بالعربية – Volley تحدثنا عن فولى ومميزاتها وكيفية اجراء الاتصال بها لاستقبال البيانات بالاضافة الى كيفية ارسال البيانات فى الـ Request واليوم سنتحدث عن retrofit  وهى مكتبة يتم تطويرها بواسطة square وتمكننا ايضا من اجراء الاتصال بالانترنت من الاندرويد بالاضافة لارسال واستقبال البيانات مثل فولى لكن من النظرة الاولى على ريتروفيت تجد أن لها فلسفة مختلفة وقد تجد أن اسلوبها غريب فاذا نظرت الى هذا الكود مثلا

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

او هذا الكود

@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId, @Query("sort") String sort);

ستجد أن الكود غريب الشكل غير واضح الملامح وتكاد تقول أن هذه ليست جافا ابدا 😀 وذلك بسبب ان الـ retrofit تتعامل مع الـ Annotation بشكل كبير لذلك قبل أن نشرح أى شىء عن ريتروفيت سنبدأ بالتعرف على الـ Annotation .

الـ Annotation

الـ Annotation هى عباره عن بيانات وصفية أو metadata  او ملحوظات  او دعنا نقول تعليقات .. هل تتذكر ايام الدراسة والمذاكرة فى الكتب ؟ ان كنت احد الطلبة المجتهدين فانك بالتاكيد كنت تقوم بعمل الكثير من الـ Annotation  واذا كنت أحد الطلبة الفاشلين وتكره المذاكرة فبالتاكيد تعلم   ان الطالب المجتهد عندما يذاكر بأحد الكتب فانه دائما يضع ملحوظات فى الهامش على ايمن الصفحة وعلى ايسرها وفى اسفلها ويشد الخطوط تحت العبارات ويستخدم القلم الفسفورى فى تحديد العبارات كل هذه تعتبر Annotation  فهى ليست جزء من الكتاب لكن مجرد ملحوظات توضيحية او اضافات تساعد الطالب على معرفة ملخص الصفحة اذا ما قرأها مرة أخرى او اى ملحوظة اخرى توضع فى اى كتاب او فى اى مقال انها لا تكون جزء من النص ولا من الموضوع لكن مجرد معلومة توضيحية .

الأمر لا يختلف فى الجافا فالـ Annotation عباره عن ملحوظة صغيرة او معلومة لا تكون جزء من الكود لكنها تكون عباره عن معلومة اضافية او ما يعرف بالـ metadata وأشهر مثال عليها هو @Override التى تجدها فوق كل ميثود تم عمل override لها وهى عباره عن ملحوظة أو metadata تخبر الأندرويد ستوديو أن هذه الميثود هى عباره عن ovveride لميثود موجوده فى الكلاس الاب وبالتالى فى حالة كانت هذه الميثود غير موجوده فى الكلاس الاب فيقوم باظهار Error  .

أحد الـ Annotations  المشهورة الاخرى هى الـ @Deprecated ففى كثير من الاحيان تريد استخدام ميثود معينه فى الاندرويد ستوديو او كلاس وتجد أنه قام بوضع خط فوقها كأنها مشطوبة أو ملغية بالتأكيد ان شطب اسم الكلاس او الميثود لا يعرفه الاندرويد ستوديو تلقائيا بل يقوم البرنامج بمعرفة هذه الميثود انها deprecated لان الميثود او الكلاس يكون مكتوب فوقها هذه الملحوظة أنها deprecated  فى الكود الاصلى للأندرويد وسنقوم الان بعمل تجربة رائعة وهى اننا سنقوم بعمل كلاس اسمها HendiwareMath وهذه الكلاس تحتوى على ميثود اسمها add تأخذ اثنين بارامتر تقوم بجمعهم والعودة بالناتج

math

استطيع الان استخدام هذه الميثود فى اى كلاس اخرى كالتالى

m1

الان تخيل انه عدت بعد سنة ووجدت ان الكلاس طويلة بشكل سىء واردت تحسينها واضافة ميثود افضل لتقوم بعملية الجمع بشكل مختصر كالتالى :

rr

الان الميثود add اريد الغاءها لكن تخيل لو أن الكلاس موجوده بمشروع كبير أعمل عليه أنا واصدقائى فى هنديوير لذلك  لا بد من عمل تنبيه لهم لكل لا يستخدموها ثم احذفها لاحقا لذلك سأقوم باستخدام ملحوظة ال deprecated كالتالى :

depracted

وبالتالى عندما يقوم  شخص  باستخدامها  تظهر لديه مشطوبة وانها deprecated وبالتالى سيبحث عن الميثود الاخرى المتاحة لفعل ذلك وسيجد newAdd .

deprcated method

الان قد فهمت الـ Annotation وبقى ان تعرف انه ليس فقط يمكنك استخدام الـ Annotation الموجودة بالجافا بل يمكنك عمل Annotation جديدة كما فعل اصحاب مكتبة Retrofit حيث قامو بعمل عدة Annotation يتم استخدامها اثناء كتابة الكود  .

الـ Retrofit

ريترو فيت هى مكتبة خاصة بااجراء عمليات الاتصال بالانترنت سواء لارسال بيانات او لاستقبال البيانات تغنيك عن طريقة الاتصال العادية HttpURLConnection وكذلك عن استخدام Volley  وطبعا لديك الخيار فى استخدام الطريقة التى تحبها فكل طريقه منهم لها مميزاتها وعيوبها والانترنت ملىء بالمقالات التى تقوم بالمقارنه بينهم واستعراض المميزات والعيوب ولعل الـرسم التوضيحى التالى  الرائع يوضع اسلوب اغلب المكتبات المستخدمة كـ volley و Rertrofit و Picasso وغيرها واسلوب كل منهم فى اجراء الاتصال بالانترنت فى صورة تجمع المكتبات معا وتوضح لك كيف تجرى الامور

httplibs

اذا لم تفهم الرسم التوضيحى السابق فلا بأس ولا علاقة لهذا بفهمك لـ Retrofit أو Volley فهو يوضح الاتصال بشكل عام فقط والان هيا بنا لنبدأ مع Retrofit .

ولـ Retrofit فسلفة مختلفة فى اجراء الطلب وطريقة مختلفة عن اسلوب الـ Volley و عن الـ HttpurlConnection بما يتعلق بكتابة الكود كما سنرى فى هذه التدوينة .

استخدام Retrofit

سنبدأ بالتطبيق كما فعلنا فى المثال السابق بالنسبة لـ Volley وبالنسبة للـ Httpurlconnection وسنحاول الاتصال بهذا الرابط http://developerhendy.16mb.com/getposts.php وجلب البيانات باستخدام Retrofit

نقوم بإضافة المكتبة باضافة هذا السطر الى ملف الـ Gradle  .

 compile 'com.squareup.retrofit2:retrofit:2.0.0-beta4'

وتقوم الـ Retrofit بعمل Convert تلقائى للـجيسون لذلك سنحتاج لإضافة هذا السطر ايضا لاستخدام مكتبة Gson  معها

compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta4'

ويمكن استخدام اى مكتبة اخرى غير Gson فى عملية  التحويل لكن سيكون الشرح فى هذه التدوينة باستخدام الـ Gson .

نريد الان انشاء ثلاث اشياء ودائما عند استخدام ريتروفيت  سوف تقوم بعملهم    :

Interface :  

يحتوى على الـ Methods  الخاصة باالاتصالات التى تريد اجراءاها فمثلا اذا كان تطبيقك سيجلب بيانات سوف تقوم بعمل ميثود خاصة بجلب البيانات فى هذه الانترفيس اذا كنت تقوم بارسال بياانات سوف تقوم ايضا بعمل ميثود اخرى لارسال البيانات وهكذا اى عمليات اخرى يمكن ان تفعلها فى الويب سيرفس سوف تضعها فى هذا الـ Interface كـميثود .

Model أو اكثر (فى حالة استقبال جيسون ) :

حيث ان الـ Retrofit تقوم بالتحويل مباشرة من جيسون الى كائنات جافا وبالتالى لن تحتاج الى عمل Json Parsing كما فى الفولى والاتصال العادى  ولذلك استخدمنا مكتبة Gson التى اضفنا السطر الخاص بها  ويكون هذا الـ Model هو عباره عن ممثل أو هيكل الناتج عن الاتصال .

Retrofit Object  :

وهو عباره عن كائن من ريتروفيت نقوم بتمرير الـ Convertor والـرابط اليه .

وسوف نبدأ الان بالتطبيق .

سوف نقوم بإنشاء الـ Interface  وسنطلق عليها HendiwareApi

inteface1

وكما ذكرنا سابقا أننا سنضع بهذا الـ Interface كل الـ Methods  الخاصة بالاتصالات التى نريدها لكن الان سأكتفى بـ Method واحدة وهى getPosts

interfac2

وقمنا بعمل النوع الخاص بهذه الميثود انها عباره عن Call من النوع ResultModel الذى سوف نقوم بانشائه بعد قليل  والـ Call هنا هو عباره عن كائن اتصال من الـ Retrofit وفى السابق فى Retrofit 1.9 كان يمكن عملها بدون Call تكتفى بعمل ResultModel getPosts(); فقط لكن كان من أحد المشاكل فى الاصدار 1.9 من Retrofit عدم القدرة على الغاء الاتصال وكذلك بعض العمليات الاخرى المتعلقة بالاتصال التى كانت سببا فى تغير النظام بعد التحديث وامساك كل ميثود بواسطة Call معين حتى يمكن التحكم فيه بالكامل .

تبقى شىء واحد ينقص هذه الميثود وهو طريقة الاتصال هل بالـ POST ام بالـ GET  او غيرها  وكذلك رابط الملف الذى سيجلب لنا الـ posts .

وكما تلاحظ فى الصورة التالية اضفنا طريقة الاتصال على هيئة Annotation يليها مباشرة اسم الملف الموجود فى الرابط : http://developerhendy.16mb.com/getposts.php

interface5

ولم نضف الرابط هنا سنضيفه فيما بعد فى كائن الـ Retrofit  .

الان انتهينا من عمل الـ Interface نريد عمل Model  ويعتمد الـ Model بشكل كامل على النتائج التى سنحصل عليها من الـ url الخاص بنا وبالنسبة للرابط http://developerhendy.16mb.com/getposts.php فهو عباره عن أوبجكت وبداخله أوبجكت متعددة من النوع Post أى اوبجكت وبداخله ليست من اوبجكتات البوست  كما هو موضح بالصورة

json

فنحن نقوم بعمل ما يسمى بالـ Maping أو تخطيط هذا الـ Json فى الأندرويد عن طريق استخدام الـ Models للوصول الى نفس تنسيق الـ JSON الموجود فى الصورة فهيا نبا نبدأ

سنقوم بإنشاء الـ Model الاب او الاوبجكت الرئيسى الذى يحتوى على ليست الـ posts

retrofitmodel

الان سنقوم بعمل الـ Model الخاص بالـ Post

Jsone

الان انتهينا من عمل الـ Model ولاحظ أن الـ Model يختلف من Json لاخرى وليس هناك نمط ثابت للـ Model بل يعتمد على الـ JSON فاذا كان فى المثال السابق مثلا فى الـ post ليست من  الـ Comments  فسنتعامل معها مثل البوستس سنقوم بعمل list من الـ Comments داخل Model الـ Post ونقوم بعمل Model اخر للكومنت  وهكذا فعملية الـ Json Mapping معتمدة على الـ Json الناتجة عن الاتصال .

الى هنا  انتهينا  من الـ Interface ومن الـ Model وتبقى كائن الـ Retrofit وإجراء الإتصال  وسيتم ذلك بتعريف كائن جديد من الـ Retrofit وتمرير الرابط ليه وكذلك الـ GsonConverter كالتالى   :

Retrofitobject

عظيم الان لدينا الـ Inteface جاهزة وكذلك لدينا Models وكائن الـ Retrofit  الان سنقوم ببدء استخدام كائن الـ retrofit وربطه بالـ API الخاص بنا .

he

سنقوم باستدعاء الميثود getPosts وإمساك نتيجتها فى Call اسمه connection

calll

الان نريد فقط تنفيذ الاتصال السابق ويمكننا ذلك بطريقتين enqueue  او execute حيث ان enqueue يقوم بإنشاء ثريد جديد وتنفيذ الاتصال فيه تلقائيا (غير متزامن ) أما الـ exexute فيقوم بتنفيذ الإتصال فى الثريد الحالى .

سأستخدم enqueue  لكن قبل ذلك سأقوم بنقل كائن الـ retrofit و كذلك الـ api فى onCreate ليتم تنفيذ الكود اثناء فتح الاكتيفيتى

callba

الان يمكننا الحصول على الناتج فى List من الـ Posts ببساطه  من خلال الـ response

response

ولاحظ أننا قمنا بسحب الـ body الخاص بالـ response واذا كنت قرأت تدوينة ما يجب أن يعرفه كل مبرمج عن الـ Http فانت تعلم جيدا ان الـ response الخاص باتصال الـ php يتكون من header و body وناتج الاتصال يكون فى الـ body وبالتالى وصلنا له وباستخدام الميثود getPosts التى قمنا بعملها سابقا فى الـ model المسمى ResultModel حصلنا على List  من النوع Post جاهزة للإستخدام الان كمصدر بيانات مع الـ RecyclerView  او بأى طريقة تحبها ولقد تم شرح تغذية الـ RecyclerView بمصدر بيانات فى تدوينة الـ RecyclerView وما وراء الـ Adapters و الـ Models الجزء الثانى يمكنك الاطلاع عليها اذا كنت لا تعرف كيفية عرض الـ List من Post فى ريسكلر .

لكن الان وبغرض التجربة سأقوم بعمل loop وطباعة الناتج فى textview فقط .

loop

ويكون الناتج كالتالى :

retrofitresult

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

retr

ارسال البيانات باستخدام Retrofit

كما فعلنا فى الـ Volley وقبلها فى HttpUrlConnection باننا اتصلنا بالرابط http://developerhendy.16mb.com/insertuserwithpost.php الذى كان يستقبل منا بيانات المستخدم بطريقة الـ POST  واضفنا مستخدم جديد وهذا ما سنفعله الان ايضا باستخدام Retrofit سوف نذهب لنفس الـ Interface الخاص بالـ API الخاصة بنا  وسنقوم بإضافة ميثود جديدة تسمى adduser(); سنذهب لنفس الـ Interface السابق Hendiware API وسنقوم باضافة الميثود وبما أننا سنرسل بيانات فسنضعها كبارامتيرز فى هذه الميثود ولأن ملف الـ php يقوم باعادة نص فقط مكتوب فيه User Register Successfully اى انه مجرد String لذلك لن نحتاج لعمل Mapping أو Modeling ولا نحتاج لـ Converter انه مجرد String بسيط فقط لذلك سنستخدم الـ ResponseBody كنوع  الاتصال وهو عباره عن الـ Body القادم من الـ Http Response فقط .

methodrespon

مثل الميثود السابقة كنا استخدمنا فيها فى السطر الاول @GET لتعريف نوع الطلب او طريقة الاتصال يليلها اسم الملف بين قوسين قمنا بعمل ذلك هنا ايضا لكن استخدمنا الطريقة POST عن طريق كتابة الـ Annotation المسمى @POST يليه اسم الملف insertuserwithpost.php  ثم فى السطر الثانى بدأنا بتعريف الاتصال Call من النوع ResponseBody لاننا نريد الناتج كـ String فقط لا نريد عمل Mapping ولا Convert لتحويل الجيسون الى كائنات جافا فليس لدينا جيسون هنا اصلا بل مجرد string .

اعطينا الميثود الـبارامتيرز كالتالى @Field  يليها الـ Key  وهو فى السطر الاول username ثم يليلها String usename وترجمة هذا ان الباراميتر الاول فى هذه الميثود هو عبارة عن String سوف يتم اخذه كـ value للـ key المسمى username  . وكذلك فى البارامترز الاخرى يأتى Annotation @Field يليه الـ Key ثم String القيمة التى سوف نمررها لاحقا وهكذا لكل الباراميترز .

الان بما أننا سنقوم بإرسال باراميترز فى مع الـ Request وهى الاسم والباسورد والايميل والعنوان فيجب أن يتم عمل encode لهم  هل تتذكر عندما قمنا بعمل encode للـ url  فى الـ httpurlconnection ؟ ولماذا نفعل ذلك ؟

سوف نقوم بإضافة Annotation يسمى FormUrlEncode لكى تقوم الـ Retrofit بمعالجة الـبارامتر التى سنرسلها  حيث أننا بالـFormUrlEncode نخبر الـ Retrofit أننا سنرسل بيانات عادية نصية فقط واذا كنا سنرسل بيانات اخرى  كالملفات فإننا نستخدم Annotation اخر خاص بالـ Multipart لكن الان نحن نرسل بيانات عادية نصية فسنستخدم FormUrlEncoe

addusermethod

عظيم الان لدينا كائن الريتروفيت جاهز مسبقا  للميثود السابقة سنعدل تعديل بسيط عليه لأنى كسول لا اريد انشاء كائن اخر  سنقوم بحذف الـaddConvertor  فحاليا لا نحتاجه لاننا لم نقم بعمل models او mapping للـجيسون والراجع لنا ليس جيسون أصلا  كل ما سنفعله هو انشاء call لهذه الـ Method فى الـ MainActivity   كاالتالى  وإعطائه البارامترز مع الملاحظة انى اقوم بذلك الان للتجربة فقط لكن عندما تقوم باستخدامها فى تسجيل الدخول مثلا فستقوم

conv

سنقوم باستخدم enqueue لتنفيذ الـ call

retrofitcall

والان سنقوم بتشغيل البرنامج

suce

وكلمة User Add Successfuly ناتجة عن ملف الـ php وأن التسجيل تم بنجاح والان اصبحت قادرا على اجراء اتصال بالانترنت من خلال Retrofit سواء لجلب البيانات أو لارسال البيانات .

هناك الكثير من المقالات التى تتحدث عن سرعة ريتروفيت  او تقارنها مع فولى والطريقة العادية  وهذه الصورة من أحد المقالات  تبين سرعة Retrofit مقارنة بـ volley والطريقة العادية

3u6s6

وسواء كانت الصورة  صحيحة أم لا فالامر متروك لك للتجربة ولاختيار ما تجده يناسبك من هذه الطرق .

الخلاصة : 

نقوم بإنشاء Interface نضع فيه الـ Methods  الذى سنستخدمها  فى تطبيقنا  لجلب وارسال البيانات ، نقوم بعمل تخطيط للـ Json باستخدام الـ Models نقوم بعمل كائن من ريتروفيت وإنشاء call وعمل enqueue له ويأتينا الناتج فى onResponse فى response.body();  واذا حدث مشكلة نجد الناتج او الخطأ فى onFailure .

 

مشروع كامل باستخدام Retrofti يمكنك الاطلاع عليه ودارسته والاستفاده منه مساهمة من صديقنا طارق .

 

السابق
الصلاحيات فى أندرويد مارشميلو
التالي
دعم الشاشات المتعددة فى الأندرويد

26 تعليق

أضف تعليقا

  1. Muhammad Mustafa قال:

    اللهم صلي عالنبي 😀 ايه الحلاوة دي

    1. Hendiware قال:

      عليه الصلاة والسلام منورنا يامحمد

  2. Ahmed Ali قال:

    شرح اكتر من رائع مجهود جبار 😀
    انا كنت بدات اشتغل بالفولي بس هجرب المكتبة دي برضو
    بس طلب اخير ياريت الدرس القادم يكون GCM انا مطلبتش منكو حاجه قبل كدا اصل محتاجها ضروري
    معلش لو بتقل علي التيم بتاعكو بس عشم بقا 😀
    مشكورين مقدما 🙂

    1. Hendiware قال:

      لازم تجرب كل حاجه وتختار الافضل بنفسك
      بالنسبة للدرس القادم صعب يكون GCM لأن الـ GCM موضوع كبير شوية ومحتاج شرح مفصل هياخد مننا وقت وممكن نأجله لبعد الـ Services لانه بيعتمد عليه لازم التدوينة تبقى شارحة كل حاجه بالتفصيل ومفيهاش اجزاء مبهمه بشكل كبير حتى بنفكر يكون فيديو فتصبر بقى شوية أما لو عندك بروجكت مستعجل جدا ولازم تعمله push notification ابعتلنا على الميل mail@hendiware.16mb.com وهخلى حد من التيم ينفذهولك فى تطبيقك أو يبعتلك الملفات الى هتستخدمها ويقولك تشغله ازاى مؤقتا .

      1. Ahmed Ali قال:

        خلاص تمام
        هستني مفيش مشاكل 🙂

  3. محمد قال:

    كلمة شكرا قليلة عليك
    انت فنااااااااااان
    رغم اني قوي في اللغة الانجليزية
    الا ان موضوع [ RecyclerView + Retrofit ]
    ما فهمتهم الا من خلال هذه المدونة العظيمة
    من صميم القلب ارسل لك كل الشكر والتقدير
    بارك الله فيك …

    1. Hendiware قال:

      تسلم صديقى على كلامك الرائع ^_^

  4. ahmad قال:

    ما شاء الله شرح جميل .. صديقي ممكن أساعد في كتابة شرح بأمور أخرى ..

    1. Hendiware قال:

      تفقد ايميلك يا أحمد

  5. slimen قال:

    شرح اكتر من رائع مجهود جبار

  6. omar قال:

    شرح ممتاز اخي الله يعطيك العافية .
    عندي سوال لو سمحت .. اقوم يعمل برنامج للاتصال ب Foursquare Api باستخدام Retrofir و عرض في قائمة الاماكن القريبة من مكان محدد, لم أستطع الحصول على رد من الموقع بعد الاتصال و دايما يذهب الى onfailer . هل من مساعدة ؟
    شكرا جزيلا

  7. osama قال:

    fe sa3at jsonArrays mabyeb2aash leeha esm fa de ezay at3amel ma3aha bel retrofit?

    1. Hendiware قال:

      مرحبا اسامة برجاء طرح سؤالك باللغة العربية .

      1. osama قال:

        فى ساعات json arrays ملهاش اسم فى الdatabase , ازاى أ parse them

        1. Naji قال:

          محدش رد ليه ؟

          1. mohamed hazem قال:

            good qustion and we wait the answer..^_^

  8. محمد كمال قال:

    كلام جميل جدا .. بارك الله فيك

    في حال ارنا ظهور ProgressDialoge اين يجب وضعه .. واين يجب الانتهاء من ال Dialog

    1. Hendiware قال:

      فى بداية الـ call قبل البداية وفى onResponse يوضع dismiss

      1. محمد كمال قال:

        اوك تمام .. جهودك مشكورة

  9. محمد كمال قال:

    لو سمحت ..
    انا استدعيت اكثر من Link , وكل Link فيه اكثر من Array
    بعد ما يتم استدعات ال JSON وتخزينه في Data Base .. اريد ان ينتقل الى صفحة اخرى عند الانتهاء من تحميل كافة ال Array
    فاين يجب وضع ال intent
    مع جزيل الشكر

  10. َ Anoir قال:

    شكرا

  11. twiti قال:

    السلام عليكم انا عملت GridLayout وحبيت لما تكون بالشكل الافقي تعرض لي عدد معين من الاعمدة وفي الشكل العمودي شكل مختلف
    والكود كالتالي
    public Activity getActivity(){
    Context context=this;

    while (context instanceof ContextWrapper){
    if (context instanceof Activity){
    return (Activity)context;
    }
    context = ((ContextWrapper)context).getBaseContext();
    }
    return null;

    }
    if (getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
    recyclerView.setLayoutManager(new GridLayoutManager(this, 2));
    } else {
    recyclerView.setLayoutManager(new GridLayoutManager(this, 4));
    }
    لكن يبقا لي المشكل البرنامج لا يعمل ويعطيني عباره على ال
    instenceof
    مضمونها انها
    nullable
    ممكن مساعدة رجاء هذا موضوع تخرجي وما بقا وقت

  12. عبدالله قال:

    كنز عظييييييم 🔥❤😭😭
    لي ثلات ايام بفتش في الكلام دا و م لقيت زول يشرحو

  13. Tareq Amn قال:

    مقالاتكم جدا رائعة بحييكم على المجهود الجبار هاد ^_^

  14. فادي قال:

    دائما بداية تعلم اي شيء جديد
    يبدأ من عندكم
    جزاكم الله خيرا

  15. Ismail Amassi قال:

    يسلموو ايديك على هيك شرح حلوو

اترك تعليقاً

هذا الموقع يستخدم Akismet للحدّ من التعليقات المزعجة والغير مرغوبة. تعرّف على كيفية معالجة بيانات تعليقك.