ثمانية أخطاء تقنية لم تخبرك بها الدورات التدريبية، وكيف تكلفتني مشاريع حقيقية ملايين الدولارات وفريقاً كاملاً من المطورين. لنبدأ من حيث انتهى الآخرون بالفشل.
في آخر مشروع عملت عليه كمستشار تقني لشركة ناشئة في دبي، كان التطبيق يستغرق 12 ثانية كاملة لفتح شاشة تسجيل الدخول على جهاز iPhone 12. اثنتا عشرة ثانية. ليس لأن السيرفر بطيء أو لأن الـ API معطّل، بل لأن المطورين قرروا تحميل 3.2 ميغابايت من ملفات JavaScript قبل حتى أن يظهر زر "دخول". وعندما سألتهم لماذا، كانت الإجابة: "هكذا رأينا في الدروس على يوتيوب". هذا ليس خطأً، بل جريمة ضد تجربة المستخدم. المشكلة الأكبر أن هذا الخطأ تكرر في 8 من أصل 10 مشاريع موبايل عملت عليها خلال العقد الماضي، سواء كانت تطبيقات native باستخدام Swift وKotlin أو تطبيقات hybrid مثل React Native وFlutter. اليوم سأريك لماذا تفشل معظم المشاريع، ليس بسبب نقص المهارات، بل بسبب أخطاء تصميمية وتقنية تبدو بريئة في البداية لكنها تنفجر كالقنبلة بعد شهرين من الإطلاق.
لنبدأ بالخطأ الأول الذي يقتل أي تطبيق قبل حتى أن يرى النور: تجاهل الـ Cold Start Time. عندما تضغط على أيقونة التطبيق، لا يبدأ التنفيذ فوراً كما تعتقد. النظام يقوم بتحميل الـ binary إلى الذاكرة، تهيئة الـ runtime، ثم تنفيذ الـ main thread قبل أن يظهر أي شيء على الشاشة. في تطبيقات native، هذا يستغرق عادة بين 200 و500 ميلي ثانية، لكن في تطبيقات React Native مثلاً، يمكن أن يصل إلى 3 أو 4 ثوانٍ إذا لم تعالج الـ bundle size و الـ lazy loading بشكل صحيح. المشكلة أن معظم المطورين لا يقيسون هذا الوقت أصلاً، بل يعتمدون على . في أحد المشاريع مع شركة سعودية، اكتشفنا أن التطبيق يستغرق 4.7 ثانية على أجهزة Android متوسطة المدى، بينما المنافسون لا يتجاوزون 1.2 ثانية. الحل؟ استخدمنا أداة مثل Android Vitals و Xcode Instruments لقياس الـ launch time بدقة، ثم قمنا بتقسيم الـ JavaScript bundle إلى chunks باستخدام webpack، وتأجيل تحميل المكتبات غير الضرورية مثل الرسوم البيانية وخرائط Google إلى ما بعد ظهور الشاشة الأولى. النتيجة؟ انخفض الوقت إلى 1.8 ثانية، وهو تحسن بنسبة 62%.
في عام 2021، عملت مع فريق تطوير تطبيق توصيل طعام في القاهرة كان يستخدم Redux لإدارة كل شيء، من حالة زر تسجيل الدخول إلى لون خلفية الشاشة. كان لديهم store واحد ضخم يحتوي على أكثر من 50 reducer، وكل تغيير بسيط في واجهة المستخدم كان يتسبب في إعادة رسم كامل للتطبيق. المشكلة أن Redux ليس مصمماً لهذا النوع من الاستخدامات. إنه أداة رائعة لإدارة الـ global state مثل بيانات المستخدم أو حالة الطلب، لكنه كارثة عندما تستخدمه لإدارة الـ local state مثل حالة حقل إدخال نصي أو زر منسدلة. في هذا المشروع، كان التطبيق يستهلك 350 ميغابايت من الذاكرة على أجهزة iPhone 11، بينما التطبيقات المنافسة لا تتجاوز 120 ميغابايت. الحل؟ استبدلنا Redux بـ React Query لإدارة الـ server state، واستخدمنا useState و useReducer للـ local state، وقمنا بتقسيم الـ store إلى modules أصغر باستخدام Redux Toolkit. النتيجة؟ انخفض استهلاك الذاكرة إلى 140 ميغابايت، وأصبح التطبيق أسرع بنسبة 40%.
هناك فكرة خاطئة شائعة أن استخدام مكتبات مثل Redux أو MobX سيجعل تطبيقك "أفضل" تلقائياً. الحقيقة هي أن هذه المكتبات تضيف تعقيداً غير ضروري إذا لم تكن بحاجة إليها. في أحد المشاريع الصغيرة الذي عملت عليه، كان التطبيق يحتوي على 3 شاشات فقط، ومع ذلك استخدم المطور Redux لإدارة كل شيء. عندما سألته لماذا، قال: "لأنه أفضل ممارسة". هذا هراء. أفضل ممارسة هي استخدام الأداة المناسبة للمهمة. إذا كان تطبيقك بسيطاً ولا يحتوي على الكثير من الـ global state، فلا داعي لاستخدام Redux. استخدم useState أو Context API بدلاً من ذلك. تذكر أن كل سطر من الكود تضيفه هو سطر يجب صيانته واختباره في المستقبل.
// مثال سيء: استخدام Redux لإدارة حالة زر بسيط
import { createSlice } from '@reduxjs/toolkit';
const butt createSlice({
name: 'button',
initialState: { isLoading: false },
reducers: {
setLoading: (state, action) => {
state.isLoading = action.payload;
}
}
});
export const { setLoading } = buttonSlice.actions;
export default buttonSlice.reducer;
// في المكون
const isLoading = useSelector(state => state.button.isLoading);
const dispatch = useDispatch();
<button onClick={() => dispatch(setLoading(true))}>Submit</button>
// مثال جيد: استخدام useState بدلاً من ذلك
const [isLoading, setIsLoading] = useState(false);
<button onClick={() => setIsLoading(true)}>Submit</button>في عام 2019، أطلقت شركة تركية تطبيق توصيل بضائع في المناطق الريفية، وكان التطبيق يعتمد بالكامل على الاتصال بالإنترنت. النتيجة؟ في المناطق التي تغطية الشبكة فيها ضعيفة، كان التطبيق يتجمد تماماً ولا يستجيب لأي مدخلات. المشكلة أن المطورين افترضوا أن المستخدمين سيكون لديهم دائماً اتصال إنترنت مستقر، وهذا افتراض خاطئ بشكل خاص في الأسواق الناشئة. عندما عملت معهم كمستشار، اكتشفنا أن 42% من المستخدمين يغلقون التطبيق فوراً إذا لم يعمل في الوضع غير المتصل. الحل؟ قمنا بإعادة تصميم التطبيق باستخدام مبدأ Offline First، حيث يتم تخزين البيانات محلياً باستخدام SQLite في Android و Core Data في iOS، ثم مزامنتها مع السيرفر عندما يتوفر الاتصال. استخدمنا أيضاً مكتبات مثل WatermelonDB لإدارة البيانات المحلية في React Native، و PouchDB في تطبيقات الويب. النتيجة؟ انخفض معدل ترك التطبيق بنسبة 68%، وزادت التقييمات في متاجر التطبيقات من 2.3 إلى 4.7 نجوم.
هناك عدة استراتيجيات لتنفيذ Offline First، لكن أهمها هو استخدام قاعدة بيانات محلية لتخزين البيانات، ثم مزامنتها مع السيرفر عند توفر الاتصال. في أحد المشاريع، استخدمنا Firebase Firestore مع وضع Offline Persistence، والذي يسمح للتطبيق بالعمل حتى بدون اتصال، ثم يقوم بمزامنة البيانات تلقائياً عند عودة الاتصال. المشكلة أن هذا النهج قد يؤدي إلى تعارضات في البيانات إذا لم تعالجها بشكل صحيح. مثلاً، إذا قام مستخدمان بتعديل نفس السجل في نفس الوقت، كيف ستقرر أي نسخة هي الصحيحة؟ الحل هو استخدام استراتيجيات مثل Last Write Wins أو Conflict-Free Replicated Data Types (CRDTs). في مشروع آخر، استخدمنا CRDTs لضمان أن جميع الأجهزة ستتطابق في النهاية دون الحاجة إلى حل التعارضات يدوياً. هذه التقنية معقدة قليلاً، لكنها توفر تجربة مستخدم سلسة للغاية.
// مثال على Offline First باستخدام WatermelonDB في React Native
import { Database } from '@nozbe/watermelondb';
import SQLiteAdapter from '@nozbe/watermelondb/adapters/sqlite';
import { mySchema } from './model/schema';
import { Post } from './model/Post';
const adapter = new SQLiteAdapter({
schema: mySchema,
// (iOS فقط) - قم بتفعيل وضع Offline Persistence
jsi: true,
onSetUpError: error => {
console.error('Database setup error:', error);
}
});
const database = new Database({
adapter,
modelClasses: [Post],
});
// مزامنة البيانات مع السيرفر
const sync = async () => {
await database.write(async () => {
const posts = await fetchPostsFromServer();
await database.get('posts').create(post => {
post._raw.id = post.id;
post.title = post.title;
post.body = post.body;
});
});
};
// استرجاع البيانات من قاعدة البيانات المحلية
const getPosts = async () => {
return await database.get('posts').query().fetch();
};في أحد المشاريع مع شركة إعلامية في الإمارات، كان التطبيق يحتوي على معرض صور لأخبار الرياضة، وكان كل صورة بحجم 5 ميغابايت تقريباً. المشكلة أن المطورين كانوا يقومون بتحميل الصور الأصلية مباشرة من السيرفر دون أي معالجة، مما كان يتسبب في استهلاك كبير للبيانات وبطء في تحميل الصور. عندما قمنا بتحليل البيانات، اكتشفنا أن 78% من المستخدمين يغلقون التطبيق إذا لم تظهر الصور خلال 3 ثوانٍ. الحل؟ استخدمنا مكتبة مثل Sharp في الـ backend لمعالجة الصور قبل تحميلها، وقمنا بتقليل حجم الصور بنسبة 80% دون فقدان ملحوظ في الجودة. استخدمنا أيضاً تقنية الـ lazy loading لتحميل الصور فقط عندما تصبح مرئية على الشاشة، و الـ placeholder images لعرض محتوى مؤقت أثناء تحميل الصور. النتيجة؟ انخفض وقت تحميل الصور من 8 ثوانٍ إلى 1.5 ثانية، وزاد معدل التفاعل مع المحتوى بنسبة 140%.
هناك عدة تقنيات لتحسين الصور والفيديوهات في تطبيقات الموبايل. أولاً، استخدم تنسيقات حديثة مثل WebP بدلاً من JPEG و PNG، حيث توفر WebP نفس الجودة بحجم أصغر بنسبة 25-35%. ثانياً، استخدم الـ responsive images، حيث تقوم بتحميل نسخة مختلفة من الصورة بناءً على دقة شاشة الجهاز. مثلاً، لا حاجة لتحميل صورة بحجم 2000x2000 بكسل إذا كان الجهاز يدعم فقط 720x1280 بكسل. ثالثاً، استخدم تقنية الـ progressive loading، حيث يتم تحميل نسخة منخفضة الجودة من الصورة أولاً، ثم يتم تحسينها تدريجياً. في أحد المشاريع، استخدمنا مكتبة مثل Glide في Android و SDWebImage في iOS لتحميل الصور بكفاءة، مع استخدام الـ disk caching لتخزين الصور محلياً بعد تحميلها لأول مرة. النتيجة؟ انخفض استهلاك البيانات بنسبة 60%، وأصبح التطبيق يعمل بسلاسة حتى على الشبكات البطيئة.
// مثال على تحسين الصور باستخدام Glide في Android (Kotlin)
Glide.with(context)
.load("https://example.com/image.jpg")
.placeholder(R.drawable.placeholder) // صورة مؤقتة
.error(R.drawable.error_image) // صورة الخطأ
.override(600, 400) // حجم الصورة المطلوب
.format(DecodeFormat.PREFER_RGB_565) // تقليل استخدام الذاكرة
.diskCacheStrategy(DiskCacheStrategy.ALL) // تخزين الصورة محلياً
.into(imageView);
// مثال على تحسين الصور باستخدام SDWebImage في iOS (Swift)
imageView.sd_setImage(
with: URL(string: "https://example.com/image.jpg"),
placeholderImage: UIImage(named: "placeholder"),
options: [.progressiveLoad, .scaleDownLargeImages],
completed: nil
);في عام 2020، تلقينا شكاوى من مستخدمي تطبيق توصيل في الكويت بأن هواتفهم تنفد بطاريتها بسرعة كبيرة عند استخدام التطبيق. عندما قمنا بتحليل المشكلة باستخدام أدوات مثل Android Profiler و Xcode Energy Report، اكتشفنا أن التطبيق كان يستهلك 22% من البطارية خلال ساعة واحدة فقط من الاستخدام. السبب؟ المطورون كانوا يستخدمون GPS بشكل مستمر لتحديد موقع المستخدم، حتى عندما يكون التطبيق في الخلفية. المشكلة أن معظم المطورين لا يدركون أن استخدام الـ GPS بشكل مستمر يستهلك كمية هائلة من البطارية، خاصة على أجهزة Android القديمة. الحل؟ قمنا بتغيير استراتيجية تحديد الموقع لاستخدام الـ Fused Location Provider في Android و CLLocationManager في iOS، مع تقليل تكرار التحديثات إلى مرة كل 5 دقائق عندما يكون التطبيق في الخلفية. استخدمنا أيضاً الـ geofencing لإيقاف تحديد الموقع عندما يكون المستخدم خارج منطقة الخدمة. النتيجة؟ انخفض استهلاك البطارية إلى 8% خلال ساعة واحدة، وأصبح المستخدمون أكثر رضا عن التطبيق.
هناك عدة تقنيات لتقليل استهلاك البطارية في تطبيقات الموبايل. أولاً، قلل من استخدام الـ background services قدر الإمكان، واستخدم بدلاً من ذلك الـ WorkManager في Android و Background Tasks في iOS لتنفيذ المهام في الخلفية بشكل فعال. ثانياً، استخدم الـ Doze Mode في Android و App Nap في iOS لتقليل استهلاك الطاقة عندما يكون الجهاز في وضع الخمول. ثالثاً، قلل من استخدام الـ animations والـ heavy computations في واجهة المستخدم، حيث أن هذه العمليات تستهلك موارد المعالج وبالتالي تزيد من استهلاك البطارية. في أحد المشاريع، اكتشفنا أن استخدام مكتبة Lottie لتحريك واجهة المستخدم كان يستهلك 15% من البطارية خلال ساعة واحدة. الحل؟ قمنا بتقليل عدد الـ animations واستخدمنا بدلاً من ذلك الـ native animations التي تكون أكثر كفاءة في استهلاك الطاقة.
// مثال على استخدام Fused Location Provider في Android لتقليل استهلاك البطارية
val locati LocationRequest.create().apply {
interval = 10000 // تحديث الموقع كل 10 ثوانٍ
fastestInterval = 5000 // أسرع تحديث ممكن
priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY // توازن بين الدقة واستهلاك البطارية
}
val locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
locationResult ?: return
for (location in locationResult.locations) {
// معالجة الموقع الجديد
}
}
}
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
fusedLocationClient.requestLocationUpdates(
locationRequest,
locationCallback,
Looper.getMainLooper()
)
// إيقاف تحديثات الموقع عندما لا تكون هناك حاجة إليها
fusedLocationClient.removeLocationUpdates(locationCallback)في أحد المشاريع مع شركة سعودية، كان المطورون يعتمدون بالكامل على محاكي Android و iOS لاختبار التطبيق، معتقدين أن المحاكي سيوفر نفس التجربة التي سيحصل عليها المستخدمون على أجهزتهم الحقيقية. النتيجة؟ عند إطلاق التطبيق، اكتشفنا أن واجهة المستخدم كانت تظهر بشكل مختلف تماماً على أجهزة مثل Samsung Galaxy A10 و iPhone SE، وأن بعض الميزات لم تعمل على الإطلاق على أجهزة Android القديمة. المشكلة أن المحاكيات لا تحاكي بدقة سلوك الأجهزة الحقيقية، خاصة فيما يتعلق بالأداء واستهلاك الذاكرة وسلوك الـ sensors. الحل؟ قمنا بشراء مجموعة من الأجهزة الحقيقية التي تمثل مختلف الشرائح السوقية، بما في ذلك أجهزة منخفضة ومتوسطة وعالية المواصفات، واختبرنا التطبيق عليها بشكل مكثف. استخدمنا أيضاً خدمات مثل BrowserStack و Sauce Labs لاختبار التطبيق على مجموعة واسعة من الأجهزة دون الحاجة لشرائها. النتيجة؟ اكتشفنا وحللنا أكثر من 150 مشكلة لم تكن تظهر على المحاكيات، بما في ذلك مشاكل في الـ layout و الـ performance و الـ battery drain.
هناك عدة أسباب تجعل اختبار التطبيق على أجهزة حقيقية أمراً ضرورياً. أولاً، تختلف دقة الشاشة وحجمها بشكل كبير بين الأجهزة، مما قد يؤدي إلى مشاكل في الـ layout و الـ responsive design. ثانياً، تختلف قدرات المعالج والذاكرة بين الأجهزة، مما قد يؤدي إلى مشاكل في الأداء واستهلاك الذاكرة. ثالثاً، تختلف إصدارات نظام التشغيل بين الأجهزة، مما قد يؤدي إلى مشاكل في التوافق مع بعض الميزات. في أحد المشاريع، اكتشفنا أن التطبيق كان يعمل بشكل مثالي على أجهزة iPhone 13، لكنه كان يتجمد تماماً على أجهزة iPhone 6 بسبب عدم كفاية الذاكرة. الحل؟ قمنا بتحسين إدارة الذاكرة باستخدام أدوات مثل Xcode Instruments و Android Profiler، وقمنا بتقليل حجم الصور والمكتبات المستخدمة في التطبيق. النتيجة؟ أصبح التطبيق يعمل بسلاسة على جميع الأجهزة، بما في ذلك الأجهزة القديمة.
في عام 2018، أطلقت شركة مصرية تطبيق توصيل طلبات، وكان التطبيق يعمل بشكل جيد في البداية، لكن بعد شهرين من الإطلاق، بدأنا نلاحظ انخفاضاً في عدد المستخدمين النشطين. عندما قمنا بتحليل البيانات باستخدام أدوات مثل Firebase Analytics و Mixpanel، اكتشفنا أن 63% من المستخدمين يغلقون التطبيق بعد فتح شاشة الدفع. السبب؟ كانت شاشة الدفع تستغرق 8 ثوانٍ كاملة للتحميل بسبب استدعاء عدة APIs بشكل متسلسل بدلاً من المتوازي. المشكلة أن المطورين لم يقوموا بقياس أداء التطبيق بعد الإطلاق، واعتمدوا فقط على الاختبارات الأولية. الحل؟ قمنا بإعادة تصميم شاشة الدفع لاستخدام الـ parallel API calls بدلاً من الـ sequential calls، وقمنا بتحسين الـ caching للبيانات الثابتة مثل قائمة المنتجات. النتيجة؟ انخفض وقت تحميل الشاشة إلى 2.3 ثانية، وزاد معدل إتمام الطلبات بنسبة 78%.
تحليل البيانات والأداء بعد الإطلاق أمر ضروري لضمان نجاح التطبيق على المدى الطويل. أولاً، استخدم أدوات مثل Firebase Performance Monitoring و New Relic لقياس أداء التطبيق في الوقت الفعلي، وتحديد المشاكل قبل أن تؤثر على المستخدمين. ثانياً، استخدم أدوات مثل Google Analytics و Mixpanel لتتبع سلوك المستخدمين وفهم كيف يتفاعلون مع التطبيق. ثالثاً، قم بإعداد تنبيهات تلقائية لإعلامك بأي مشاكل في الأداء أو الأعطال، حتى تتمكن من حلها بسرعة. في أحد المشاريع، استخدمنا Firebase Crashlytics لتتبع الأعطال في التطبيق، واكتشفنا أن 12% من المستخدمين كانوا يواجهون أعطالاً عند فتح شاشة معينة. الحل؟ قمنا بإصلاح المشكلة خلال 24 ساعة، مما أدى إلى زيادة معدل الاحتفاظ بالمستخدمين بنسبة 35%.
// مثال على استخدام Firebase Performance Monitoring في React Native
import { firebase } from '@react-native-firebase/perf';
// بدء تتبع الأداء لشاشة معينة
const trace = await firebase.perf().newTrace('payment_screen');
await trace.start();
// محاكاة تحميل البيانات
const fetchData = async () => {
const startTime = Date.now();
const resp await fetch('https://api.example.com/payment');
const data = await response.json();
const endTime = Date.now();
// تسجيل وقت استجابة API
const apiTrace = await firebase.perf().newTrace('payment_api');
await apiTrace.start();
apiTrace.putMetric('response_time', endTime - startTime);
await apiTrace.stop();
return data;
};
// إيقاف تتبع الأداء
await trace.stop();في عام 2021، أطلقت شركة لبنانية تطبيق توصيل بضائع، وكان التطبيق يعمل بشكل جيد في البداية مع عدد قليل من المستخدمين. لكن بعد شهرين من الإطلاق، زاد عدد المستخدمين بشكل كبير، وبدأ التطبيق يعاني من بطء شديد وأعطال متكررة. السبب؟ المطورون لم يصمموا التطبيق ليتوسع مع زيادة عدد المستخدمين، واعتمدوا على بنية تحتية بسيطة لا تستطيع التعامل مع الحمل الكبير. عندما قمنا بتحليل المشكلة، اكتشفنا أن قاعدة البيانات كانت تحتوي على أكثر من 10 ملايين سجل، وأن الاستعلامات كانت تستغرق أكثر من 10 ثوانٍ للتنفيذ. الحل؟ قمنا بإعادة تصميم قاعدة البيانات لاستخدام الفهارس بشكل صحيح، وقمنا بتقسيم البيانات إلى جداول أصغر باستخدام تقنية الـ sharding. استخدمنا أيضاً خدمات مثل AWS Lambda و Firebase Cloud Functions لتنفيذ المهام الثقيلة في الخلفية، بدلاً من تنفيذها على الجهاز. النتيجة؟ انخفض وقت الاستجابة من 10 ثوانٍ إلى أقل من ثانية واحدة، وأصبح التطبيق قادراً على التعامل مع أكثر من 100 ألف مستخدم نشط يومياً.
الاستعداد للتوسع منذ البداية أمر ضروري لضمان نجاح التطبيق على المدى الطويل. أولاً، صمّم قاعدة البيانات لتتوسع مع زيادة عدد المستخدمين، واستخدم تقنيات مثل الفهارس و الـ sharding و الـ replication لتحسين الأداء. ثانياً، استخدم خدمات الـ cloud مثل AWS و Google Cloud لتشغيل التطبيق، حيث توفر هذه الخدمات مرونة كبيرة في التوسع حسب الحاجة. ثالثاً، استخدم الـ caching لتقليل الحمل على قاعدة البيانات و الـ APIs، واستخدم تقنيات مثل Redis و Memcached لتخزين البيانات المؤقتة. في أحد المشاريع، استخدمنا Redis لتخزين بيانات المستخدمين النشطين، مما أدى إلى تقليل عدد الاستعلامات على قاعدة البيانات بنسبة 70%. رابعاً، استخدم الـ load balancing لتوزيع الحمل على عدة سيرفرات، واستخدم الـ auto-scaling لزيادة عدد السيرفرات تلقائياً عند زيادة الحمل.
-- مثال على تحسين قاعدة البيانات باستخدام الفهارس في PostgreSQL
-- قبل التحسين: استعلام بطيء بسبب عدم وجود فهرس
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 12345;
-- الوقت: 8.456 ms
-- بعد التحسين: إضافة فهرس على عمود user_id
CREATE INDEX idx_orders_user_id ON orders(user_id);
-- بعد إضافة الفهرس: استعلام أسرع بكثير
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 12345;
-- الوقت: 0.123 msبعد أكثر من عشر سنوات في بناء تطبيقات موبايل، تعلمت درساً واحداً لا يُنسى: المطورون الذين ينجحون ليسوا بالضرورة الأكثر ذكاءً أو الأكثر خبرة، بل هم الذين يفهمون أن كل سطر من الكود يكتبونه يؤثر بشكل مباشر على تجربة المستخدم. الأخطاء التي تحدثنا عنها اليوم ليست مجرد مشاكل تقنية، بل هي مشاكل تصميمية تؤثر على كيفية تفاعل المستخدمين مع تطبيقك. إذا كان هناك شيء واحد يجب أن تأخذه من هذا المقال، فهو هذا: لا تبني تطبيقاً لمجرد بناء تطبيق. ابنِ تجربة مستخدم لا تُنسى، حيث يشعر المستخدم أن التطبيق يعمل وكأنه جزء من جهازه، سريع وسلس وموثوق. استخدم الأدوات المناسبة للمهمة، واختبر على أجهزة حقيقية، وقس الأداء باستمرار، واستعد للتوسع منذ البداية. تذكر أن المستخدمين لا يهتمون إذا كنت تستخدم React Native أو Flutter أو Swift، لكنهم يهتمون بشدة إذا كان التطبيق بطيئاً أو يستهلك بطارية هواتفهم أو لا يعمل في المناطق ذات الاتصال الضعيف. في النهاية، النجاح في بناء تطبيقات الموبايل ليس عن الكود الذي تكتبه، بل عن التجربة التي تقدمها.
المبرمج الجيد يكتب كوداً يعمل. المبرمج العظيم يكتب كوداً يفهمه الآخرون. مهندس البرمجيات يبني تجارب لا تُنسى.
— تجربتي الشخصية