خطأ في infinite scroll
السلام عليكم ورحمة الله وبركاته
استخدم ionic3 لمشروع تطبيق اخباري
وواجهتني مشكلة في infinite scroll !!
اول مرة استخدم هذي الميزة
لكن سويت الكود في البداية ما تظهر معي الاخبار!
ومع سحب الشاشة للاعلى يظهر لي خبر ١ ثم مع السحب مرة اخرى تظهر باقي الاخبار؟
للخبراء هل الكود التالي صحيح ام خطأ؟
Html
<ion-header>
<ion-toolbar color="dark">
<ion-segment [(ngModel)]="selectThe">
<ion-segment-button value="news">
اخبار
</ion-segment-button>
<ion-segment-button value="article">
مقالات
</ion-segment-button>
</ion-segment>
</ion-toolbar>
</ion-header>
<ion-content id="page7" class="ion-content">
<div *ngFor="let segmentitem of getItems(selectThe)">
<div [hidden]=segmentitem.hide>
<ion-list *ngIf="segmentitem.col == 'newsitems'">
<ion-item class="ion-item" *ngFor="let item of newsitems | async" (click)="showNewsInfo(item)">
<img [src]="item.img" width="100px" height="90px" item-start>
<h1 text-wrap>{{item.title}}</h1>
</ion-item>
</ion-list>
<ion-list *ngIf="segmentitem.col == 'artitems'">
<ion-item class="ion-item" *ngFor="let item of artitems | async">
<img [src]="item.img" (click)="showTopicInfo(item)" width="100px" height="90px" item-start>
<h1 text-wrap>{{item.title}}</h1>
<p>
<span class="writer">{{ item.author }}</span> <span class="time"> {{ item.date | amLocale:'ar-sa' | amTimeAgo }}</span>
</p>
</ion-item>
</ion-list>
</div>
</div>
<ion-infinite-scroll (ionInfinite)="loadMore($event)" loadingSpinner="bubbles">
<ion-infinite-scroll-content></ion-infinite-scroll-content>
</ion-infinite-scroll>
</ion-content>
كود ts
import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';
// Connect Page with Firebase
import { AngularFirestore, AngularFirestoreCollection } from 'angularfire2/firestore';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import { ViewnewsPage } from '../viewnews/viewnews';
import { ViewtopicPage } from '../viewtopic/viewtopic';
// News interface
interface Newsinf {
title: string;
img: string;
slideImg: string;
keywords: string;
desc: string;
newsAuthor: string;
newsDate: string;
text: string;
isShown: boolean;
isSlide: boolean;
newsViews: number;
newsLikes: number;
id?: any;
}
// Article interface
interface Artinf {
title: string;
img: string;
author: string;
keywords: string;
essay: string;
date: string;
id?: any;
}
@Component({
selector: 'page-news',
templateUrl: 'news.html'
})
export class NewsPage {
selectThe = 'news';
select: any = {
'news': [
{
hide: false,
col: 'newsitems'
}
],
'article': [
{
hide: false,
col: 'artitems'
}
]
};
newsCol: AngularFirestoreCollection<Newsinf>;
newsitems: Observable<Newsinf[]>;
artCol: AngularFirestoreCollection<Artinf>;
artitems: Observable<Artinf[]>;
title: string;
img: string;
slideImg: string;
keywords: string;
desc: string;
newsAuthor: string;
newsDate: string;
text: string;
isShown: boolean;
isSlide: boolean;
newsViews: number;
newsLikes: number;
essay: string;
date: string;
id?: any;
limit = 1;
maximumNews = 100;
constructor(public navCtrl: NavController, public navParams: NavParams,
public afs: AngularFirestore) {}
getItems(type: any) {
return this.select[type];
}
ngOnInit() {
this.artCol = this.afs.collection('articles', ref => {
return ref.orderBy('date', 'desc').limit(this.limit)
});
this.artitems = this.artCol.valueChanges();
}
showNewsInfo(item){
this.navCtrl.push(ViewnewsPage, item);
}
showTopicInfo(item) {
this.navCtrl.push(ViewtopicPage, item);
}
loadNews(infiniteScroll?) {
this.newsCol = this.afs.collection('news', ref => {
return ref.orderBy('newsDate', 'desc').limit(this.limit);
});
this.newsitems = this.newsCol.valueChanges();
if (infiniteScroll) {
infiniteScroll.complete();
}
}
loadMore(infiniteScroll) {
this.limit++;
this.loadNews(infiniteScroll);
if (this.limit === this.maximumNews) {
infiniteScroll.enable(false);
}
}
}
وشكرا مقدما على المساعدة 😘
الإجابة الصحيحة
Refresher vs InfiniteScroll
هنالك فرق بين InfiniteScroll و Refresher في Ionic.
نستخدم الـRefresher عند الحاجة إلى تحديث البيانات في الصفحة أو جلب آخر الأخبار الجديدة مثلاُ.
أما الـInfiniteScroll فيستخدم لجلب المزيد من البيانات.
أيضًا الـInfiniteScroll لا يتأثر بسحب الشاشة للأعلى (بعكس الـالـRefresher)، بل يبدأ العمل عند وصول المستخدم إلى أسفل الصفحة، وبالتالي يحب أن تكون الصفحة مليئة البيانات حتى يتم تفعيل الـالـInfiniteScroll. (غير الحد (limit) في الكود إلى ١٠ أو ١٥.
Simple Pagination
ما ترغب في عمله - جلب بيانات إضافية من السيرفر عند الوصول إلى أسفل الصفحة - هو ما يسمى بالـPagination. وهي ما يظهر في صفحة النتائح (أرقام الصفحات) عند البحث في قوقل مثلاً.
ولجلب الصفحة التالية، فإننا نقول بالتعديل على الـquery بطلب النتائج التالية (offset) للصفحة الحالية، وليس بتغيير الحد (limit) كما هو في الكود المرفق بسؤالك.
لنفترض أن لدينا ٣٠ خبر في قاعدة البيانات (total = 30) والحد لكل صفحة هو ١٠ أخبار (limit = 10)، والصفحة الحالية هي الأولى (page = 1).
لجلب بيانات الصفحة الثانية (page = 2) بإننا نقوم بطلب نفس البيانات مع تجاوز العشرة أخبار التي سبق جلبها. (offset = 10)
مثال باستخادم SQL:
# offset = (page -1) * limit;
# PAGE 1: offset = (1 - 1) * 10 = 0;
SELECT * FROM news LIMIT 10 OFFSET 0;
# PAGE 2: offset = (2 - 1) * 10 = 10;
SELECT * FROM news LIMIT 10 OFFSET 10;
# PAGE 3: offset = (3 - 1) * 10 = 20
SELECT * FROM news LIMIT 10 OFFSET 20;
Ionic Pagination
في ionic نستخدم نفس المبدأ لكن باختلاف أنه لا يوجد لدينا صفحات مرقمة، وبالتالي فيكون الاعتماد على عدد النتائج الحالية، والزيادة عليها بحسب الحد لكل صفحة.
فلو بدأت الشاشة بعرض عشرة أخبار، فإننا نقوم بجلب البيانات التالية مع تجاوز العدد الحالي (١٠)، ليصبح الإجمالي ٢٠، وبعدها نقوم بتجاوز ٢٠ خبرًا وهكذا...
Firestore Pagination
بما أنك تتعامل مع Firestore وليس SQL فالطريقة تختلف قليلاً. يمكنك مراجعة هذه الصفحة لمزيد من التفاصيل.
وكما هو موضح في الكود أدناه، فإن الطريقة تتلخص بأن تقوم بجلب البيانات في المرة الأولى مع تحديد أمرين: الحد (limit)، والترتيب (orderBy).
ولجلب المزيد من البيانات (للصفحة التالية) فإننا نقوم تحديد نفس المعطيات وبنفس القيم (limit & orderBy) مع إضافة (startAt) والتي تماثل الــ(offset).
قيمة startAt هي قيمة أخر خبر لنفس الـfield المستخدم في orderBy.
تعديلات الكود
في البداية لايمكن استخدام (async pipe) لجلب البيانات الإضافية، بل يجب عليك جلبها أول مرة عند تحميل الصفحة، ومن ثم جلب المزيد عند تفعيل InfiniteScroll
وللقيام بذلك فإننا نحتاج إلى إضافة مصفوفة فارغة وإضافة البيانات (الأخبار) لها في كل مرة نقوم بجلب بيانات جديدة.
articles: Artinf[] = [];
limit = 10;
وعند تحميل الصفحة لأول مرة:
ionViewWillEnter() {
// تحديد البيانات الملطلوبة أول مرة
this.newsCol = this.afs.collection('news', ref => {
return ref.orderBy('newsDate', 'desc').limit(this.limit)
});
// جلب أول مجموعة من الأخبار
this.newsCol.valueChanges().subscribe((items) => {
// إضافة النتائج للمصفوفة
this.news.push(...items);
});
}
ولجلب البيانات الإضافية:
loadMore(infiniteScroll) {
// تحديد البيانات المطلوبة
this.newsCol = this.afs.collection('news', ref => {
return ref
.orderBy('newsDate', 'desc')
.limit(this.limit)
// تحديد البيانات المرغوب تجاوزها
.startAfter(this.news[this.news.length - 1].newsDate);
});
// جلب البيانات الجديدة
this.newsCol.valueChanges().subscribe((items) => {
// إضافة البيانات للمصفوفة
this.news.push(...items);
infiniteScroll.complete();
// infiniteScroll عندما لا يكون هناك بيانات إضافية، نقوم بتعطيل
if (items.length < this.limit || this.maximumNews === this.news.length) {
console.log("Disabling news scroll");
infiniteScroll.enable(false)
}
});
}
الكود بعد التعديل
HTML
<ion-header>
<ion-toolbar color="dark">
<ion-segment [(ngModel)]="selectThe">
<ion-segment-button value="news">
اخبار
</ion-segment-button>
<ion-segment-button value="article">
مقالات
</ion-segment-button>
</ion-segment>
</ion-toolbar>
</ion-header>
<ion-content id="page7" class="ion-content">
<div *ngFor="let segmentitem of getItems(selectThe)">
<div [hidden]=segmentitem.hide>
<ion-item *ngIf="segmentitem.col == 'newsitems'">
<ion-list>
<ion-item class="ion-item" *ngFor="let item of news" (click)="showNewsInfo(item)">
<img [src]="item.img" width="100px" height="90px" item-start>
<h1 text-wrap>{{item.title}}</h1>
</ion-item>
</ion-list>
<ion-infinite-scroll (ionInfinite)="loadMore($event, segmentitem.col)" loadingSpinner="bubbles">
<ion-infinite-scroll-content></ion-infinite-scroll-content>
</ion-infinite-scroll>
</ion-item>
<ion-item *ngIf="segmentitem.col == 'artitems'">
<ion-list>
<ion-item class="ion-item" *ngFor="let item of articles">
<img [src]="item.img" (click)="showTopicInfo(item)" width="100px" height="90px" item-start>
<h1 text-wrap>{{item.title}}</h1>
<p>
<!--<span class="writer">{{ item.author }}</span> <span class="time"> {{ item.date | amLocale:'ar-sa' | amTimeAgo }}</span>-->
</p>
</ion-item>
</ion-list>
<ion-infinite-scroll (ionInfinite)="loadMore($event, segmentitem.col)" loadingSpinner="bubbles">
<ion-infinite-scroll-content></ion-infinite-scroll-content>
</ion-infinite-scroll>
</ion-item>
</div>
</div>
</ion-content>
TS
import { Component } from '@angular/core';
import { IonicPage } from 'ionic-angular';
// Connect Page with Firebase
import { AngularFirestore, AngularFirestoreCollection } from 'angularfire2/firestore';
import 'rxjs/add/operator/map';
import { Subscription } from "rxjs/Subscription";
// News interface
interface Newsinf {
title: string;
img: string;
slideImg: string;
keywords: string;
desc: string;
newsAuthor: string;
newsDate: string;
text: string;
isShown: boolean;
isSlide: boolean;
newsViews: number;
newsLikes: number;
id?: any;
}
// Article interface
interface Artinf {
title: string;
img: string;
author: string;
keywords: string;
essay: string;
date: string;
id?: any;
}
@IonicPage()
@Component({
selector: 'page-news',
templateUrl: 'news.html'
})
export class NewsPage {
selectThe = 'news';
select: any = {
'news': [
{
hide: false,
col: 'newsitems'
}
],
'article': [
{
hide: false,
col: 'artitems'
}
]
};
newsCol: AngularFirestoreCollection<Newsinf>;
artCol: AngularFirestoreCollection<Artinf>;
news: Newsinf[] = [];
articles: Artinf[] = [];
subscription: Subscription[] = [];
title: string;
img: string;
slideImg: string;
keywords: string;
desc: string;
newsAuthor: string;
newsDate: string;
text: string;
isShown: boolean;
isSlide: boolean;
newsViews: number;
newsLikes: number;
essay: string;
date: string;
id?: any;
limit = 10;
maximumNews = 100;
constructor(public afs: AngularFirestore) {
}
getItems(type: any) {
return this.select[type];
}
ionViewWillEnter() {
this.artCol = this.afs.collection('articles', ref => {
return ref.orderBy('date', 'desc').limit(this.limit)
});
this.newsCol = this.afs.collection('news', ref => {
return ref.orderBy('newsDate', 'desc').limit(this.limit)
});
this.subscription.push(this.newsCol.valueChanges().subscribe((items) => {
this.news.push(...items);
}));
this.subscription.push(this.artCol.valueChanges().subscribe((items) => {
this.articles.push(...items);
}));
}
ionViewWillLeave() {
this.subscription.forEach(subscription => {
subscription.unsubscribe();
})
}
showNewsInfo(item) {
// this.navCtrl.push(ViewnewsPage, item);
}
showTopicInfo(item) {
// this.navCtrl.push(ViewtopicPage, item);
}
loadNews(infiniteScroll?) {
this.newsCol = this.afs.collection('news', ref => {
return ref
.orderBy('newsDate', 'desc')
.limit(this.limit)
.startAfter(this.news[this.news.length - 1].newsDate);
});
const sub = this.newsCol.valueChanges().subscribe((items) => {
this.news.push(...items);
infiniteScroll.complete();
if (items.length < this.limit || this.maximumNews === this.news.length) {
console.log("Disabling news scroll");
infiniteScroll.enable(false)
}
});
this.subscription.push(sub);
}
loadArticles(infiniteScroll) {
console.log('Loading more articles');
}
loadMore(infiniteScroll, segment) {
switch (segment) {
case 'newsitems':
this.loadNews(infiniteScroll);
break;
case 'articlesitems':
this.loadArticles(infiniteScroll);
break
}
}
}
الإجابات (2)
Refresher vs InfiniteScroll
هنالك فرق بين InfiniteScroll و Refresher في Ionic.
نستخدم الـRefresher عند الحاجة إلى تحديث البيانات في الصفحة أو جلب آخر الأخبار الجديدة مثلاُ.
أما الـInfiniteScroll فيستخدم لجلب المزيد من البيانات.
أيضًا الـInfiniteScroll لا يتأثر بسحب الشاشة للأعلى (بعكس الـالـRefresher)، بل يبدأ العمل عند وصول المستخدم إلى أسفل الصفحة، وبالتالي يحب أن تكون الصفحة مليئة البيانات حتى يتم تفعيل الـالـInfiniteScroll. (غير الحد (limit) في الكود إلى ١٠ أو ١٥.
Simple Pagination
ما ترغب في عمله - جلب بيانات إضافية من السيرفر عند الوصول إلى أسفل الصفحة - هو ما يسمى بالـPagination. وهي ما يظهر في صفحة النتائح (أرقام الصفحات) عند البحث في قوقل مثلاً.
ولجلب الصفحة التالية، فإننا نقول بالتعديل على الـquery بطلب النتائج التالية (offset) للصفحة الحالية، وليس بتغيير الحد (limit) كما هو في الكود المرفق بسؤالك.
لنفترض أن لدينا ٣٠ خبر في قاعدة البيانات (total = 30) والحد لكل صفحة هو ١٠ أخبار (limit = 10)، والصفحة الحالية هي الأولى (page = 1).
لجلب بيانات الصفحة الثانية (page = 2) بإننا نقوم بطلب نفس البيانات مع تجاوز العشرة أخبار التي سبق جلبها. (offset = 10)
مثال باستخادم SQL:
# offset = (page -1) * limit;
# PAGE 1: offset = (1 - 1) * 10 = 0;
SELECT * FROM news LIMIT 10 OFFSET 0;
# PAGE 2: offset = (2 - 1) * 10 = 10;
SELECT * FROM news LIMIT 10 OFFSET 10;
# PAGE 3: offset = (3 - 1) * 10 = 20
SELECT * FROM news LIMIT 10 OFFSET 20;
Ionic Pagination
في ionic نستخدم نفس المبدأ لكن باختلاف أنه لا يوجد لدينا صفحات مرقمة، وبالتالي فيكون الاعتماد على عدد النتائج الحالية، والزيادة عليها بحسب الحد لكل صفحة.
فلو بدأت الشاشة بعرض عشرة أخبار، فإننا نقوم بجلب البيانات التالية مع تجاوز العدد الحالي (١٠)، ليصبح الإجمالي ٢٠، وبعدها نقوم بتجاوز ٢٠ خبرًا وهكذا...
Firestore Pagination
بما أنك تتعامل مع Firestore وليس SQL فالطريقة تختلف قليلاً. يمكنك مراجعة هذه الصفحة لمزيد من التفاصيل.
وكما هو موضح في الكود أدناه، فإن الطريقة تتلخص بأن تقوم بجلب البيانات في المرة الأولى مع تحديد أمرين: الحد (limit)، والترتيب (orderBy).
ولجلب المزيد من البيانات (للصفحة التالية) فإننا نقوم تحديد نفس المعطيات وبنفس القيم (limit & orderBy) مع إضافة (startAt) والتي تماثل الــ(offset).
قيمة startAt هي قيمة أخر خبر لنفس الـfield المستخدم في orderBy.
تعديلات الكود
في البداية لايمكن استخدام (async pipe) لجلب البيانات الإضافية، بل يجب عليك جلبها أول مرة عند تحميل الصفحة، ومن ثم جلب المزيد عند تفعيل InfiniteScroll
وللقيام بذلك فإننا نحتاج إلى إضافة مصفوفة فارغة وإضافة البيانات (الأخبار) لها في كل مرة نقوم بجلب بيانات جديدة.
articles: Artinf[] = [];
limit = 10;
وعند تحميل الصفحة لأول مرة:
ionViewWillEnter() {
// تحديد البيانات الملطلوبة أول مرة
this.newsCol = this.afs.collection('news', ref => {
return ref.orderBy('newsDate', 'desc').limit(this.limit)
});
// جلب أول مجموعة من الأخبار
this.newsCol.valueChanges().subscribe((items) => {
// إضافة النتائج للمصفوفة
this.news.push(...items);
});
}
ولجلب البيانات الإضافية:
loadMore(infiniteScroll) {
// تحديد البيانات المطلوبة
this.newsCol = this.afs.collection('news', ref => {
return ref
.orderBy('newsDate', 'desc')
.limit(this.limit)
// تحديد البيانات المرغوب تجاوزها
.startAfter(this.news[this.news.length - 1].newsDate);
});
// جلب البيانات الجديدة
this.newsCol.valueChanges().subscribe((items) => {
// إضافة البيانات للمصفوفة
this.news.push(...items);
infiniteScroll.complete();
// infiniteScroll عندما لا يكون هناك بيانات إضافية، نقوم بتعطيل
if (items.length < this.limit || this.maximumNews === this.news.length) {
console.log("Disabling news scroll");
infiniteScroll.enable(false)
}
});
}
الكود بعد التعديل
HTML
<ion-header>
<ion-toolbar color="dark">
<ion-segment [(ngModel)]="selectThe">
<ion-segment-button value="news">
اخبار
</ion-segment-button>
<ion-segment-button value="article">
مقالات
</ion-segment-button>
</ion-segment>
</ion-toolbar>
</ion-header>
<ion-content id="page7" class="ion-content">
<div *ngFor="let segmentitem of getItems(selectThe)">
<div [hidden]=segmentitem.hide>
<ion-item *ngIf="segmentitem.col == 'newsitems'">
<ion-list>
<ion-item class="ion-item" *ngFor="let item of news" (click)="showNewsInfo(item)">
<img [src]="item.img" width="100px" height="90px" item-start>
<h1 text-wrap>{{item.title}}</h1>
</ion-item>
</ion-list>
<ion-infinite-scroll (ionInfinite)="loadMore($event, segmentitem.col)" loadingSpinner="bubbles">
<ion-infinite-scroll-content></ion-infinite-scroll-content>
</ion-infinite-scroll>
</ion-item>
<ion-item *ngIf="segmentitem.col == 'artitems'">
<ion-list>
<ion-item class="ion-item" *ngFor="let item of articles">
<img [src]="item.img" (click)="showTopicInfo(item)" width="100px" height="90px" item-start>
<h1 text-wrap>{{item.title}}</h1>
<p>
<!--<span class="writer">{{ item.author }}</span> <span class="time"> {{ item.date | amLocale:'ar-sa' | amTimeAgo }}</span>-->
</p>
</ion-item>
</ion-list>
<ion-infinite-scroll (ionInfinite)="loadMore($event, segmentitem.col)" loadingSpinner="bubbles">
<ion-infinite-scroll-content></ion-infinite-scroll-content>
</ion-infinite-scroll>
</ion-item>
</div>
</div>
</ion-content>
TS
import { Component } from '@angular/core';
import { IonicPage } from 'ionic-angular';
// Connect Page with Firebase
import { AngularFirestore, AngularFirestoreCollection } from 'angularfire2/firestore';
import 'rxjs/add/operator/map';
import { Subscription } from "rxjs/Subscription";
// News interface
interface Newsinf {
title: string;
img: string;
slideImg: string;
keywords: string;
desc: string;
newsAuthor: string;
newsDate: string;
text: string;
isShown: boolean;
isSlide: boolean;
newsViews: number;
newsLikes: number;
id?: any;
}
// Article interface
interface Artinf {
title: string;
img: string;
author: string;
keywords: string;
essay: string;
date: string;
id?: any;
}
@IonicPage()
@Component({
selector: 'page-news',
templateUrl: 'news.html'
})
export class NewsPage {
selectThe = 'news';
select: any = {
'news': [
{
hide: false,
col: 'newsitems'
}
],
'article': [
{
hide: false,
col: 'artitems'
}
]
};
newsCol: AngularFirestoreCollection<Newsinf>;
artCol: AngularFirestoreCollection<Artinf>;
news: Newsinf[] = [];
articles: Artinf[] = [];
subscription: Subscription[] = [];
title: string;
img: string;
slideImg: string;
keywords: string;
desc: string;
newsAuthor: string;
newsDate: string;
text: string;
isShown: boolean;
isSlide: boolean;
newsViews: number;
newsLikes: number;
essay: string;
date: string;
id?: any;
limit = 10;
maximumNews = 100;
constructor(public afs: AngularFirestore) {
}
getItems(type: any) {
return this.select[type];
}
ionViewWillEnter() {
this.artCol = this.afs.collection('articles', ref => {
return ref.orderBy('date', 'desc').limit(this.limit)
});
this.newsCol = this.afs.collection('news', ref => {
return ref.orderBy('newsDate', 'desc').limit(this.limit)
});
this.subscription.push(this.newsCol.valueChanges().subscribe((items) => {
this.news.push(...items);
}));
this.subscription.push(this.artCol.valueChanges().subscribe((items) => {
this.articles.push(...items);
}));
}
ionViewWillLeave() {
this.subscription.forEach(subscription => {
subscription.unsubscribe();
})
}
showNewsInfo(item) {
// this.navCtrl.push(ViewnewsPage, item);
}
showTopicInfo(item) {
// this.navCtrl.push(ViewtopicPage, item);
}
loadNews(infiniteScroll?) {
this.newsCol = this.afs.collection('news', ref => {
return ref
.orderBy('newsDate', 'desc')
.limit(this.limit)
.startAfter(this.news[this.news.length - 1].newsDate);
});
const sub = this.newsCol.valueChanges().subscribe((items) => {
this.news.push(...items);
infiniteScroll.complete();
if (items.length < this.limit || this.maximumNews === this.news.length) {
console.log("Disabling news scroll");
infiniteScroll.enable(false)
}
});
this.subscription.push(sub);
}
loadArticles(infiniteScroll) {
console.log('Loading more articles');
}
loadMore(infiniteScroll, segment) {
switch (segment) {
case 'newsitems':
this.loadNews(infiniteScroll);
break;
case 'articlesitems':
this.loadArticles(infiniteScroll);
break
}
}
}
لايوجد لديك حساب في عالم البرمجة؟
تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !