فلاتر ودارت في التقنية المالية

أين يناسب Flutter وDart تطبيقات البنوك والمحافظ والتداول، وما الذي يجيدانه، وأين تكمن المخاطر، وكيف تطلق تطبيقًا آمنًا وسريعًا يدعم العربية بفريق تستطيع توظيفه فعلًا في المنطقة.

· · تحديث · 27 دقائق قراءة

رسم توضيحي لتطبيق مصرفي على الجوال مبني بـ Flutter وDart
في هذا المقال

كل منتج تقنية مالية اقتربت منه خاض الجدال نفسه في أسبوعه الأول: بماذا نبني التطبيق؟ ليس دفتر الحسابات، ولا مزوّد اعرف عميلك، ولا الرخصة. التطبيق. في مصر والسعودية والإمارات التطبيق هو المنتج؛ الفرع إجراء شكلي والموقع الإلكتروني مجرد كتيّب تعريفي.

أصبح Flutter وDart الإجابة الافتراضية لدى كثير من المحافظ والبنوك الرقمية وتطبيقات الاستثمار الجديدة في المنطقة. وهذا لا يعني أنهما الإجابة الصحيحة دائمًا. يتناول هذا المقال المنظومة بوصفها موضوعًا كاملًا: أين يناسب Flutter، وأين يؤذي، وكيف تؤمّنه، وما الذي يكلّفه، وكيف تتخذ القرار. كتبته للمؤسسين وقادة التقنية وكبار المهندسين الذين عليهم الدفاع عن الاختيار أمام مجلس إدارة وبنك شريك وجهة رقابية. وليس هذا درسًا تعليميًا؛ عينات الكود الثلاث هنا لتوضيح نقاط عن الأمان ونمذجة الحالة، لا أكثر.

لماذا يهم Flutter في التقنية المالية

الهاتف هو الفرع

في معظم أسواق المنطقة تعيش علاقة المنتج المالي بعميله كاملةً داخل تطبيق الجوال: التسجيل، واعرف عميلك، والتحكم في البطاقات، والتحويلات، والفواتير، والادخار، والنزاعات. التطبيق هو واجهة المتجر بأكملها، والمعيار الذي يُقاس به تحدده أفضل التطبيقات الاستهلاكية على هاتف المستخدم، لا البنوك الأخرى.

لذلك عليك أن تطلق التجربة نفسها على Android وiOS في الوقت نفسه، لأن عملاءك موزعون بين المنصتين: مصر تميل إلى Android، والخليج يميل إلى iOS. وعليك أن تواصل التحسين لسنوات، لأن تطبيقات التقنية المالية لا تكتمل أبدًا. عرض Flutter — قاعدة كود واحدة، وفريق واحد، وترجمة إلى كود أصلي للمنصتين — ينطبق تمامًا على هاتين الحاجتين.

ما هو Flutter فعلًا (وما ليس هو)

Flutter مجموعة أدوات لبناء الواجهات من Google ترسم كل بكسل بنفسها. لا يغلّف عناصر الواجهة الأصلية كما يفعل React Native؛ بل يأتي بمحرك رسم خاص به ويترجم كود Dart مسبقًا إلى كود ARM أصلي. وDart لغة ذات أنواع ثابتة، مع أمان من القيم الفارغة، ومنذ Dart 3 في 2023 جاءت السجلات (records) والأنماط (patterns) والأصناف المختومة (sealed classes).

رسم كل بكسل يعني أن التطبيق يبدو متطابقًا على المنصتين، وهو ما يسهّل اتساق العلامة التجارية وتدقيقات إتاحة الوصول. ويعني أيضًا أنك حين تحتاج إلى Keychain أو Keystore أو القياسات الحيوية أو NFC أو تصديق التطبيق، تعبر قناة منصة (platform channel) وتكتب قليلًا من Kotlin أو Swift. Flutter ليس وسيلة لتجنب الكود الأصلي. إنه وسيلة لكتابة أقل منه بكثير.

لماذا يخدم التوقيت Flutter الآن

تغيّرت ثلاثة أشياء منذ الموجة الأولى من تطبيقات التقنية المالية على Flutter. منح Dart 3 اللغة أدوات النمذجة التي يحتاجها كود المجال الجاد. وحلّ Impeller محل Skia فأزال تقطّع ترجمة الـ shaders الذي كان يُحرج التطبيقات المبكرة في ثوانيها الأولى. وأصبحت إضافات التخزين الآمن والقياسات الحيوية والإشعارات وتقارير الأعطال مصانة ومملة بدرجة معقولة، وهذا بالضبط ما تريده في منتج خاضع للرقابة.

لم يعد السؤال الصحيح "هل يستطيع Flutter بناء تطبيق مصرفي؟" بل "هل يناسب Flutter هذا الفريق وهذه الخطة وهذه الجهة الرقابية؟" وبقية هذا المقال عن السؤال الثاني.

أين يُستخدم

سأذكر فقط الاستخدامات التي وثّقتها الشركات نفسها علنًا؛ أما القائمة الحقيقية فأطول.

استخدامات علنية موثّقة

Nubank، البنك الرقمي البرازيلي وأحد أكبر البنوك الرقمية في العالم بعدد العملاء، اختار Flutter لتطبيقه الاستهلاكي وشرح أسبابه علنًا: فريق واحد، وقاعدة كود واحدة، وتجربة متسقة، وحرية نقل المهندسين بين المزايا.

Google Pay هو الحالة المرجعية الأخرى. كتبت Google علنًا عن إعادة بناء تطبيق Google Pay بـ Flutter بدءًا من الهند، وعن الوفر الناتج عن صيانة قاعدة كود واحدة بدل اثنتين. وتستخدم Google إطار Flutter أيضًا في Google Ads وGoogle Classroom، وهذه إشارة استمرارية: إطار يشغّل تطبيق الدفع الخاص بـ Google لن يُهجر بهدوء.

تطبيق Xianyu من Alibaba كان من أوائل العروض الكبيرة لـ Flutter في فترة الإصدار 1.0 تقريبًا، ودليلًا مبكرًا على أن شاشات Flutter تستطيع العيش داخل تطبيق أصلي قائم. وخارج التمويل، بنت BMW تطبيق My BMW بـ Flutter وتبنّته Toyota لأنظمة السيارات الداخلية: مؤسسات كبيرة خاضعة للرقابة، وهذه إجابة مفيدة حين يقول أحدهم إن Flutter للشركات الناشئة فقط. صفحة عرض Flutter تحتفظ بقائمة منتقاة.

في المنطقة

تطلق عدة محافظ وبنوك رقمية ومزوّدي خدمة الشراء الآن والدفع لاحقًا وتطبيقات وساطة في مصر والسعودية والإمارات تطبيقات Flutter اليوم. أتعمّد عدم تسميتها: معظمها لا يعلن عن منظومته التقنية، ومن يعلن قد يغيّر رأيه. ما يُظهره سوق التوظيف هو أن إعلانات وظائف Flutter في أدوار التقنية المالية في القاهرة والرياض ودبي أصبحت أمرًا معتادًا.

النمط في المنطقة واضح: المنتجات الاستهلاكية الجديدة تختار Flutter افتراضيًا؛ والبنوك القائمة تبقي تطبيقها الأساسي أصليًا وتستخدم Flutter للتطبيقات الفرعية والعلامات الجديدة أو لرحلات محددة تُدمج عبر add-to-app.

نقاط القوة

قاعدة كود واحدة ولغة تصميم واحدة ومتجران

التكلفة الخفية لقاعدتي كود أصليتين ليست الرواتب؛ إنها الانحراف. يطلق Android فحص حد التحويل الجديد قبل iOS بأسبوع، فيسأل الامتثال لماذا يختلف التطبيقان، ويبدأ مدير المنتج بكتابة كلمة "تطابق" في كل تذكرة. يزيل Flutter هذه الفئة من الأخطاء: كل سلوك خاضع للرقابة — الحدود، وشاشات الموافقة، والإفصاحات، وفترات التراجع — موجود مرة واحدة بالضبط.

تحكم بالبكسل في واجهات خاضعة للرقابة

لأن Flutter يرسم واجهته بنفسه، فأنت تتحكم في الخطوط وتشكيل الحروف العربية ونمط الأرقام وموضع الإفصاح وحالات الأزرار على كل جهاز. حين تشترط قواعد حماية المستهلك إفصاحًا في موضع محدد قبل أن يؤكد المستخدم رسمًا ما، تبنيه مرة واحدة كـ widget فيكون متطابقًا في كل مكان.

Dart 3 لغةً للنمذجة

كانت Dart الجزء الأضعف في العرض. أما الآن فالأصناف المختومة مع جمل switch الشاملة، والسجلات، ومطابقة الأنماط، وأمان القيم الفارغة تمنحك معظم ما تقدمه Kotlin أو Swift لنمذجة المجال. التحويل الذي يمكن أن يكون في واحدة من ست حالات هو بالضبط ما صُممت الأصناف المختومة له، وحين يشير المترجم إلى شاشة نسيت حالة needsOtp فهذا عيب حقيقي مُنع قبل وقوعه. صفحة معدّلات الأصناف على dart.dev تشرح هذه الأدوات.

الأدوات والاختبار وإعادة التحميل الفورية

إعادة التحميل الفورية (hot reload) هي الميزة التي يذكرها الجميع ويقلّلون من قيمتها. الجزء المكلف في تطوير التقنية المالية هو المسارات الطويلة متعددة الخطوات: التسجيل، والتحويل مع رمز التحقق، والنزاع. القدرة على تغيير widget ورؤيته في مكانه مع الحفاظ على الحالة تغيّر سرعة تكرار الفريق على تلك المسارات. أضف إلى ذلك اختبارات golden للانحدار البصري ومحللًا يلتقط معظم الأخطاء قبل الترجمة.

موقعه أمام البدائل

المعيار Flutter React Native أصلي (Kotlin + Swift)
اتساق الواجهة عبر المنصات متطابق بحكم البناء قريب تنفيذان منفصلان
اللغة وأمان الأنواع Dart 3 وأنواع مختومة TypeScript بأنواع تدريجية ممتاز
الوصول إلى واجهات أمان النظام قنوات أو إضافات وحدات أصلية مباشر
نموذج الرسم محرك خاص (Impeller) عناصر أصلية عبر JSI أدوات المنصة
تحديثات الكود عبر الهواء غير مدمجة ممكنة لكود JS لا تنطبق
عمق المواهب في المنطقة مبتدئون كثر ومتقدمون قلة يتداخل مع مواهب الويب نادر ومكلف
واجهات مخصصة ثقيلة بالهوية قوي جدًا جيد قوي بضعف التكلفة
مزايا عميقة في النظام (NFC والساعات والودجات) ممكن مع كود أصلي ممكن مع كود أصلي الأفضل

المخاطر والمزالق

الإفراط في الثقة بوعد "اكتب مرة واحدة"

يشارك Flutter الواجهة ومنطق الأعمال. لكنه لا يشارك أجزاء التطبيق المصرفي التي تلامس نظام التشغيل، وهي بالضبط الأجزاء التي يسأل عنها فريق أمن البنك: المفاتيح المرتبطة بـ Keystore، والتصديق، وأعلام النوافذ الآمنة، وSMS Retriever، وNFC. كل منها وحدة أصلية صغيرة. الفريق الذي لا يضم أحدًا مرتاحًا مع Kotlin وSwift سيتعثر فيها، أو سيثبّت أول حزمة على pub.dev تزعم أنها تفعل ذلك دون أن يقرأ مصدرها.

جودة الحزم وسلسلة التوريد

منصة pub.dev سجل عام، ولتطبيقات التقنية المالية مشكلة في سلسلة التوريد لا تعاني منها الألعاب. ثبّت الإصدارات. اقرأ الجانب الأصلي لأي إضافة تلامس الأمان. فضّل الحزم التي يصونها فريق Flutter أو مزوّد معروف، ولا تُدخل أي حزمة دون فحص الأذونات التي يضيفها دمج ملف manifest الخاص بها على Android؛ فعدد التطبيقات المصرفية التي تحمل إذنًا من نوع READ_SMS بسبب اعتمادية غير مباشرة أكبر مما ينبغي.

فجوة الكود الأصلي في فريقك

أكثر إخفاق أراه ليس تقنيًا. إنه فريق Flutter من خمسة أشخاص بلا أي عمق في الكود الأصلي، فتتحول كل مشكلة خاصة بالمنصة — استثناء Keystore على طراز Samsung معين، أو رمز خطأ من Play Integrity — إلى تحقيق يستغرق أيامًا بدل ساعات. وظّف أو درّب شخصًا واحدًا على الأقل في كل فريق يقرأ كود المنصة بطلاقة.

تشتت إدارة الحالة

لا يفرض Flutter أسلوبًا لإدارة الحالة، وقد أنتج المجتمع كثيرًا منها: Provider وRiverpod وBloc وGetX وغيرها. الاختيار أقل أهمية من الاتساق. اختر واحدًا، ودوّن كيف تُبنى الميزة، وافرض ذلك في مراجعة الكود. التطبيق الذي يضم ثلاثة أساليب لإدارة الحالة عبر التسجيل والمدفوعات والبطاقات هو تطبيق يستغرق فيه إيجاد خطأ في التحويل أسبوعًا.

أنماط البنية والتكامل

طبقات التطبيق المصرفي

البنية التي نجحت في خبرتي مملة وصارمة: طبقة عرض (widgets وحالة العرض)، وطبقة تطبيق (حالات الاستخدام وآلات الحالة والتحقق)، وطبقة مجال (الكيانات وأنواع المال والقواعد)، وطبقة بنية تحتية (HTTP والتخزين والقنوات). قاعدتان تبقيان هذا صادقًا: المال ليس double أبدًا، بل نوع Money مبني على وحدات صغرى صحيحة ورمز عملة؛ والـ widgets لا تستدعي البنية التحتية مباشرة أبدًا، بل ترسل أحداثًا إلى حامل حالة يستدعي حالة استخدام.

نمذجة حركة المال كآلة حالة

التحويل ليس نموذجًا مع مؤشر تحميل. إنه آلة حالة ذات انتقالات مشروعة وأخرى غير مشروعة، وغير المشروعة هي التي تكلّف المال: الإرسال مرتين، والتأكيد دون رمز تحقق، وإعادة محاولة فشل غير قابل لإعادة المحاولة.

/// Every state a transfer can be in. Exhaustive switches keep the UI honest.
sealed class TransferState { const TransferState(); }
class Idle extends TransferState { const Idle(); }
class Validating extends TransferState { const Validating(); }
class Submitting extends TransferState {
  const Submitting(this.key); final String key; // idempotency key
}
class NeedsOtp extends TransferState {
  const NeedsOtp(this.key, this.attemptsLeft); final String key; final int attemptsLeft;
}
class Succeeded extends TransferState { const Succeeded(this.reference); final String reference; }
class Failed extends TransferState {
  const Failed(this.reason, {this.retryable = false}); final String reason; final bool retryable;
}

sealed class TransferEvent { const TransferEvent(); }
class Submit extends TransferEvent { const Submit(); }
class Validated extends TransferEvent { const Validated(this.key); final String key; }
class OtpRequired extends TransferEvent { const OtpRequired(this.attemptsLeft); final int attemptsLeft; }
class OtpEntered extends TransferEvent { const OtpEntered(); }
class Confirmed extends TransferEvent { const Confirmed(this.reference); final String reference; }
class Rejected extends TransferEvent {
  const Rejected(this.reason, {this.retryable = false}); final String reason; final bool retryable;
}
class Reset extends TransferEvent { const Reset(); }

/// Pure: the same state and event always give the same next state.
/// Anything not listed is illegal and throws, so the bug is loud in tests
/// instead of double-charging a customer in production.
TransferState reduce(TransferState s, TransferEvent e) => switch ((s, e)) {
  (Idle(), Submit()) => const Validating(),
  (Validating(), Validated(:final key)) => Submitting(key),
  (Submitting(:final key), OtpRequired(:final attemptsLeft)) => NeedsOtp(key, attemptsLeft),
  (NeedsOtp(:final key), OtpEntered()) => Submitting(key),
  (Submitting(), Confirmed(:final reference)) => Succeeded(reference),
  (Validating() || Submitting() || NeedsOtp(), Rejected(:final reason, :final retryable)) =>
    Failed(reason, retryable: retryable),
  (Failed(retryable: true), Submit()) => const Validating(),
  (Succeeded() || Failed(), Reset()) => const Idle(),
  _ => throw StateError('Illegal transition: ${s.runtimeType} on ${e.runtimeType}'),
};

تم الاختبار مع Flutter 3.x / Dart 3.

لاحظ ما يرفضه المخفِّض (reducer): Submit أثناء Submitting غير مشروع، فلا يستطيع النقر المزدوج إنشاء طلب ثانٍ؛ وConfirmed أثناء Failed غير مشروع. يُولَّد مفتاح التكرار الآمن (idempotency key) مرة واحدة عند التحقق ويُحمل عبر خطوة رمز التحقق، حتى تصيب إعادة المحاولة بعد انتهاء المهلة السجل نفسه على الخادم. وإضافة حالة سابعة تصبح خطأ ترجمة في كل شاشة حتى تُعالج.

مخاطبة الخادم: التكرار الآمن وإعادة المحاولة والساعات

شبكات الجوال تنقطع في المصاعد وأنفاق المترو وأثناء التجوال. كل عملية كتابة تحرّك المال تحمل مفتاح تكرار آمن؛ ويعيد العميل المحاولة فقط عند أخطاء الشبكة واستجابات 5xx مع تراجع محدود السقف، ولا يعيدها أبدًا عند 4xx. ولا يثق العميل بساعته أبدًا. وdio هو طبقة HTTP المعتادة لأن اعتراضاته (interceptors) هي المكان الذي يعيش فيه تجديد الرموز وتثبيت الشهادات وتوقيع الطلبات وحجب البيانات الشخصية.

قنوات المنصة وPigeon وFFI

قنوات المنصة هي الطريقة التي يستدعي بها Dart كود Kotlin وSwift. واجهة MethodChannel الخام تعتمد على النصوص ويسهل الخطأ فيها عبر ثلاث لغات؛ أما Pigeon فتولّد روابط آمنة الأنواع من واجهة Dart، وهذا ما تريده للجسور الحساسة أمنيًا. أما مكتبات C مثل حزمة SDK تشفيرية من مزوّد، فيستدعيها dart:ffi مباشرة، ويُفضَّل أن يكون ذلك من isolate خلفي.

add-to-app ومفاتيح الإيقاف

لا يلزم إعادة كتابة تطبيق مصرفي أصلي قائم. يتيح وضع add-to-app في Flutter تضمين محرك Flutter داخل تطبيق أصلي، فتُطلق رحلة جديدة كشاشات Flutter بينما يبقى الباقي أصليًا. المقايضات هي زمن تشغيل المحرك عند الدخول الأول ونظاما تنقّل؛ لكنها الطريقة التي تبدأ بها معظم البنوك الكبيرة التي تتبنى Flutter.

وأيًا كان الشكل، تحتاج كل ميزة تحرّك المال إلى مفتاح إيقاف على الخادم. لا يوفر Flutter تحديثات كود Dart عبر الهواء، لذا تأتي المرونة من الإعدادات البعيدة: الحدود، والمزايا المفعّلة، وإصدارات الترقية الإجبارية. اجلبها عند التشغيل، وخزّنها مؤقتًا، واجعل الفشل مغلقًا لأي شيء يحرّك المال.

الأمان والامتثال

الجهاز معادٍ؛ كل ضابط أدناه موجود بسبب ذلك، والضوابط متعددة الطبقات لأن كل واحد منها يفشل بطريقة معروفة.

التخزين الآمن: غلاف يمكن تدقيقه

رموز الجلسة ورموز التجديد ومفاتيح الجهاز تذهب إلى التخزين الآمن للمنصة: Keychain في iOS والتفضيلات المشفرة المدعومة بـ Keystore في Android. تغلّف حزمة flutter_secure_storage كليهما. غلّفها مرة أخرى بنفسك حتى لا يرى بقية التطبيق الحزمة أبدًا ويصبح تسجيل الخروج استدعاءً واحدًا.

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

/// Tokens never go into SharedPreferences: that is plain XML or a plist on
/// disk, readable on a rooted device and sometimes included in backups.
class TokenStore {
  TokenStore({FlutterSecureStorage? storage})
      : _storage = storage ??
            const FlutterSecureStorage(
              iOptions: IOSOptions(
                accessibility: KeychainAccessibility.first_unlock_this_device,
              ),
            );

  final FlutterSecureStorage _storage;
  final Map<String, String?> _cache = {};

  static const _access = 'access_token';
  static const _refresh = 'refresh_token';

  Future<String?> accessToken() => _read(_access);
  Future<String?> refreshToken() => _read(_refresh);

  Future<void> save({required String access, required String refresh}) async {
    await Future.wait([
      _storage.write(key: _access, value: access),
      _storage.write(key: _refresh, value: refresh),
    ]);
    _cache[_access] = access;
    _cache[_refresh] = refresh;
  }

  /// Call on logout, on a 401 that refresh cannot recover from, and when
  /// the user removes device biometrics. Wipes memory first, then disk.
  Future<void> clear() async {
    _cache.clear();
    await Future.wait([
      _storage.delete(key: _access),
      _storage.delete(key: _refresh),
    ]);
  }

  Future<String?> _read(String key) async {
    if (_cache.containsKey(key)) return _cache[key];
    final value = await _storage.read(key: key);
    _cache[key] = value;
    return value;
  }
}

تم الاختبار مع Flutter 3.x / Dart 3.

يتجنب التخزين المؤقت في الذاكرة رحلة إلى Keychain مع كل طلب، وclear() صريحة لأن الرمز الذي ينجو من تسجيل الخروج أسوأ من الرمز المسرّب. يحمي التخزين البيانات الساكنة فقط، لذا أبقِ الجلسات قصيرة ورموز التجديد دوّارة.

القياسات الحيوية بوابةً لا قفلًا

تعرض حزمة local_auth مطالبة القياسات الحيوية للنظام وتخبرك إن اجتازها المستخدم. هذه بوابة في وجه شخص عابر يحمل هاتفك. ليست دليلًا تشفيريًا، والجهاز مكسور الحماية مع أدوات حقن يستطيع جعل authenticate() تعيد true. للتصعيد في تحويل مرتفع القيمة، اربط العملية بمفتاح في Keystore أو Secure Enclave يتطلب مصادقة المستخدم، ودع الخادم يتحقق من توقيع؛ وهذا كود أصلي خلف قناة.

import 'package:flutter/services.dart';
import 'package:local_auth/local_auth.dart';

/// What the UI needs to know. Nothing else leaks out of the gate.
sealed class BiometricResult {
  const BiometricResult();
}

class BiometricPassed extends BiometricResult { const BiometricPassed(); }
class BiometricUnavailable extends BiometricResult { const BiometricUnavailable(); }
class BiometricNotEnrolled extends BiometricResult { const BiometricNotEnrolled(); }
class BiometricCancelled extends BiometricResult { const BiometricCancelled(); }
class BiometricLockedOut extends BiometricResult { const BiometricLockedOut(); }
class BiometricError extends BiometricResult {
  const BiometricError(this.code);
  final String code;
}

Future<BiometricResult> biometricGate({
  LocalAuthentication? auth,
  String reason = 'Confirm it is you to continue',
}) async {
  final local = auth ?? LocalAuthentication();
  try {
    if (!await local.isDeviceSupported()) return const BiometricUnavailable();
    final enrolled = await local.getAvailableBiometrics();
    if (enrolled.isEmpty) return const BiometricNotEnrolled();

    final ok = await local.authenticate(
      localizedReason: reason,
      options: const AuthenticationOptions(
        biometricOnly: true,    // no PIN or pattern fallback when money moves
        stickyAuth: true,       // survive a brief trip to the background
        useErrorDialogs: false, // we render our own dialogs, Arabic included
      ),
    );
    return ok ? const BiometricPassed() : const BiometricCancelled();
  } on PlatformException catch (e) {
    return switch (e.code) {
      'NotAvailable' => const BiometricUnavailable(),
      'NotEnrolled' => const BiometricNotEnrolled(),
      'LockedOut' || 'PermanentlyLockedOut' => const BiometricLockedOut(),
      _ => BiometricError(e.code),
    };
  }
}

تم الاختبار مع Flutter 3.x / Dart 3.

تجبر النتيجة المختومة كل شاشة على معالجة حالتي "غير مسجّل" و"مقفل" صراحةً: المستخدم الذي أزال بصمته يجب أن يصل إلى شاشة رقم التعريف لا إلى رسالة خطأ عابرة. وينبغي أن يُبطل تسجيل قياس حيوي جديد المفتاح المرتبط بالقياسات الحيوية على الجانب الأصلي؛ فكثير من الجهات الرقابية تعامل البصمة الجديدة كجهاز جديد.

كشف الروت وكسر الحماية وحدوده

تتوقع فرق أمن البنوك والجهات الرقابية كشف الروت وكسر الحماية، وهو يوقف فعلًا الأغلبية الأمينة من الأجهزة الخطرة. لكن أدوات الروت مبنية للاختباء منه، والمهاجم الذي يملك Frida أو وحدة Magisk سيتجاوز أي فحص مكتوب بـ Dart وحدها. عامله كإشارة خطر تغذّي محرك الاحتيال لديك، لا كجدار: نفّذ الفحص في كود أصلي، وكرّره أكثر من مرة، وأرسل النتيجة إلى الخادم.

مقايضات تثبيت الشهادات

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

تصديق التطبيق

يتيح Play Integrity على Android وApp Attest على iOS لخادمك أن يسأل المنصة ما إذا كان الطلب قادمًا من تطبيقك الأصلي على جهاز يجتاز فحوص السلامة. يطلب التطبيق رمزًا مع رقم عشوائي (nonce) يوفره الخادم ويتحقق الخادم منه لدى Google أو Apple؛ تجاهل الرقم العشوائي أو التحقق على الخادم فيصبح الأمر بلا قيمة. تحذير إقليمي واحد: الأجهزة بلا خدمات Google (بعض طرازات Huawei ما زالت شائعة في مصر والخليج) لا تستطيع إصدار أحكام Play Integrity، فقرّر مسبقًا ما تفعله معها.

لقطات الشاشة والتراكبات واختطاف النقر

على Android، يحجب FLAG_SECURE على النافذة لقطات الشاشة وتسجيلها لشاشات الرصيد والبطاقات. أما iOS فلا يستطيع منع لقطات الشاشة؛ يمكنك فقط اكتشافها وتمويه التطبيق في مبدّل التطبيقات. هجمات التراكب — تطبيق خبيث يرسم فوق تطبيقك لالتقاط رقم التعريف — هي التهديد الأكبر على Android: فعّل filterTouchesWhenObscured على العناصر الحساسة، واكشف خدمات إتاحة الوصول غير المعروفة، وارفض التأكيدات القادمة من نافذة محجوبة.

القراءة التلقائية لرمز التحقق بالطريقة الصحيحة

القراءة التلقائية لرموز التحقق عبر الرسائل القصيرة ميزة تحسّن معدل الإتمام، والطريقة الخاطئة لها تجعل متجر Play يرفض تطبيقك. على Android استخدم واجهة SMS Retriever (تحمل الرسالة تجزئة التطبيق ويسلّمك النظام تلك الرسالة وحدها دون إذن للرسائل) أو SMS User Consent. وعلى iOS، اضبط الحقل كحقل رمز لمرة واحدة فتعرض لوحة المفاتيح الرمز. لا تطلب أبدًا الإذن الواسع لقراءة الرسائل.

التعتيم والسجلات والبيانات الشخصية

إصدارات النشر كود Dart مترجم مسبقًا، وهو أصلًا أصعب قراءةً بكثير من حزمة JavaScript. أضف --obfuscate --split-debug-info لإعادة تسمية الرموز واحتفظ بملفات الربط لتقارير الأعطال. يبطئ التعتيم الهندسة العكسية ولا يوقفها؛ ولا أسرار في التطبيق أصلًا. وأسرع طريقة للرسوب في تدقيق هي سطر في السجل: ابنِ طبقة حجب مبكرًا حتى لا تصل الرموز وأرقام البطاقات وIBAN وأرقام الهوية وأرقام الهواتف أبدًا إلى print أو مسجّلك أو أداة تقارير الأعطال، ونظّف المسارات في خطاف ما قبل الإرسال في تلك الأداة. وتذكر أن assert يُحذف في إصدار النشر، وقرّر أي أحداث التحليلات يُسمح لها بحمل المبالغ (لا شيء عادةً).

PCI DSS ومشكلة بيانات البطاقات

في PCI DSS، أبقِ بيانات البطاقة خارج كودك. استخدم SDK مزوّد الدفع أو الحقول المستضافة: يُدخل رقم البطاقة في مكوّن يتحكم فيه المزوّد ويُرمَّز على جانبه، فلا يرى تطبيقك سوى رمز ويبقى معظم نطاق PCI لدى المزوّد. وإن عرضت رقم البطاقة الكامل، فاجلبه لحظة العرض عبر جلسة مصدّقة ولا تخزنه مؤقتًا أبدًا. أما SoftPOS والدفع بلمس الهاتف فيأتيان بنظام اعتماد خاص بهما ويعيشان في حزم SDK أصلية.

الجهات الرقابية: SAMA وCBUAE وCBE وPSD2 وSCA

ينشر البنك المركزي السعودي ومصرف الإمارات المركزي والبنك المركزي المصري أطرًا للأمن السيبراني وحماية المستهلك للمؤسسات المرخصة لديها. تختلف التفاصيل لكن توقعات الجوال متشابهة: مصادقة متعددة العوامل، وربط الجهاز، وتخزين آمن، وكشف الروت، وضوابط الجلسة، وحدود وتنبيهات، وتسجيل التدقيق. وتضيف قواعد المصادقة القوية للعميل في PSD2 الربط الديناميكي: مصادقة الدفع مرتبطة بالمبلغ والمستفيد، ويتحقق الخادم منهما. ولا شيء من هذا خاص بـ Flutter.

العربية والاتجاه من اليمين إلى اليسار وامتثال الواجهة

المستخدم الذي يسيء قراءة شاشة التأكيد يصبح نزاعًا، لذا دعم العربية موضوع أمني. يعالج Flutter النص ثنائي الاتجاه وتشكيل الحروف العربية في المحرك، ويقلب Directionality التخطيطات في اللغات التي تُكتب من اليمين إلى اليسار بشرط أن تكون قد استخدمت الخصائص الاتجاهية (EdgeInsetsDirectional وstart وend بدل left وright). قرّر نمط الأرقام عمدًا: معظم التطبيقات المالية في المنطقة تعرض الأرقام الغربية حتى في الواجهة العربية؛ اجعله إعدادًا للغة. والنص المختلط الاتجاه مثل IBAN داخل جملة عربية هو حيث تختبئ أخطاء التخطيط؛ اختبر كل مبلغ وIBAN وتاريخ باللغتين.

ما يوقفه كل ضابط فعلًا

الضابط يوقف لا يوقف
التخزين الآمن (Keychain / Keystore) قراءة الرموز من نسخة احتياطية أو متصفح ملفات برمجية خبيثة داخل عملية تطبيق مخترقة
بوابة القياسات الحيوية (local_auth) الوصول العابر على هاتف غير مقفل التجاوز بأدوات الحقن على جهاز مكسور الحماية
كشف الروت / كسر الحماية الأغلبية الأمينة من الأجهزة الخطرة الروت المخفي وFrida والأنظمة المبنية للاختباء
تثبيت الشهادات الاعتراض على الشبكات المعادية الهجمات من جهاز معادٍ
تصديق التطبيق التطبيقات المعدّلة والمحاكيات وكثير من الروبوتات الأجهزة بلا خدمات Google
FLAG_SECURE / فحوص التراكب لقطات الشاشة ومعظم التراكبات على Android لقطات الشاشة على iOS

الأداء والتوسع

Impeller ونهاية تقطّع الـ shaders

كان محرك الرسم القديم في Flutter يترجم الـ shaders أثناء التشغيل، فيسبب التلعثم عند أول حركة الذي أكسب Flutter سمعة التقطّع. يترجم Impeller الـ shaders مسبقًا وقت البناء؛ وأصبح الافتراضي على iOS في 2023 وعلى Android في أواخر 2024، مع الرجوع إلى المحرك القديم على الأجهزة التي تفتقر إلى واجهة الرسوميات المطلوبة. ومع ذلك، اختبر على جهاز Android منخفض المواصفات من السوق المحلية، لا على الهاتف الرائد في جيبك.

ميزانية 16 مللي ثانية

عند 60 هرتز يحصل كل إطار على نحو 16 مللي ثانية؛ وعند 120 هرتز على نحو 8. كل ما يعمل على isolate الواجهة يجب أن يتسع في ذلك. المخالفون المعتادون في التطبيقات المصرفية: تحليل كشف JSON كبير على خيط الواجهة، وإعادة بناء الشاشة كلها لأن الحالة تعيش في موضع مرتفع جدًا في الشجرة، وwidgets غير const في قوائم طويلة، وظلال وتمويه على كل بطاقة. قِس أبطأ ثلاث شاشات في DevTools قبل كل إصدار.

الـ isolates للأعمال الثقيلة

تشغّل Dart خيط تنفيذ واحدًا لكل isolate، وللواجهة isolate خاص بها. أي عمل ثقيل — فك ترميز معاملات سنة كاملة، أو توليد كشف PDF، أو تشفير حمولة — مكانه isolate منفصل؛ ويجعل Isolate.run ذلك سطرًا واحدًا. لا تتشارك الـ isolates الذاكرة، فلا يستطيع العامل إفساد حالة الواجهة.

زمن التشغيل وحجم التطبيق

البدء البارد هو أول ما سيقيسه مراجع البنك الشريك. أبقِ main() خفيفة: هيّئ تقارير الأعطال والإعدادات البعيدة، واعرض الإطار الأول، وأجّل كل شيء آخر. أما الحجم، فيحمل تطبيق Flutter بضعة ميغابايتات من المحرك لا يحملها التطبيق الأصلي؛ وبعد ذلك يأتي النمو من الأصول والخطوط. قسّم بحسب ABI، واقتطع خطوطك العربية، وشغّل تحليل الحجم على كل إصدار نشر.

القوائم والكشوف والعرض الافتراضي

سجلات المعاملات أطول القوائم في التطبيق. استخدم ListView.builder أو الـ slivers حتى تُبنى الصفوف الظاهرة فقط، وقسّم الصفحات من الخادم بمؤشر، ولا تضبط أبدًا shrinkWrap: true على قائمة طويلة. ونسّق التواريخ والمبالغ مرة واحدة لكل عنصر في نموذج العرض، لا داخل build.

العمل دون اتصال بلا مال دون اتصال

يجب أن يفتح تطبيق التقنية المالية فورًا دون شبكة ويعرض آخر رصيد معروف مع ختم زمني واضح "حتى تاريخ". ويجب ألا يتيح للمستخدم أبدًا تحريك المال دون اتصال. خزّن محليًا (طبقة SQLite مثل drift)، واقرأ الذاكرة المحلية أولًا، وحدّثها في الخلفية، ودع الخادم المصدر الوحيد لحقيقة الأرصدة. وأي شيء يخصم من حساب ينتظر اتصالًا حيًا وتصديقًا حيًا.

كلام صريح عن الويب وسطح المكتب

يرسم Flutter للويب داخل لوحة canvas مع تنزيل أولي كبير، وتحسين محدود لمحركات البحث، وسلوك في إتاحة الوصول يختلف عن تطبيق DOM. للموقع العام استخدم React أو ما شابه. أما للوحة داخلية خلف تسجيل الدخول، فيتيح لفريق الجوال إعادة استخدام كود المجال، وقد يكون ذلك مكسبًا حقيقيًا. وسطح المكتب مستقر ويعمل جيدًا لأدوات الفروع والصرّافين.

الفريق والتوظيف في المنطقة

خريطة المواهب

أصبح Flutter أكثر مهارات الجوال شيوعًا بين المهندسين في بداية مسيرتهم في مصر وجزء كبير من الخليج. يمنحك ذلك مجمعًا عميقًا في المستويين المبتدئ والمتوسط. أما المجمع المتقدم — من شغّلوا تطبيق Flutter بمتطلبات أمنية حقيقية عبر عدة إصدارات في المتجر — فأقل عمقًا، ومن فيه يعرفون قيمتهم. ومهندسو الكود الأصلي المتقدمون أندر، وغالبًا متركزون في البنوك الكبيرة وشركات الاتصالات. وتتداخل مواهب React Native مع مواهب الويب: سهلة الإيجاد، ونادرًا ما تكون عميقة في مشكلات الجوال الخاصة.

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

الارتقاء بالمهارات من Android وiOS وReact Native

أفضل مهندسي Flutter في التقنية المالية الذين عملت معهم جاؤوا من خلفيات أصلية. مهندس Android الذي يعرف Kotlin يتعلم Dart في أيام، وقد علّمه Compose أو SwiftUI النموذج التصريحي بالفعل. يحمل هؤلاء ما يفتقده فريق Flutter الخالص: يقرؤون تتبع مكدس Keystore ولا يخافون قناة المنصة. أما مهندسو React Native فيأتون بالعقلية التصريحية وغالبًا بحس منتج قوي، ويحتاجون إلى نسيان ردود فعل الأنواع الديناميكية وتوقّع تحديثات الحزم عبر الهواء.

إشارات المقابلة التي تهم

في دور Flutter في التقنية المالية أهتم بالحكم أكثر من المعلومات الصغيرة. هل يستطيع المرشح شرح ما هي إعادة البناء وأين لا ينبغي أن يعيش العمل المكلف؟ هل يستطيع نمذجة تحويل كآلة حالة دون أن يُطلب منه؟ هل يستطيع وصف ما يحميه التخزين الآمن فعلًا وما لا يحميه؟ هل بنى واجهة عربية واصطدم بأخطاء الاتجاه المختلط؟ والسؤال الذي يميز المتقدمين: هل يستطيع فتح مشروع Android أو iOS وإيجاد طريقه فيه؟

وظّف من استُدعوا ليلًا بسبب تطبيق في الإنتاج. الإطار يُتعلم في شهر؛ أما الغريزة بأن التحويل المرسل مرتين خطأ ينهي مسيرة مهنية فلا.

إطار اتخاذ القرار

متى يكون Flutter الخيار الصحيح

Flutter هو الافتراضي الصحيح حين تبني تطبيقًا ماليًا جديدًا للمستهلكين أو الشركات الصغيرة على المنصتين بفريق متواضع الحجم، وحين تهم الهوية واتساق الواجهة، وحين تطلق بالعربية والإنجليزية في عدة دول، وحين تتوقع التكرار لسنوات. ويناسب أيضًا التطبيقات الفرعية حول بنك قائم.

متى يفوز الكود الأصلي

اختر الأصلي حين يكون المنتج هو المنصة: الدفع بلمس الهاتف وSoftPOS، والتكاملات العميقة مع المحافظ، وتطبيقات الساعات، وودجات الشاشة الرئيسية، وCarPlay أو Android Auto، أو أي شيء يعتمد على مزايا نظام تشغيل جديدة في شهر إطلاقها. وابقَ على الأصلي حين يكون لديك فريقان أصليان قويان وتطبيق مستقر. وفكّر في الأصلي حين يكون تقييم الأمن لدى بنك شريك رئيسي مبنيًا حول أدوات أصلية.

متى يفوز React Native

اختر React Native حين تكون المؤسسة بيت React بمنتج ويب كبير، ويتشارك تطبيق الجوال معه منطق أعمال كبيرًا، وتكون خبرة الفريق المتقدمة في TypeScript. وكن حذرًا بشأن التحديثات عبر الهواء: جذابة لفرق المنتج، وغير جذابة للجهات الرقابية والبنوك الشركاء، ومقيّدة بسياسات المتاجر.

مصفوفة القرار

امنح كل معيار وزنًا لوضعك من 1 إلى 5؛ والدرجات دليل توضيحي لا قياس.

المعيار الوزن (لديك) Flutter React Native أصلي
الوقت حتى أول إصدار 5 4 2
تطابق السلوك الخاضع للرقابة 5 4 2
مزايا الأمان على مستوى النظام 3 3 5
واجهة ثقيلة بالهوية ودعم العربية 5 3 4
إعادة استخدام قاعدة كود ويب React 1 5 1
التوظيف في القاهرة والرياض ودبي 4 4 2
الأداء على Android منخفض المواصفات 4 3 5
ارتياح مقيّمي أمن البنوك 4 3 5
المزايا العميقة في المنصة 2 2 5
إعادة الاستخدام في العمليات 4 3 1

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

الأسئلة الشائعة

هل Flutter آمن بما يكفي لتطبيق مصرفي؟

نعم، مع التحفظ الذي ينطبق على كل إطار: الإطار ليس هو الأمان. يُترجم Flutter إلى كود أصلي، ولا يخزن شيئًا بنفسه، ويمنحك وصولًا نظيفًا إلى Keychain وKeystore والقياسات الحيوية والتصديق عبر الإضافات والقنوات. يأتي الأمان من الضوابط التي تنفذها والكود الأصلي الذي يقف خلفها، لا من مجموعة الأدوات.

هل يجتاز تطبيق Flutter تدقيقات PCI DSS والبنوك المركزية؟

يجتاز تطبيق Flutter التدقيق أو يرسب فيه بالمعايير نفسها التي تنطبق على التطبيق الأصلي. في PCI DSS، أبقِ بيانات البطاقة خارج كودك باستخدام SDK المزوّد أو الحقول المستضافة حتى يبقى نطاقك صغيرًا. وفي تقييمات البنوك المركزية والبنوك الشركاء، توقع أسئلة عن التخزين الآمن والمصادقة متعددة العوامل وكشف الروت وتثبيت الشهادات والتعتيم والسجلات؛ ولكلها إجابات معيارية في Flutter.

هل ينبغي إعادة كتابة تطبيقنا المصرفي الأصلي القائم بـ Flutter؟

عادةً ليس دفعة واحدة. إن كان التطبيق الأصلي مستقرًا والفرق قوية، فإعادة الكتابة مكلفة والفائدة في الأغلب سرعة مستقبلية. المسار الشائع هو add-to-app: أطلق رحلات جديدة أو علامة جديدة بـ Flutter داخل التطبيق القائم وتوسّع من هناك. وتكون إعادة الكتابة الكاملة منطقية حين يكون التطبيقان الأصليان قد تباعدا بشدة أو حين يمنحك منتج جديد سببًا للبدء من صفحة نظيفة.

ما مدى جودة تعامل Flutter مع العربية والتخطيطات من اليمين إلى اليسار؟

جيد جدًا، بشرط أن يستخدم الفريق خصائص التخطيط الاتجاهية ويختبر باللغتين من أول سبرنت. يتولى المحرك التشكيل والنص ثنائي الاتجاه؛ أما الأخطاء ففي كود التطبيق الذي يثبّت اليسار واليمين، وفي الأرقام التي تبدّل نمطها بين الشاشات، وفي النصوص مختلطة الاتجاه مثل IBAN داخل جمل عربية. وتلتقط اختبارات golden باللغتين معظم الانحدارات.

هل يستطيع Flutter التعامل مع شاشات التداول والرسوم البيانية الفورية؟

نعم. الرسم المخصص يناسب الرسوم البيانية وشرائط الأسعار، والشاشة المبنية جيدًا تتحدث مرات كثيرة في الثانية دون تقطّع، خصوصًا على Impeller. الانضباط في مسار البيانات: جمّع بيانات السوق في isolate خلفي، وادفع فقط التغييرات التي تحتاجها الشاشة، وأبقِ مناطق إعادة الرسم الثقيلة داخل widgets من نوع RepaintBoundary.

الخلاصة

  • في المنطقة التطبيق هو البنك، وقاعدة كود Flutter الواحدة تزيل أخطاء التطابق التي لا يحتملها السلوك الخاضع للرقابة.
  • يقلّل Flutter الكود الأصلي ولا يلغيه. يحتاج كل فريق إلى مهندس واحد يقرأ Kotlin وSwift بطلاقة.
  • الأصناف المختومة وجمل switch الشاملة في Dart 3 هي الأداة الصحيحة لحركة المال؛ ضع كل تحويل خلف آلة حالة تمنع الانتقالات غير المشروعة.
  • تعيش الرموز في Keychain وKeystore عبر غلاف قابل للتدقيق، لا في SharedPreferences أبدًا، ويمسح تسجيل الخروج القرص والذاكرة.
  • القياسات الحيوية وكشف الروت والتثبيت والتعتيم طبقات بحدود معروفة؛ أما التصديق والتحقق على الخادم فيحملان الثقل الحقيقي.
  • أبقِ بيانات البطاقات خارج التطبيق عبر SDK المزوّد والحقول المستضافة؛ وأبقِ البيانات الشخصية خارج السجلات بطبقة حجب.
  • Impeller والـ isolates والقوائم الافتراضية ومسار بدء خفيف تجعل Flutter سريعًا بما يكفي للخدمات المصرفية على Android منخفض المواصفات، إن اختبرت على Android منخفض المواصفات.
  • وظّف مهندسي Flutter الملمّين بالكود الأصلي، واختر أسلوبًا واحدًا لإدارة الحالة، وقرّر بمصفوفة موزونة بالفريق الذي تستطيع توظيفه فعلًا.