دروس Flutter: كيفية إنشاء تطبيق Flutter الأول
نشرت: 2022-03-11ما هو Flutter؟
Flutter هو SDK لتطوير تطبيقات الأجهزة المحمولة من Google والذي يسمح لمنتجك باستهداف كل من أنظمة Android و iOS في وقت واحد ، دون الحاجة إلى الاحتفاظ بقاعدة بيانات منفصلة. علاوة على ذلك ، يمكن أيضًا تجميع التطبيقات التي تستخدم Flutter لاستهداف نظام التشغيل Fuchsia القادم من Google.
حقق Flutter مؤخرًا إنجازًا كبيرًا - الإصدار 1.0 المستقر. تم الإصدار في لندن ، 5 ديسمبر 2018 ، في حدث Flutter Live. في حين أنه لا يزال من الممكن اعتباره مشروعًا برمجيًا مبكرًا ومتطورًا ، ستركز هذه المقالة على مفهوم مثبت بالفعل وتوضح كيفية تطوير تطبيق مراسلة يعمل بكامل طاقته ويستهدف كلاً من منصات الأجهزة المحمولة الرئيسية باستخدام Flutter 1.2 و Firebase.
كما يتضح من الرسم البياني أدناه ، فقد اكتسب Flutter الكثير من المستخدمين في الأشهر الأخيرة. في عام 2018 ، تضاعفت حصة Flutter في السوق وهي في طريقها لتجاوز React Native من حيث استعلامات البحث ، ومن هنا جاء قرارنا بإنشاء برنامج تعليمي جديد لـ Flutter.
ملاحظة: تركز هذه المقالة فقط على أجزاء معينة من التنفيذ. يمكن العثور على مرجع رمز المصدر الكامل للمشروع في GitHub repo.
المتطلبات الأساسية
على الرغم من بذل الجهود للسماح للقراء بمتابعة وإنجاز هذا المشروع حتى لو كانت محاولتهم الأولى في تطوير الأجهزة المحمولة ، فقد تم ذكر الكثير من مفاهيم تطوير الأجهزة المحمولة الأساسية التي ليست خاصة بـ Flutter واستخدامها دون شرح مفصل.
تم إجراء هذا لإيجاز المقال حيث أن أحد أهدافه هو أن يكمل القارئ المشروع في جلسة واحدة. أخيرًا ، تفترض المقالة أنك قد قمت بالفعل بإعداد بيئة التطوير الخاصة بك ، بما في ذلك المكونات الإضافية المطلوبة لـ Android Studio و Flutter SDK.
إعداد Firebase
إعداد Firebase هو الشيء الوحيد الذي يتعين علينا القيام به بشكل مستقل لكل نظام أساسي. بادئ ذي بدء ، تأكد من إنشاء مشروع جديد في Firebase Dashboard وإضافة تطبيقات Android و iOS في مساحة العمل التي تم إنشاؤها حديثًا. ستنتج المنصة ملفي تكوين تحتاج إلى تنزيلهما: google-services.json
لنظام Android و GoogleService-Info.plist
لنظام التشغيل iOS. قبل إغلاق لوحة التحكم ، تأكد من تمكين Firebase وموفري مصادقة Google لأننا سنستخدمهم لتعريف المستخدم. للقيام بذلك ، اختر عنصر المصادقة من القائمة ثم حدد علامة التبويب طريقة تسجيل الدخول.
يمكنك الآن إغلاق لوحة القيادة حيث أن باقي الإعداد يحدث في قاعدة التعليمات البرمجية الخاصة بنا. بادئ ذي بدء ، نحتاج إلى وضع الملفات التي نزّلناها في مشروعنا. يجب وضع ملف google-services.json
في المجلد $(FLUTTER_PROJECT_ROOT)/android/app
ويجب وضع GoogleService-Info.plist
في $(FLUTTER_PROJECT_ROOT)/ios/Runner
. بعد ذلك ، نحتاج إلى إعداد مكتبات Firebase التي سنستخدمها في المشروع وربطها بملفات التكوين. يتم ذلك عن طريق تحديد حزم Dart (المكتبات) التي سنستخدمها في ملف pubspec.yaml
الخاص بمشروعنا. في قسم التبعيات بالملف ، الصق المقتطف التالي:
flutter_bloc: shared_preferences: firebase_auth: cloud_firestore: google_sign_in: flutter_facebook_login:
لا يتعلق النوعان الأولين بـ Firebase ولكن سيتم استخدامهما بشكل متكرر في المشروع. الأخيرين ، كما نأمل ، لا يحتاجان إلى شرح.
أخيرًا ، نحتاج إلى تكوين إعدادات المشروع الخاصة بالنظام الأساسي والتي ستمكن تدفق المصادقة لدينا من الإكمال بنجاح. على جانب Android ، نحتاج إلى إضافة المكوّن الإضافي Gradle الخاص بخدمات google إلى تكوين Gradle على مستوى المشروع. بمعنى آخر ، نحتاج إلى إضافة العنصر التالي إلى قائمة التبعية في ملف $(FLUTTER_PROJECT_ROOT)/android/build.gradle
:
classpath 'com.google.gms:google-services:4.2.0' // change 4.2.0 to the latest version
ثم نحتاج إلى تطبيق هذا المكون الإضافي عن طريق إضافة هذا السطر إلى نهاية $(FLUTTER_PROJECT_ROOT)/android/app/build.gradle
:
apply plugin: 'com.google.gms.google-services'
آخر شيء لهذا النظام الأساسي هو إدراج معلمات تطبيق Facebook. ما نبحث عنه هنا هو تحرير هذين الملفين - $(FLUTTER_PROJECT_ROOT)/android/app/src/main/AndroidManifest.xml
و $(FLUTTER_PROJECT_ROOT)/android/app/src/main/res/values/strings.xml
:
<!-- AndroidManifest.xml --> <manifest xmlns:androcom.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/> <activity android:name="com.facebook.FacebookActivity" android:configChanges="keyboard|keyboardHidden|screenLayout|screenSize|orientation" android:label="@string/app_name" /> <activity android:name="com.facebook.CustomTabActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="@string/fb_login_protocol_scheme" /> </intent-filter> </activity> <!-- … --> </application> </manifest> <!-- strings.xml --> <resources> <string name="app_name">Toptal Chat</string> <string name="facebook_app_id">${YOUR_FACEBOOK_APP_ID}</string> <string name="fb_login_protocol_scheme">${YOUR_FACEBOOK_URL}</string> </resources>
حان الوقت الآن لنظام iOS. لحسن الحظ ، نحتاج فقط إلى تغيير ملف واحد في هذه الحالة. أضف القيم التالية (لاحظ أن عنصر CFBundleURLTypes
قد يكون موجودًا بالفعل في القائمة ؛ في هذه الحالة ، تحتاج إلى إضافة هذه العناصر إلى المصفوفة الحالية بدلاً من إعلانها مرة أخرى) إلى $(FLUTTER_PROJECT)ROOT/ios/Runner/Info.plist
ملف:
<key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleURLSchemes</key> <array> <string>${YOUR_FACEBOOK_URL}</string> </array> </dict> <dict> <key>CFBundleTypeRole</key> <string>Editor</string> <key>CFBundleURLSchemes</key> <array> <string>${YOUR_REVERSED_GOOGLE_WEB_CLIENT_ID}</string> </array> </dict> </array> <key>FacebookAppID</key> <string>${YOUR_FACEBOOK_APP_ID}</string> <key>FacebookDisplayName</key> <string>${YOUR_FACEBOOK_APP_NAME}</string> <key>LSApplicationQueriesSchemes</key> <array> <string>fbapi</string> <string>fb-messenger-share-api</string> <string>fbauth2</string> <string>fbshareextension</string> </array>
كلمة عن معمارية BLoC
تم وصف معيار البنية هذا في إحدى مقالاتنا السابقة ، مما يوضح استخدام BLoC لمشاركة الكود في Flutter و AngularDart ، لذلك لن نشرحها بالتفصيل هنا.
الفكرة الأساسية وراء الفكرة الرئيسية هي أن كل شاشة بها الفئات التالية: - المشاهدة - وهي المسؤولة عن عرض الحالة الحالية وتفويض مدخلات المستخدم كأحداث للكتلة. - الحالة - التي تمثل البيانات "الحية" التي يتفاعل معها المستخدم باستخدام طريقة العرض الحالية. - الكتلة - التي تستجيب للأحداث وتقوم بتحديث الحالة وفقًا لذلك ، وتطلب بشكل اختياري البيانات من واحد أو أكثر من المستودعات المحلية أو البعيدة. - الحدث - وهو نتيجة عمل محددة قد تغير أو لا تغير الحالة الحالية.
كتمثيل رسومي ، يمكن التفكير فيه على النحو التالي:
بالإضافة إلى ذلك ، لدينا دليل نموذج يحتوي على فئات البيانات والمستودعات التي تنتج مثيلات من هذه الفئات.
تطوير واجهة المستخدم
يتم إنشاء واجهة مستخدم باستخدام Flutter بالكامل في Dart ، على عكس تطوير التطبيق الأصلي في Android و iOS حيث يتم إنشاء واجهة المستخدم باستخدام مخطط XML ويتم فصلها تمامًا عن قاعدة بيانات منطق الأعمال. سنستخدم تركيبات عنصر واجهة مستخدم بسيطة نسبيًا مع مكونات مختلفة بناءً على الحالة الحالية (على سبيل المثال isLoading ، isEmpty parameters). تدور واجهة المستخدم في Flutter حول الأدوات ، أو بالأحرى شجرة الأدوات. يمكن أن تكون الأدوات المصغّرة إما بدون حالة أو ذات حالة. عندما يتعلق الأمر بالحالات ذات الحالة ، من المهم التأكيد على أنه عندما يتم استدعاء setState()
على عنصر واجهة مستخدم معين يتم عرضه حاليًا (استدعاءه في المُنشئ أو بعد التخلص منه يؤدي إلى خطأ في وقت التشغيل) ، فإن ممر البناء والرسم يكون من المقرر إجراؤها في دورة الرسم التالية.
للإيجاز ، سنعرض هنا فئة واحدة فقط من فئات واجهة المستخدم (عرض):
class LoginScreen extends StatefulWidget { LoginScreen({Key key}) : super(key: key); @override State<StatefulWidget> createState() => _LoginState(); } class _LoginState extends State<LoginScreen> { final _bloc = LoginBloc(); @override Widget build(BuildContext context) { return BlocProvider<LoginBloc>( bloc: _bloc, child: LoginWidget(widget: widget, widgetState: this) ); } @override void dispose() { _bloc.dispose(); super.dispose(); } } class LoginWidget extends StatelessWidget { const LoginWidget({Key key, @required this.widget, @required this.widgetState}) : super(key: key); final LoginScreen widget; final _LoginState widgetState; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Login"), ), body: BlocBuilder( bloc: BlocProvider.of<LoginBloc>(context), builder: (context, LoginState state) { if (state.loading) { return Center( child: CircularProgressIndicator(strokeWidth: 4.0) ); } else { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ ButtonTheme( minWidth: 256.0, height: 32.0, child: RaisedButton( onPressed: () => BlocProvider.of<LoginBloc>(context).onLoginGoogle(this), child: Text( "Login with Google", style: TextStyle(color: Colors.white), ), color: Colors.redAccent, ), ), ButtonTheme( minWidth: 256.0, height: 32.0, child: RaisedButton( onPressed: () => BlocProvider.of<LoginBloc>(context).onLoginFacebook(this), child: Text( "Login with Facebook", style: TextStyle(color: Colors.white), ), color: Colors.blueAccent, ), ), ], ), ); } }), ); } void navigateToMain() { NavigationHelper.navigateToMain(widgetState.context); } }
تتبع بقية فئات واجهة المستخدم نفس الأنماط ولكن ربما يكون لها إجراءات مختلفة وقد تحتوي على شجرة عناصر واجهة مستخدم حالة فارغة بالإضافة إلى حالة التحميل.

المصادقة
كما قد تكون خمنت ، سنستخدم مكتبات google_sign_in
و flutter_facebook_login
لمصادقة المستخدم من خلال الاعتماد على ملف تعريف الشبكة الاجتماعية الخاص به. بادئ ذي بدء ، تأكد من استيراد هذه الحزم إلى الملف الذي سيتعامل مع منطق طلب تسجيل الدخول:
import 'package:flutter_facebook_login/flutter_facebook_login.dart'; import 'package:google_sign_in/google_sign_in.dart';
الآن ، سيكون لدينا جزأين مستقلين سيهتمان بتدفق المصادقة لدينا. سيبدأ الأول في طلب تسجيل الدخول إلى Facebook أو Google:
void onLoginGoogle(LoginWidget view) async { dispatch(LoginEventInProgress()); final googleSignInRepo = GoogleSignIn(signInOption: SignInOption.standard, scopes: ["profile", "email"]); final account = await googleSignInRepo.signIn(); if (account != null) { LoginRepo.getInstance().signInWithGoogle(account); } else { dispatch(LogoutEvent()); } } void onLoginFacebook(LoginWidget view) async { dispatch(LoginEventInProgress()); final facebookSignInRepo = FacebookLogin(); final signInResult = await facebookSignInRepo.logInWithReadPermissions(["email"]); if (signInResult.status == FacebookLoginStatus.loggedIn) { LoginRepo.getInstance().signInWithFacebook(signInResult); } else if (signInResult.status == FacebookLoginStatus.cancelledByUser) { dispatch(LogoutEvent()); } else { dispatch(LoginErrorEvent(signInResult.errorMessage)); } }
سيتم استدعاء الثاني عندما نحصل على بيانات الملف الشخصي من أي مزود. سنحقق ذلك من خلال توجيه معالج تسجيل الدخول لدينا للاستماع إلى firebase_auth onAuthStateChange
stream:
void _setupAuthStateListener(LoginWidget view) { if (_authStateListener == null) { _authStateListener = FirebaseAuth.instance.onAuthStateChanged.listen((user) { if (user != null) { final loginProvider = user.providerId; UserRepo.getInstance().setCurrentUser(User.fromFirebaseUser(user)); if (loginProvider == "google") { // TODO analytics call for google login provider } else { // TODO analytics call for facebook login provider } view.navigateToMain(); } else { dispatch(LogoutEvent()); } }, onError: (error) { dispatch(LoginErrorEvent(error)); }); } }
لن يتم نشر تطبيق UserRepo
و LoginRepo
هنا ، ولكن لا تتردد في إلقاء نظرة على GitHub repo للرجوع إليها بالكامل.
دروس Flutter: كيفية إنشاء تطبيق مراسلة فورية
أخيرًا ، وصلنا إلى الجزء المثير للاهتمام. كما يوحي الاسم ، يجب تبادل الرسائل بأسرع ما يمكن ، ومن الناحية المثالية ، يجب أن يكون هذا فوريًا . لحسن الحظ ، يسمح لنا cloud_firestore
بالتفاعل مع مثيل Firestore ويمكننا استخدام ميزة snapshots()
الخاصة به لفتح دفق بيانات يمنحنا تحديثات في الوقت الفعلي. في رأيي ، كل كود chat_repo
واضح ومباشر باستثناء طريقة startChatroomForUsers
. إنها مسؤولة عن إنشاء غرفة دردشة جديدة لمستخدمين ما لم تكن هناك غرفة حالية تحتوي على كلا المستخدمين (لأننا لا نريد أن يكون لدينا مثيلات متعددة من نفس زوج المستخدم) وفي هذه الحالة تقوم بإرجاع غرفة الدردشة الحالية.
ومع ذلك ، نظرًا لتصميم Firestore ، فهو لا يدعم حاليًا الاستعلامات المتداخلة array-contains
. لذلك لا يمكننا استرداد دفق البيانات المناسب ولكننا بحاجة إلى إجراء تصفية إضافية من جانبنا. يتكون هذا الحل من استرداد جميع غرف الدردشة للمستخدم الذي قام بتسجيل الدخول ثم البحث عن الغرفة التي تحتوي أيضًا على المستخدم المحدد:
Future<SelectedChatroom> startChatroomForUsers(List<User> users) async { DocumentReference userRef = _firestore .collection(FirestorePaths.USERS_COLLECTION) .document(users[1].uid); QuerySnapshot queryResults = await _firestore .collection(FirestorePaths.CHATROOMS_COLLECTION) .where("participants", arrayContains: userRef) .getDocuments(); DocumentReference otherUserRef = _firestore .collection(FirestorePaths.USERS_COLLECTION) .document(users[0].uid); DocumentSnapshot roomSnapshot = queryResults.documents.firstWhere((room) { return room.data["participants"].contains(otherUserRef); }, orElse: () => null); if (roomSnapshot != null) { return SelectedChatroom(roomSnapshot.documentID, users[0].displayName); } else { Map<String, dynamic> chatroomMap = Map<String, dynamic>(); chatroomMap["messages"] = List<String>(0); List<DocumentReference> participants = List<DocumentReference>(2); participants[0] = otherUserRef; participants[1] = userRef; chatroomMap["participants"] = participants; DocumentReference reference = await _firestore .collection(FirestorePaths.CHATROOMS_COLLECTION) .add(chatroomMap); DocumentSnapshot chatroomSnapshot = await reference.get(); return SelectedChatroom(chatroomSnapshot.documentID, users[0].displayName); } }
أيضًا ، نظرًا لقيود التصميم المماثلة ، لا يدعم Firebase حاليًا تحديثات المصفوفة (إدراج عنصر جديد في قيمة حقل مصفوفة موجودة) مع قيمة FieldValue.serverTimestamp()
الخاصة.
تشير هذه القيمة إلى النظام الأساسي إلى أنه يجب ملء الحقل الذي يحتوي على هذا بدلاً من القيمة الفعلية بالطابع الزمني الفعلي على الخادم في اللحظة التي تتم فيها المعاملة. بدلاً من ذلك ، نحن نستخدم DateTime.now()
في الوقت الحالي ، حيث نقوم بإنشاء كائن تسلسلي جديد للرسالة وإدخال هذا الكائن في مجموعة رسائل غرفة الدردشة.
Future<bool> sendMessageToChatroom(String chatroomId, User user, String message) async { try { DocumentReference authorRef = _firestore.collection(FirestorePaths.USERS_COLLECTION).document(user.uid); DocumentReference chatroomRef = _firestore.collection(FirestorePaths.CHATROOMS_COLLECTION).document(chatroomId); Map<String, dynamic> serializedMessage = { "author" : authorRef, "timestamp" : DateTime.now(), "value" : message }; chatroomRef.updateData({ "messages" : FieldValue.arrayUnion([serializedMessage]) }); return true; } catch (e) { print(e.toString()); return false; } }
تغليف
من الواضح أن تطبيق المراسلة Flutter الذي قمنا بتطويره هو إثبات للمفهوم أكثر من كونه تطبيق مراسلة فورية جاهز للسوق. كأفكار لمزيد من التطوير ، قد يفكر المرء في تقديم تشفير شامل أو محتوى غني (محادثات جماعية ، ومرفقات وسائط ، وتحليل عناوين URL). ولكن قبل كل ذلك ، يجب على المرء تنفيذ إشعارات الدفع لأنها إلى حد كبير ميزة لا غنى عنها لتطبيق المراسلة الفورية ، وقد نقلناها خارج نطاق هذه المقالة من أجل الإيجاز. بالإضافة إلى ذلك ، لا يزال Firestore يفتقد بعض الميزات من أجل الحصول على استعلامات أبسط وأكثر دقة مثل البيانات المتداخلة array-contains
.
كما ذكرنا في بداية المقال ، لم ينضج Flutter إلا مؤخرًا ليصبح إصدارًا مستقرًا 1.0 وسيستمر في النمو ، ليس فقط عندما يتعلق الأمر بميزات وإمكانيات إطار العمل ولكن أيضًا عندما يتعلق الأمر بمجتمع التطوير ومكتبات الجهات الخارجية و مصادر. من المنطقي أن تستثمر وقتك في التعرف على تطوير تطبيق Flutter الآن ، لأنه من الواضح أنه موجود لتبقى وتسريع عملية تطوير هاتفك المحمول.
بدون أي نفقات إضافية ، سيكون مطورو Flutter مستعدين أيضًا لاستهداف نظام التشغيل - الفوشيا الناشئ من Google.