Cloud Firestore Android Codelab

1. بررسی اجمالی

اهداف

در این کد لبه شما یک برنامه توصیه رستوران در اندروید با پشتیبانی Cloud Firestore می‌سازید. شما یاد خواهید گرفت که چگونه:

  • خواندن و نوشتن داده ها در Firestore از یک برنامه Android
  • به تغییرات داده های Firestore در زمان واقعی گوش دهید
  • از Firebase Authentication و قوانین امنیتی برای ایمن سازی داده های Firestore استفاده کنید
  • پرس و جوهای پیچیده Firestore را بنویسید

پیش نیازها

قبل از شروع این کد لبه مطمئن شوید که:

  • Android Studio Flamingo یا جدیدتر
  • شبیه ساز اندروید با API 19 یا بالاتر
  • Node.js نسخه 16 یا بالاتر
  • جاوا نسخه 17 یا بالاتر

2. یک پروژه Firebase ایجاد کنید

  1. با حساب Google خود وارد کنسول Firebase شوید.
  2. در کنسول Firebase ، روی افزودن پروژه کلیک کنید.
  3. همانطور که در تصویر زیر نشان داده شده است، یک نام برای پروژه Firebase خود وارد کنید (به عنوان مثال، "Friendly Eats") و روی Continue کلیک کنید.

9d2f625aebcab6af.png

  1. ممکن است از شما خواسته شود که Google Analytics را فعال کنید، برای اهداف این کد کد، انتخاب شما مهم نیست.
  2. پس از یک دقیقه یا بیشتر، پروژه Firebase شما آماده خواهد شد. روی Continue کلیک کنید.

3. پروژه نمونه را راه اندازی کنید

کد را دانلود کنید

دستور زیر را برای کلون کردن کد نمونه برای این Codelab اجرا کنید. با این کار یک پوشه به نام friendlyeats-android در دستگاه شما ایجاد می شود:

$ git clone https://github.com/firebase/friendlyeats-android

اگر git روی دستگاه خود ندارید، می توانید کد را مستقیماً از GitHub نیز دانلود کنید.

پیکربندی Firebase را اضافه کنید

  1. در کنسول Firebase ، نمای کلی پروژه را در ناوبری سمت چپ انتخاب کنید. برای انتخاب پلتفرم روی دکمه اندروید کلیک کنید. وقتی نام بسته از شما خواسته شد از com.google.firebase.example.fireeats استفاده کنید

73d151ed16016421.png

  1. روی ثبت برنامه کلیک کنید و دستورالعمل ها را برای دانلود فایل google-services.json دنبال کنید و آن را به پوشه app/ کدی که دانلود کرده اید منتقل کنید. سپس روی Next کلیک کنید.

پروژه را وارد کنید

اندروید استودیو را باز کنید. روی File > New > Import Project کلیک کنید و پوشه friendlyeats-android را انتخاب کنید.

4. شبیه سازهای Firebase را راه اندازی کنید

در این کد لبه از مجموعه شبیه ساز Firebase برای شبیه سازی محلی Cloud Firestore و سایر سرویس های Firebase استفاده خواهید کرد. این یک محیط توسعه محلی امن، سریع و بدون هزینه برای ساخت برنامه شما فراهم می کند.

Firebase CLI را نصب کنید

ابتدا باید Firebase CLI را نصب کنید. اگر از macOS یا Linux استفاده می کنید، می توانید دستور cURL زیر را اجرا کنید:

curl -sL https://firebase.tools | bash

اگر از ویندوز استفاده می کنید، دستورالعمل های نصب را بخوانید تا یک باینری مستقل دریافت کنید یا از طریق npm نصب کنید.

پس از نصب CLI، اجرای firebase --version باید نسخه 9.0.0 یا بالاتر را گزارش کند:

$ firebase --version
9.0.0

وارد شوید

برای اتصال CLI به حساب Google خود firebase login اجرا کنید. با این کار یک پنجره مرورگر جدید برای تکمیل فرآیند ورود باز می شود. اطمینان حاصل کنید که همان حسابی را انتخاب کنید که قبلاً هنگام ایجاد پروژه Firebase خود استفاده می کردید.

از داخل پوشه friendlyeats-android ، firebase use --add . دستورات را دنبال کنید تا پروژه ای را که قبلا ایجاد کرده اید انتخاب کنید و اگر از شما خواسته شد نام مستعار را انتخاب کنید default وارد کنید.

5. برنامه را اجرا کنید

اکنون زمان اجرای Firebase Emulator Suite و اپلیکیشن اندروید FriendlyEats برای اولین بار است.

شبیه سازها را اجرا کنید

در ترمینال خود از داخل دایرکتوری friendlyeats-android firebase emulators:start به راه اندازی شبیه سازهای Firebase کنید. شما باید گزارش هایی مانند این را ببینید:

$ firebase emulators:start
i  emulators: Starting emulators: auth, firestore
i  firestore: Firestore Emulator logging to firestore-debug.log
i  ui: Emulator UI logging to ui-debug.log

┌─────────────────────────────────────────────────────────────┐
│ ✔  All emulators ready! It is now safe to connect your app. │
│ i  View Emulator UI at http://localhost:4000                │
└─────────────────────────────────────────────────────────────┘

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ localhost:9099 │ http://localhost:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ localhost:8080 │ http://localhost:4000/firestore │
└────────────────┴────────────────┴─────────────────────────────────┘
  Emulator Hub running at localhost:4400
  Other reserved ports: 4500

Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.

شما اکنون یک محیط توسعه محلی کامل در حال اجرا بر روی دستگاه خود دارید! مطمئن شوید که این دستور را برای بقیه قسمت‌های Codelab اجرا می‌کنید، برنامه اندروید شما باید به شبیه‌سازها متصل شود.

برنامه را به شبیه سازها وصل کنید

فایل‌های util/FirestoreInitializer.kt و util/AuthInitializer.kt را در Android Studio باز کنید. این فایل ها حاوی منطق اتصال Firebase SDK به شبیه سازهای محلی در حال اجرا بر روی دستگاه شما، پس از راه اندازی برنامه هستند.

در متد create() کلاس FirestoreInitializer ، این قطعه کد را بررسی کنید:

    // Use emulators only in debug builds
    if (BuildConfig.DEBUG) {
        firestore.useEmulator(FIRESTORE_EMULATOR_HOST, FIRESTORE_EMULATOR_PORT)
    }

ما از BuildConfig استفاده می کنیم تا مطمئن شویم فقط زمانی که برنامه ما در حالت debug اجرا می شود به شبیه سازها متصل می شویم. وقتی برنامه را در حالت release کامپایل می کنیم، این شرط نادرست خواهد بود.

می بینیم که از روش useEmulator(host, port) برای اتصال Firebase SDK به شبیه ساز محلی Firestore استفاده می کند. در سرتاسر برنامه از FirebaseUtil.getFirestore() برای دسترسی به این نمونه از FirebaseFirestore استفاده می‌کنیم، بنابراین مطمئن هستیم که همیشه هنگام اجرای در حالت debug به شبیه‌ساز Firestore متصل می‌شویم.

برنامه را اجرا کنید

اگر فایل google-services.json را به درستی اضافه کرده اید، اکنون پروژه باید کامپایل شود. در Android Studio روی Build > Rebuild Project کلیک کنید و مطمئن شوید که هیچ خطایی باقی نمانده است.

در اندروید استودیو برنامه را روی شبیه ساز اندروید خود اجرا کنید . در ابتدا با یک صفحه "ورود به سیستم" نمایش داده می شود. برای ورود به برنامه می توانید از هر ایمیل و رمز عبوری استفاده کنید. این فرآیند ورود به سیستم در حال اتصال به شبیه‌ساز احراز هویت Firebase است، بنابراین هیچ اعتبار واقعی منتقل نمی‌شود.

اکنون رابط کاربری Emulators را با رفتن به http://localhost:4000 در مرورگر وب خود باز کنید. سپس بر روی تب Authentication کلیک کنید و باید حسابی را که ایجاد کرده اید مشاهده کنید:

شبیه ساز Firebase Auth

پس از تکمیل فرآیند ورود، باید صفحه اصلی برنامه را ببینید:

de06424023ffb4b9.png

به زودی مقداری داده برای پر کردن صفحه اصلی اضافه خواهیم کرد.

6. داده ها را در Firestore بنویسید

در این بخش داده هایی را در Firestore می نویسیم تا بتوانیم صفحه اصلی خالی فعلی را پر کنیم.

شی مدل اصلی در برنامه ما یک رستوران است (به model/Restaurant.kt مراجعه کنید). داده های Firestore به اسناد، مجموعه ها و زیر مجموعه ها تقسیم می شوند. ما هر رستوران را به عنوان یک سند در یک مجموعه سطح بالا به نام "restaurants" ذخیره خواهیم کرد. برای کسب اطلاعات بیشتر در مورد مدل داده Firestore، اسناد و مجموعه‌ها را در اسناد مطالعه کنید.

برای اهداف نمایشی، وقتی روی دکمه "افزودن موارد تصادفی" در منوی سرریز کلیک می کنیم، عملکردی را به برنامه اضافه می کنیم تا ده رستوران تصادفی ایجاد کنیم. فایل MainFragment.kt را باز کنید و محتوای متد onAddItemsClicked() را با:

    private fun onAddItemsClicked() {
        val restaurantsRef = firestore.collection("restaurants")
        for (i in 0..9) {
            // Create random restaurant / ratings
            val randomRestaurant = RestaurantUtil.getRandom(requireContext())

            // Add restaurant
            restaurantsRef.add(randomRestaurant)
        }
    }

چند نکته مهم در مورد کد بالا وجود دارد:

  • ما با ارجاع به مجموعه "restaurants" شروع کردیم. مجموعه ها به طور ضمنی هنگام اضافه شدن اسناد ایجاد می شوند، بنابراین نیازی به ایجاد مجموعه قبل از نوشتن داده ها وجود نداشت.
  • اسناد را می توان با استفاده از کلاس های داده Kotlin ایجاد کرد، که ما از آنها برای ایجاد هر Doc رستوران استفاده می کنیم.
  • متد add() سندی را به مجموعه ای با شناسه تولید شده خودکار اضافه می کند، بنابراین نیازی به تعیین شناسه منحصر به فرد برای هر رستوران نداریم.

اکنون برنامه را دوباره اجرا کنید و روی دکمه "افزودن موارد تصادفی" در منوی سرریز (در گوشه بالا سمت راست) کلیک کنید تا کدی را که نوشتید فراخوانی کنید:

95691e9b71ba55e3.png

اکنون رابط کاربری Emulators را با رفتن به http://localhost:4000 در مرورگر وب خود باز کنید. سپس بر روی تب Firestore کلیک کنید و باید داده هایی را که به تازگی اضافه کرده اید مشاهده کنید:

شبیه ساز Firebase Auth

این داده ها 100% محلی برای دستگاه شما هستند. در واقع، پروژه واقعی شما هنوز حاوی پایگاه داده Firestore نیست! این بدان معناست که آزمایش با تغییر و حذف این داده‌ها بدون عواقب بی‌خطر است.

تبریک می‌گوییم، شما فقط داده‌ها را به Firestore نوشتید! در مرحله بعدی نحوه نمایش این داده ها را در برنامه یاد خواهیم گرفت.

7. نمایش داده ها از Firestore

در این مرحله یاد می گیریم که چگونه داده ها را از Firestore بازیابی کرده و در برنامه خود نمایش دهیم. اولین قدم برای خواندن داده ها از Firestore ایجاد یک Query است. فایل MainFragment.kt را باز کنید و کد زیر را به ابتدای متد onViewCreated() اضافه کنید:

        // Firestore
        firestore = Firebase.firestore

        // Get the 50 highest rated restaurants
        query = firestore.collection("restaurants")
            .orderBy("avgRating", Query.Direction.DESCENDING)
            .limit(LIMIT.toLong())

اکنون می‌خواهیم به پرس و جو گوش دهیم تا همه اسناد منطبق را دریافت کنیم و از به‌روزرسانی‌های آینده در زمان واقعی مطلع شویم. از آنجایی که هدف نهایی ما اتصال این داده ها به یک RecyclerView است، باید یک کلاس RecyclerView.Adapter ایجاد کنیم تا به داده ها گوش دهیم.

کلاس FirestoreAdapter را که قبلاً تا حدی پیاده سازی شده است باز کنید. ابتدا، اجازه دهید آداپتور را اجرا کنیم که EventListener اجرا کرده و تابع onEvent را تعریف کنیم تا بتواند به‌روزرسانی‌های یک پرسش Firestore را دریافت کند:

abstract class FirestoreAdapter<VH : RecyclerView.ViewHolder>(private var query: Query?) :
        RecyclerView.Adapter<VH>(),
        EventListener<QuerySnapshot> { // Add this implements
    
    // ...

    // Add this method
    override fun onEvent(documentSnapshots: QuerySnapshot?, e: FirebaseFirestoreException?) {
        
        // Handle errors
        if (e != null) {
            Log.w(TAG, "onEvent:error", e)
            return
        }

        // Dispatch the event
        if (documentSnapshots != null) {
            for (change in documentSnapshots.documentChanges) {
                // snapshot of the changed document
                when (change.type) {
                    DocumentChange.Type.ADDED -> {
                        // TODO: handle document added
                    }
                    DocumentChange.Type.MODIFIED -> {
                        // TODO: handle document changed
                    }
                    DocumentChange.Type.REMOVED -> {
                        // TODO: handle document removed
                    }
                }
            }
        }

        onDataChanged()
    }
    
    // ...
}

در بارگذاری اولیه شنونده یک رویداد ADDED برای هر سند جدید دریافت می کند. از آنجایی که مجموعه نتیجه پرس و جو در طول زمان تغییر می کند، شنونده رویدادهای بیشتری حاوی تغییرات را دریافت خواهد کرد. حالا بیایید اجرای شنونده را به پایان برسانیم. ابتدا سه روش جدید اضافه کنید: onDocumentAdded ، onDocumentModified و onDocumentRemoved :

    private fun onDocumentAdded(change: DocumentChange) {
        snapshots.add(change.newIndex, change.document)
        notifyItemInserted(change.newIndex)
    }

    private fun onDocumentModified(change: DocumentChange) {
        if (change.oldIndex == change.newIndex) {
            // Item changed but remained in same position
            snapshots[change.oldIndex] = change.document
            notifyItemChanged(change.oldIndex)
        } else {
            // Item changed and changed position
            snapshots.removeAt(change.oldIndex)
            snapshots.add(change.newIndex, change.document)
            notifyItemMoved(change.oldIndex, change.newIndex)
        }
    }

    private fun onDocumentRemoved(change: DocumentChange) {
        snapshots.removeAt(change.oldIndex)
        notifyItemRemoved(change.oldIndex)
    }

سپس این متدهای جدید را از onEvent فراخوانی کنید:

    override fun onEvent(documentSnapshots: QuerySnapshot?, e: FirebaseFirestoreException?) {

        // Handle errors
        if (e != null) {
            Log.w(TAG, "onEvent:error", e)
            return
        }

        // Dispatch the event
        if (documentSnapshots != null) {
            for (change in documentSnapshots.documentChanges) {
                // snapshot of the changed document
                when (change.type) {
                    DocumentChange.Type.ADDED -> {
                        onDocumentAdded(change) // Add this line
                    }
                    DocumentChange.Type.MODIFIED -> {
                        onDocumentModified(change) // Add this line
                    }
                    DocumentChange.Type.REMOVED -> {
                        onDocumentRemoved(change) // Add this line
                    }
                }
            }
        }

        onDataChanged()
    }

در نهایت متد startListening() را برای پیوست کردن شنونده پیاده سازی کنید:

    fun startListening() {
        if (registration == null) {
            registration = query.addSnapshotListener(this)
        }
    }

اکنون برنامه به طور کامل برای خواندن داده ها از Firestore پیکربندی شده است. دوباره برنامه را اجرا کنید و باید رستوران هایی را که در مرحله قبل اضافه کرده اید ببینید:

9e45f40faefce5d0.png

اکنون به رابط کاربری Emulator در مرورگر خود برگردید و یکی از نام‌های رستوران را ویرایش کنید. شما باید تغییر آن را در برنامه تقریباً بلافاصله مشاهده کنید!

8. مرتب سازی و فیلتر کردن داده ها

این برنامه در حال حاضر بهترین رستوران ها را در کل مجموعه نمایش می دهد، اما در یک برنامه رستوران واقعی کاربر می خواهد داده ها را مرتب و فیلتر کند. برای مثال، برنامه باید بتواند «رستوران‌های برتر غذاهای دریایی در فیلادلفیا» یا «ارزان‌ترین پیتزا» را نشان دهد.

با کلیک بر روی نوار سفید در بالای برنامه، یک گفتگوی فیلترها ظاهر می شود. در این بخش از پرس و جوهای Firestore برای کارکرد این گفتگو استفاده خواهیم کرد:

67898572a35672a5.png

بیایید متد onFilter() MainFragment.kt را ویرایش کنیم. این روش یک شی Filters را می پذیرد که یک شی کمکی است که ما برای گرفتن خروجی گفتگوی فیلترها ایجاد کرده ایم. ما این روش را برای ساخت یک پرس و جو از فیلترها تغییر می دهیم:

    override fun onFilter(filters: Filters) {
        // Construct query basic query
        var query: Query = firestore.collection("restaurants")

        // Category (equality filter)
        if (filters.hasCategory()) {
            query = query.whereEqualTo(Restaurant.FIELD_CATEGORY, filters.category)
        }

        // City (equality filter)
        if (filters.hasCity()) {
            query = query.whereEqualTo(Restaurant.FIELD_CITY, filters.city)
        }

        // Price (equality filter)
        if (filters.hasPrice()) {
            query = query.whereEqualTo(Restaurant.FIELD_PRICE, filters.price)
        }

        // Sort by (orderBy with direction)
        if (filters.hasSortBy()) {
            query = query.orderBy(filters.sortBy.toString(), filters.sortDirection)
        }

        // Limit items
        query = query.limit(LIMIT.toLong())

        // Update the query
        adapter.setQuery(query)

        // Set header
        binding.textCurrentSearch.text = HtmlCompat.fromHtml(
            filters.getSearchDescription(requireContext()),
            HtmlCompat.FROM_HTML_MODE_LEGACY
        )
        binding.textCurrentSortBy.text = filters.getOrderDescription(requireContext())

        // Save filters
        viewModel.filters = filters
    }

در قطعه بالا، یک شی Query را با پیوست کردن عبارت‌های where و orderBy برای مطابقت با فیلترهای داده شده می‌سازیم.

برنامه را دوباره اجرا کنید و فیلتر زیر را برای نمایش محبوب ترین رستوران های ارزان قیمت انتخاب کنید:

7a67a8a400c80c50.png

اکنون باید یک لیست فیلتر شده از رستوران ها را ببینید که فقط شامل گزینه های ارزان قیمت است:

a670188398c3c59.png

اگر تا اینجا پیش رفته اید، اکنون یک برنامه مشاهده توصیه رستوران کاملاً کارآمد در Firestore ساخته اید! اکنون می توانید رستوران ها را در زمان واقعی مرتب و فیلتر کنید. در چند بخش بعدی نظراتی را به رستوران ها اضافه می کنیم و قوانین امنیتی را به برنامه اضافه می کنیم.

9. داده ها را در زیر مجموعه ها سازماندهی کنید

در این بخش رتبه‌بندی‌هایی را به برنامه اضافه می‌کنیم تا کاربران بتوانند رستوران‌های مورد علاقه (یا حداقل مورد علاقه) خود را بررسی کنند.

مجموعه ها و زیر مجموعه ها

تا کنون ما تمام داده های رستوران را در یک مجموعه سطح بالا به نام "رستوران" ذخیره کرده ایم. وقتی کاربر به یک رستوران امتیاز می دهد، می خواهیم یک شی Rating جدید به رستوران ها اضافه کنیم. برای این کار از یک زیر مجموعه استفاده می کنیم. شما می توانید یک زیر مجموعه را به عنوان مجموعه ای در نظر بگیرید که به یک سند پیوست شده است. بنابراین هر سند رستوران یک زیرمجموعه رتبه بندی پر از اسناد رتبه بندی خواهد داشت. مجموعه‌های فرعی به سازماندهی داده‌ها کمک می‌کنند بدون اینکه اسناد ما را متورم کند یا نیاز به جستجوهای پیچیده داشته باشد.

برای دسترسی به یک زیر مجموعه، .collection() را در سند والد فراخوانی کنید:

val subRef = firestore.collection("restaurants")
        .document("abc123")
        .collection("ratings")

شما می توانید مانند مجموعه های سطح بالا به یک زیرمجموعه دسترسی داشته باشید و آن را پرس و جو کنید، هیچ محدودیتی در اندازه یا تغییر عملکرد وجود ندارد. در اینجا می توانید اطلاعات بیشتری در مورد مدل داده Firestore بخوانید.

نوشتن داده در تراکنش

برای افزودن Rating به زیرمجموعه مناسب، فقط نیاز به فراخوانی .add() است، اما همچنین باید میانگین رتبه‌بندی شی Restaurant و تعداد رتبه‌بندی‌ها را به‌روزرسانی کنیم تا داده‌های جدید را منعکس کنیم. اگر از عملیات جداگانه برای ایجاد این دو تغییر استفاده کنیم، تعدادی شرایط مسابقه وجود دارد که می تواند منجر به داده های قدیمی یا نادرست شود.

برای اطمینان از اینکه رتبه‌بندی‌ها به درستی اضافه می‌شوند، از تراکنش برای افزودن رتبه‌بندی به یک رستوران استفاده می‌کنیم. این تراکنش چند عمل انجام می دهد:

  • رتبه فعلی رستوران را بخوانید و امتیاز جدید را محاسبه کنید
  • امتیاز را به زیر مجموعه اضافه کنید
  • میانگین رتبه‌بندی رستوران و تعداد رتبه‌بندی‌ها را به‌روزرسانی کنید

RestaurantDetailFragment.kt را باز کنید و تابع addRating را پیاده سازی کنید:

    private fun addRating(restaurantRef: DocumentReference, rating: Rating): Task<Void> {
        // Create reference for new rating, for use inside the transaction
        val ratingRef = restaurantRef.collection("ratings").document()

        // In a transaction, add the new rating and update the aggregate totals
        return firestore.runTransaction { transaction ->
            val restaurant = transaction.get(restaurantRef).toObject<Restaurant>()
                ?: throw Exception("Restaurant not found at ${restaurantRef.path}")

            // Compute new number of ratings
            val newNumRatings = restaurant.numRatings + 1

            // Compute new average rating
            val oldRatingTotal = restaurant.avgRating * restaurant.numRatings
            val newAvgRating = (oldRatingTotal + rating.rating) / newNumRatings

            // Set new restaurant info
            restaurant.numRatings = newNumRatings
            restaurant.avgRating = newAvgRating

            // Commit to Firestore
            transaction.set(restaurantRef, restaurant)
            transaction.set(ratingRef, rating)

            null
        }
    }

تابع addRating() یک Task را برمی گرداند که کل تراکنش را نشان می دهد. در تابع onRating() شنوندگان به وظیفه اضافه می شوند تا به نتیجه تراکنش پاسخ دهند.

حالا دوباره برنامه را اجرا کنید و روی یکی از رستوران ها کلیک کنید، که باید صفحه جزئیات رستوران ظاهر شود. برای شروع اضافه کردن نظر، روی دکمه + کلیک کنید. با انتخاب تعدادی ستاره و وارد کردن متن، نظری را اضافه کنید.

78fa16cdf8ef435a.png

با زدن گزینه Submit تراکنش آغاز می شود. وقتی تراکنش کامل شد، نظر خود را در زیر نمایش داده می‌شود و به‌روزرسانی تعداد بازبینی رستوران را مشاهده می‌کنید:

f9e670f40bd615b0.png

تبریک میگم اکنون یک برنامه بررسی رستوران اجتماعی، محلی و موبایل دارید که بر روی Cloud Firestore ساخته شده است. شنیده ام که این روزها بسیار محبوب هستند.

10. داده های خود را ایمن کنید

تا کنون امنیت این اپلیکیشن را در نظر نگرفته ایم. چگونه بفهمیم که کاربران فقط می توانند داده های صحیح خود را بخوانند و بنویسند؟ پایگاه داده های Firestore توسط یک فایل پیکربندی به نام قوانین امنیتی محافظت می شوند.

فایل firestore.rules را باز کنید، باید موارد زیر را مشاهده کنید:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      //
      // WARNING: These rules are insecure! We will replace them with
      // more secure rules later in the codelab
      //
      allow read, write: if request.auth != null;
    }
  }
}

بیایید این قوانین را تغییر دهیم تا از دسترسی یا تغییرات ناخواسته به داده ها جلوگیری کنیم، فایل firestore.rules را باز کرده و محتوا را با موارد زیر جایگزین کنیم:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Determine if the value of the field "key" is the same
    // before and after the request.
    function isUnchanged(key) {
      return (key in resource.data)
        && (key in request.resource.data)
        && (resource.data[key] == request.resource.data[key]);
    }

    // Restaurants
    match /restaurants/{restaurantId} {
      // Any signed-in user can read
      allow read: if request.auth != null;

      // Any signed-in user can create
      // WARNING: this rule is for demo purposes only!
      allow create: if request.auth != null;

      // Updates are allowed if no fields are added and name is unchanged
      allow update: if request.auth != null
                    && (request.resource.data.keys() == resource.data.keys())
                    && isUnchanged("name");

      // Deletes are not allowed.
      // Note: this is the default, there is no need to explicitly state this.
      allow delete: if false;

      // Ratings
      match /ratings/{ratingId} {
        // Any signed-in user can read
        allow read: if request.auth != null;

        // Any signed-in user can create if their uid matches the document
        allow create: if request.auth != null
                      && request.resource.data.userId == request.auth.uid;

        // Deletes and updates are not allowed (default)
        allow update, delete: if false;
      }
    }
  }
}

این قوانین دسترسی را محدود می کند تا اطمینان حاصل شود که مشتریان فقط تغییرات ایمن را انجام می دهند. به عنوان مثال، به‌روزرسانی‌های یک سند رستوران فقط می‌تواند رتبه‌بندی‌ها را تغییر دهد، نه نام یا هر داده غیرقابل تغییر دیگری. رتبه‌بندی‌ها تنها در صورتی ایجاد می‌شوند که شناسه کاربری با کاربر واردشده مطابقت داشته باشد، که از جعل جلوگیری می‌کند.

برای مطالعه بیشتر درباره قوانین امنیتی، به مستندات مراجعه کنید.

11. نتیجه گیری

شما اکنون یک برنامه با امکانات کامل در بالای Firestore ایجاد کرده اید. شما در مورد مهمترین ویژگی های Firestore از جمله:

  • اسناد و مجموعه ها
  • خواندن و نوشتن داده ها
  • مرتب سازی و فیلتر کردن با پرس و جو
  • زیر مجموعه ها
  • معاملات

بیشتر بدانید

برای ادامه یادگیری در مورد Firestor، در اینجا چند مکان خوب برای شروع وجود دارد:

برنامه رستوران در این کد لبه بر اساس برنامه مثال "Friendly Eats" بود. می‌توانید کد منبع آن برنامه را در اینجا مرور کنید.

اختیاری: استقرار در تولید

این برنامه تاکنون فقط از مجموعه شبیه ساز Firebase استفاده کرده است. اگر می خواهید یاد بگیرید که چگونه این برنامه را در یک پروژه Firebase واقعی استقرار دهید، به مرحله بعدی ادامه دهید.

12. (اختیاری) برنامه خود را مستقر کنید

تاکنون این برنامه کاملاً محلی بوده است، همه داده‌ها در مجموعه Firebase Emulator موجود است. در این بخش یاد خواهید گرفت که چگونه پروژه Firebase خود را پیکربندی کنید تا این برنامه در مرحله تولید کار کند.

احراز هویت Firebase

در کنسول Firebase به بخش Authentication بروید و روی شروع کلیک کنید. به برگه روش ورود به سیستم بروید و گزینه ایمیل/گذرواژه را از ارائه دهندگان بومی انتخاب کنید.

روش ورود به سیستم ایمیل/گذرواژه را فعال کنید و روی ذخیره کلیک کنید.

sign-in-providers.png

آتش نشانی

ایجاد پایگاه داده

به بخش Firestore Database کنسول بروید و روی Create Database کلیک کنید:

  1. وقتی در مورد قوانین امنیتی از شما خواسته شد که در حالت تولید شروع به کار کنید، به زودی آن قوانین را به‌روزرسانی خواهیم کرد.
  2. مکان پایگاه داده ای را که می خواهید برای برنامه خود استفاده کنید، انتخاب کنید. توجه داشته باشید که انتخاب مکان پایگاه داده یک تصمیم دائمی است و برای تغییر آن باید یک پروژه جدید ایجاد کنید. برای اطلاعات بیشتر در مورد انتخاب محل پروژه، به مستندات مراجعه کنید.

استقرار قوانین

برای استقرار قوانین امنیتی که قبلاً نوشتید، دستور زیر را در پوشه codelab اجرا کنید:

$ firebase deploy --only firestore:rules

این محتویات firestore.rules را در پروژه شما مستقر می کند، که می توانید با رفتن به تب Rules در کنسول، آن را تأیید کنید.

استقرار شاخص ها

برنامه FriendlyEats مرتب‌سازی و فیلترینگ پیچیده‌ای دارد که به تعدادی شاخص ترکیبی سفارشی نیاز دارد. اینها را می توان با دست در کنسول Firebase ایجاد کرد، اما نوشتن تعاریف آنها در فایل firestore.indexes.json و استقرار آنها با استفاده از Firebase CLI ساده تر است.

اگر فایل firestore.indexes.json را باز کنید، می بینید که ایندکس های مورد نیاز قبلا ارائه شده اند:

{
  "indexes": [
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "city", "mode": "ASCENDING" },
        { "fieldPath": "avgRating", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "category", "mode": "ASCENDING" },
        { "fieldPath": "avgRating", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "price", "mode": "ASCENDING" },
        { "fieldPath": "avgRating", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "city", "mode": "ASCENDING" },
        { "fieldPath": "numRatings", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "category", "mode": "ASCENDING" },
        { "fieldPath": "numRatings", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "price", "mode": "ASCENDING" },
        { "fieldPath": "numRatings", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "city", "mode": "ASCENDING" },
        { "fieldPath": "price", "mode": "ASCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "fields": [
        { "fieldPath": "category", "mode": "ASCENDING" },
        { "fieldPath": "price", "mode": "ASCENDING" }
      ]
    }
  ],
  "fieldOverrides": []
}

برای استقرار این شاخص ها دستور زیر را اجرا کنید:

$ firebase deploy --only firestore:indexes

توجه داشته باشید که ایجاد ایندکس آنی نیست، می توانید پیشرفت را در کنسول Firebase نظارت کنید.

برنامه را پیکربندی کنید

در فایل‌های util/FirestoreInitializer.kt و util/AuthInitializer.kt ما Firebase SDK را برای اتصال به شبیه‌سازها در حالت اشکال‌زدایی پیکربندی کردیم:

    override fun create(context: Context): FirebaseFirestore {
        val firestore = Firebase.firestore
        // Use emulators only in debug builds
        if (BuildConfig.DEBUG) {
            firestore.useEmulator(FIRESTORE_EMULATOR_HOST, FIRESTORE_EMULATOR_PORT)
        }
        return firestore
    }

اگر می خواهید برنامه خود را با پروژه Firebase واقعی خود آزمایش کنید، می توانید:

  1. برنامه را در حالت انتشار بسازید و آن را روی دستگاه اجرا کنید.
  2. به طور موقت BuildConfig.DEBUG با false جایگزین کنید و دوباره برنامه را اجرا کنید.

توجه داشته باشید که ممکن است لازم باشد از برنامه خارج شوید و دوباره وارد شوید تا بتوانید به درستی به تولید وصل شوید.

،

1. بررسی اجمالی

اهداف

در این کد لبه شما یک برنامه توصیه رستوران در اندروید با پشتیبانی Cloud Firestore می‌سازید. شما یاد خواهید گرفت که چگونه:

  • خواندن و نوشتن داده ها در Firestore از یک برنامه Android
  • به تغییرات داده های Firestore در زمان واقعی گوش دهید
  • از Firebase Authentication و قوانین امنیتی برای ایمن سازی داده های Firestore استفاده کنید
  • پرس و جوهای پیچیده Firestore را بنویسید

پیش نیازها

قبل از شروع این کد لبه مطمئن شوید که:

  • Android Studio Flamingo یا جدیدتر
  • شبیه ساز اندروید با API 19 یا بالاتر
  • Node.js نسخه 16 یا بالاتر
  • جاوا نسخه 17 یا بالاتر

2. یک پروژه Firebase ایجاد کنید

  1. با حساب Google خود وارد کنسول Firebase شوید.
  2. در کنسول Firebase ، روی افزودن پروژه کلیک کنید.
  3. همانطور که در تصویر زیر نشان داده شده است، یک نام برای پروژه Firebase خود وارد کنید (به عنوان مثال، "Friendly Eats") و روی Continue کلیک کنید.

9d2f625aebcab6af.png

  1. ممکن است از شما خواسته شود که Google Analytics را فعال کنید، برای اهداف این کد کد، انتخاب شما مهم نیست.
  2. پس از یک دقیقه یا بیشتر، پروژه Firebase شما آماده خواهد شد. روی Continue کلیک کنید.

3. پروژه نمونه را راه اندازی کنید

کد را دانلود کنید

دستور زیر را برای کلون کردن کد نمونه برای این Codelab اجرا کنید. با این کار یک پوشه به نام friendlyeats-android در دستگاه شما ایجاد می شود:

$ git clone https://github.com/firebase/friendlyeats-android

اگر git روی دستگاه خود ندارید، می توانید کد را مستقیماً از GitHub نیز دانلود کنید.

پیکربندی Firebase را اضافه کنید

  1. در کنسول Firebase ، نمای کلی پروژه را در ناوبری سمت چپ انتخاب کنید. برای انتخاب پلتفرم روی دکمه اندروید کلیک کنید. وقتی نام بسته از شما خواسته شد از com.google.firebase.example.fireeats استفاده کنید

73d151ed16016421.png

  1. روی ثبت برنامه کلیک کنید و دستورالعمل ها را برای دانلود فایل google-services.json دنبال کنید و آن را به پوشه app/ کدی که دانلود کرده اید منتقل کنید. سپس روی Next کلیک کنید.

پروژه را وارد کنید

اندروید استودیو را باز کنید. روی File > New > Import Project کلیک کنید و پوشه friendlyeats-android را انتخاب کنید.

4. شبیه سازهای Firebase را راه اندازی کنید

در این کد لبه از مجموعه شبیه ساز Firebase برای شبیه سازی محلی Cloud Firestore و سایر سرویس های Firebase استفاده خواهید کرد. این یک محیط توسعه محلی امن، سریع و بدون هزینه برای ساخت برنامه شما فراهم می کند.

Firebase CLI را نصب کنید

ابتدا باید Firebase CLI را نصب کنید. اگر از macOS یا Linux استفاده می کنید، می توانید دستور cURL زیر را اجرا کنید:

curl -sL https://firebase.tools | bash

اگر از ویندوز استفاده می کنید، دستورالعمل های نصب را بخوانید تا یک باینری مستقل دریافت کنید یا از طریق npm نصب کنید.

پس از نصب CLI، اجرای firebase --version باید نسخه 9.0.0 یا بالاتر را گزارش کند:

$ firebase --version
9.0.0

وارد شوید

برای اتصال CLI به حساب Google خود firebase login اجرا کنید. با این کار یک پنجره مرورگر جدید برای تکمیل فرآیند ورود باز می شود. اطمینان حاصل کنید که همان حسابی را انتخاب کنید که قبلاً هنگام ایجاد پروژه Firebase خود استفاده می کردید.

از داخل پوشه friendlyeats-android ، firebase use --add . دستورات را دنبال کنید تا پروژه ای را که قبلا ایجاد کرده اید انتخاب کنید و اگر از شما خواسته شد نام مستعار را انتخاب کنید default وارد کنید.

5. برنامه را اجرا کنید

اکنون زمان اجرای Firebase Emulator Suite و اپلیکیشن اندروید FriendlyEats برای اولین بار است.

شبیه سازها را اجرا کنید

در ترمینال خود از داخل دایرکتوری friendlyeats-android firebase emulators:start به راه اندازی شبیه سازهای Firebase کنید. شما باید گزارش هایی مانند این را ببینید:

$ firebase emulators:start
i  emulators: Starting emulators: auth, firestore
i  firestore: Firestore Emulator logging to firestore-debug.log
i  ui: Emulator UI logging to ui-debug.log

┌─────────────────────────────────────────────────────────────┐
│ ✔  All emulators ready! It is now safe to connect your app. │
│ i  View Emulator UI at http://localhost:4000                │
└─────────────────────────────────────────────────────────────┘

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ localhost:9099 │ http://localhost:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ localhost:8080 │ http://localhost:4000/firestore │
└────────────────┴────────────────┴─────────────────────────────────┘
  Emulator Hub running at localhost:4400
  Other reserved ports: 4500

Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.

شما اکنون یک محیط توسعه محلی کامل در حال اجرا بر روی دستگاه خود دارید! مطمئن شوید که این دستور را برای بقیه قسمت‌های Codelab اجرا می‌کنید، برنامه اندروید شما باید به شبیه‌سازها متصل شود.

برنامه را به شبیه سازها وصل کنید

فایل‌های util/FirestoreInitializer.kt و util/AuthInitializer.kt را در Android Studio باز کنید. این فایل ها حاوی منطق اتصال Firebase SDK به شبیه سازهای محلی در حال اجرا بر روی دستگاه شما، پس از راه اندازی برنامه هستند.

در متد create() کلاس FirestoreInitializer ، این قطعه کد را بررسی کنید:

    // Use emulators only in debug builds
    if (BuildConfig.DEBUG) {
        firestore.useEmulator(FIRESTORE_EMULATOR_HOST, FIRESTORE_EMULATOR_PORT)
    }

ما از BuildConfig استفاده می کنیم تا مطمئن شویم فقط زمانی که برنامه ما در حالت debug اجرا می شود به شبیه سازها متصل می شویم. وقتی برنامه را در حالت release کامپایل می کنیم، این شرط نادرست خواهد بود.

می بینیم که از روش useEmulator(host, port) برای اتصال Firebase SDK به شبیه ساز محلی Firestore استفاده می کند. در سرتاسر برنامه از FirebaseUtil.getFirestore() برای دسترسی به این نمونه از FirebaseFirestore استفاده می‌کنیم، بنابراین مطمئن هستیم که همیشه هنگام اجرای در حالت debug به شبیه‌ساز Firestore متصل می‌شویم.

برنامه را اجرا کنید

اگر فایل google-services.json را به درستی اضافه کرده اید، اکنون پروژه باید کامپایل شود. در Android Studio روی Build > Rebuild Project کلیک کنید و مطمئن شوید که هیچ خطایی باقی نمانده است.

در اندروید استودیو برنامه را روی شبیه ساز اندروید خود اجرا کنید . در ابتدا با یک صفحه "ورود به سیستم" نمایش داده می شود. برای ورود به برنامه می توانید از هر ایمیل و رمز عبوری استفاده کنید. این فرآیند ورود به سیستم در حال اتصال به شبیه‌ساز احراز هویت Firebase است، بنابراین هیچ اعتبار واقعی منتقل نمی‌شود.

اکنون رابط کاربری Emulators را با رفتن به http://localhost:4000 در مرورگر وب خود باز کنید. سپس بر روی تب Authentication کلیک کنید و باید حسابی را که ایجاد کرده اید مشاهده کنید:

شبیه ساز Firebase Auth

پس از تکمیل فرآیند ورود، باید صفحه اصلی برنامه را ببینید:

de06424023ffb4b9.png

به زودی مقداری داده برای پر کردن صفحه اصلی اضافه خواهیم کرد.

6. داده ها را در Firestore بنویسید

در این بخش داده هایی را در Firestore می نویسیم تا بتوانیم صفحه اصلی خالی فعلی را پر کنیم.

شی مدل اصلی در برنامه ما یک رستوران است (به model/Restaurant.kt مراجعه کنید). داده های Firestore به اسناد، مجموعه ها و زیر مجموعه ها تقسیم می شوند. ما هر رستوران را به عنوان یک سند در یک مجموعه سطح بالا به نام "restaurants" ذخیره خواهیم کرد. برای کسب اطلاعات بیشتر در مورد مدل داده Firestore، اسناد و مجموعه‌ها را در اسناد مطالعه کنید.

برای اهداف نمایشی، وقتی روی دکمه "افزودن موارد تصادفی" در منوی سرریز کلیک می کنیم، عملکردی را به برنامه اضافه می کنیم تا ده رستوران تصادفی ایجاد کنیم. فایل MainFragment.kt را باز کنید و محتوای متد onAddItemsClicked() را با:

    private fun onAddItemsClicked() {
        val restaurantsRef = firestore.collection("restaurants")
        for (i in 0..9) {
            // Create random restaurant / ratings
            val randomRestaurant = RestaurantUtil.getRandom(requireContext())

            // Add restaurant
            restaurantsRef.add(randomRestaurant)
        }
    }

چند نکته مهم در مورد کد بالا وجود دارد:

  • ما با ارجاع به مجموعه "restaurants" شروع کردیم. مجموعه ها به طور ضمنی هنگام اضافه شدن اسناد ایجاد می شوند، بنابراین نیازی به ایجاد مجموعه قبل از نوشتن داده ها وجود نداشت.
  • اسناد را می توان با استفاده از کلاس های داده Kotlin ایجاد کرد، که ما از آنها برای ایجاد هر Doc رستوران استفاده می کنیم.
  • متد add() سندی را به مجموعه ای با شناسه تولید شده خودکار اضافه می کند، بنابراین نیازی به تعیین شناسه منحصر به فرد برای هر رستوران نداریم.

اکنون برنامه را دوباره اجرا کنید و روی دکمه "افزودن موارد تصادفی" در منوی سرریز (در گوشه بالا سمت راست) کلیک کنید تا کدی را که نوشتید فراخوانی کنید:

95691e9b71ba55e3.png

اکنون رابط کاربری Emulators را با رفتن به http://localhost:4000 در مرورگر وب خود باز کنید. سپس بر روی تب Firestore کلیک کنید و باید داده هایی را که به تازگی اضافه کرده اید مشاهده کنید:

شبیه ساز Firebase Auth

این داده ها 100% محلی برای دستگاه شما هستند. در واقع، پروژه واقعی شما هنوز حاوی پایگاه داده Firestore نیست! این بدان معناست که آزمایش با تغییر و حذف این داده‌ها بدون عواقب بی‌خطر است.

تبریک می‌گوییم، شما فقط داده‌ها را به Firestore نوشتید! در مرحله بعدی نحوه نمایش این داده ها را در برنامه یاد خواهیم گرفت.

7. نمایش داده ها از Firestore

در این مرحله یاد می گیریم که چگونه داده ها را از Firestore بازیابی کرده و در برنامه خود نمایش دهیم. اولین قدم برای خواندن داده ها از Firestore ایجاد یک Query است. فایل MainFragment.kt را باز کنید و کد زیر را به ابتدای متد onViewCreated() اضافه کنید:

        // Firestore
        firestore = Firebase.firestore

        // Get the 50 highest rated restaurants
        query = firestore.collection("restaurants")
            .orderBy("avgRating", Query.Direction.DESCENDING)
            .limit(LIMIT.toLong())

اکنون می‌خواهیم به پرس و جو گوش دهیم تا همه اسناد منطبق را دریافت کنیم و از به‌روزرسانی‌های آینده در زمان واقعی مطلع شویم. از آنجایی که هدف نهایی ما اتصال این داده ها به یک RecyclerView است، باید یک کلاس RecyclerView.Adapter ایجاد کنیم تا به داده ها گوش دهیم.

کلاس FirestoreAdapter را که قبلاً تا حدی پیاده سازی شده است باز کنید. ابتدا، اجازه دهید آداپتور را اجرا کنیم که EventListener اجرا کرده و تابع onEvent را تعریف کنیم تا بتواند به‌روزرسانی‌های یک پرسش Firestore را دریافت کند:

abstract class FirestoreAdapter<VH : RecyclerView.ViewHolder>(private var query: Query?) :
        RecyclerView.Adapter<VH>(),
        EventListener<QuerySnapshot> { // Add this implements
    
    // ...

    // Add this method
    override fun onEvent(documentSnapshots: QuerySnapshot?, e: FirebaseFirestoreException?) {
        
        // Handle errors
        if (e != null) {
            Log.w(TAG, "onEvent:error", e)
            return
        }

        // Dispatch the event
        if (documentSnapshots != null) {
            for (change in documentSnapshots.documentChanges) {
                // snapshot of the changed document
                when (change.type) {
                    DocumentChange.Type.ADDED -> {
                        // TODO: handle document added
                    }
                    DocumentChange.Type.MODIFIED -> {
                        // TODO: handle document changed
                    }
                    DocumentChange.Type.REMOVED -> {
                        // TODO: handle document removed
                    }
                }
            }
        }

        onDataChanged()
    }
    
    // ...
}

در بارگذاری اولیه شنونده یک رویداد ADDED برای هر سند جدید دریافت می کند. از آنجایی که مجموعه نتیجه پرس و جو در طول زمان تغییر می کند، شنونده رویدادهای بیشتری حاوی تغییرات را دریافت خواهد کرد. حالا بیایید اجرای شنونده را به پایان برسانیم. ابتدا سه روش جدید اضافه کنید: onDocumentAdded ، onDocumentModified و onDocumentRemoved :

    private fun onDocumentAdded(change: DocumentChange) {
        snapshots.add(change.newIndex, change.document)
        notifyItemInserted(change.newIndex)
    }

    private fun onDocumentModified(change: DocumentChange) {
        if (change.oldIndex == change.newIndex) {
            // Item changed but remained in same position
            snapshots[change.oldIndex] = change.document
            notifyItemChanged(change.oldIndex)
        } else {
            // Item changed and changed position
            snapshots.removeAt(change.oldIndex)
            snapshots.add(change.newIndex, change.document)
            notifyItemMoved(change.oldIndex, change.newIndex)
        }
    }

    private fun onDocumentRemoved(change: DocumentChange) {
        snapshots.removeAt(change.oldIndex)
        notifyItemRemoved(change.oldIndex)
    }

سپس این متدهای جدید را از onEvent فراخوانی کنید:

    override fun onEvent(documentSnapshots: QuerySnapshot?, e: FirebaseFirestoreException?) {

        // Handle errors
        if (e != null) {
            Log.w(TAG, "onEvent:error", e)
            return
        }

        // Dispatch the event
        if (documentSnapshots != null) {
            for (change in documentSnapshots.documentChanges) {
                // snapshot of the changed document
                when (change.type) {
                    DocumentChange.Type.ADDED -> {
                        onDocumentAdded(change) // Add this line
                    }
                    DocumentChange.Type.MODIFIED -> {
                        onDocumentModified(change) // Add this line
                    }
                    DocumentChange.Type.REMOVED -> {
                        onDocumentRemoved(change) // Add this line
                    }
                }
            }
        }

        onDataChanged()
    }

در نهایت متد startListening() را برای پیوست کردن شنونده پیاده سازی کنید:

    fun startListening() {
        if (registration == null) {
            registration = query.addSnapshotListener(this)
        }
    }

اکنون برنامه به طور کامل برای خواندن داده ها از Firestore پیکربندی شده است. دوباره برنامه را اجرا کنید و باید رستوران هایی را که در مرحله قبل اضافه کرده اید ببینید:

9e45f40faefce5d0.png

اکنون به رابط کاربری Emulator در مرورگر خود برگردید و یکی از نام‌های رستوران را ویرایش کنید. شما باید تغییر آن را در برنامه تقریباً بلافاصله مشاهده کنید!

8. مرتب سازی و فیلتر کردن داده ها

این برنامه در حال حاضر بهترین رستوران ها را در کل مجموعه نمایش می دهد، اما در یک برنامه رستوران واقعی کاربر می خواهد داده ها را مرتب و فیلتر کند. برای مثال، برنامه باید بتواند «رستوران‌های برتر غذاهای دریایی در فیلادلفیا» یا «ارزان‌ترین پیتزا» را نشان دهد.

با کلیک بر روی نوار سفید در بالای برنامه، یک گفتگوی فیلترها ظاهر می شود. در این بخش از پرس و جوهای Firestore برای کارکرد این گفتگو استفاده خواهیم کرد:

67898572a35672a5.png

بیایید متد onFilter() MainFragment.kt را ویرایش کنیم. این روش یک شی Filters را می پذیرد که یک شی کمکی است که ما برای گرفتن خروجی گفتگوی فیلترها ایجاد کرده ایم. ما این روش را برای ساخت یک پرس و جو از فیلترها تغییر می دهیم:

    override fun onFilter(filters: Filters) {
        // Construct query basic query
        var query: Query = firestore.collection("restaurants")

        // Category (equality filter)
        if (filters.hasCategory()) {
            query = query.whereEqualTo(Restaurant.FIELD_CATEGORY, filters.category)
        }

        // City (equality filter)
        if (filters.hasCity()) {
            query = query.whereEqualTo(Restaurant.FIELD_CITY, filters.city)
        }

        // Price (equality filter)
        if (filters.hasPrice()) {
            query = query.whereEqualTo(Restaurant.FIELD_PRICE, filters.price)
        }

        // Sort by (orderBy with direction)
        if (filters.hasSortBy()) {
            query = query.orderBy(filters.sortBy.toString(), filters.sortDirection)
        }

        // Limit items
        query = query.limit(LIMIT.toLong())

        // Update the query
        adapter.setQuery(query)

        // Set header
        binding.textCurrentSearch.text = HtmlCompat.fromHtml(
            filters.getSearchDescription(requireContext()),
            HtmlCompat.FROM_HTML_MODE_LEGACY
        )
        binding.textCurrentSortBy.text = filters.getOrderDescription(requireContext())

        // Save filters
        viewModel.filters = filters
    }

در قطعه بالا، یک شی Query را با پیوست کردن عبارت‌های where و orderBy برای مطابقت با فیلترهای داده شده می‌سازیم.

برنامه را دوباره اجرا کنید و فیلتر زیر را برای نمایش محبوب ترین رستوران های ارزان قیمت انتخاب کنید:

7a67a8a400c80c50.png

اکنون باید یک لیست فیلتر شده از رستوران ها را ببینید که فقط شامل گزینه های ارزان قیمت است:

a670188398c3c59.png

اگر تا اینجا پیش رفته اید، اکنون یک برنامه مشاهده توصیه رستوران کاملاً کارآمد در Firestore ساخته اید! اکنون می توانید رستوران ها را در زمان واقعی مرتب و فیلتر کنید. در چند بخش بعدی نظراتی را به رستوران ها اضافه می کنیم و قوانین امنیتی را به برنامه اضافه می کنیم.

9. داده ها را در زیر مجموعه ها سازماندهی کنید

در این بخش رتبه‌بندی‌هایی را به برنامه اضافه می‌کنیم تا کاربران بتوانند رستوران‌های مورد علاقه (یا حداقل مورد علاقه) خود را بررسی کنند.

مجموعه ها و زیر مجموعه ها

تا کنون ما تمام داده های رستوران را در یک مجموعه سطح بالا به نام "رستوران" ذخیره کرده ایم. وقتی کاربر به یک رستوران امتیاز می دهد، می خواهیم یک شی Rating جدید به رستوران ها اضافه کنیم. برای این کار از یک زیر مجموعه استفاده می کنیم. شما می توانید یک زیر مجموعه را به عنوان مجموعه ای در نظر بگیرید که به یک سند پیوست شده است. بنابراین هر سند رستوران یک زیرمجموعه رتبه بندی پر از اسناد رتبه بندی خواهد داشت. مجموعه‌های فرعی به سازماندهی داده‌ها کمک می‌کنند بدون اینکه اسناد ما را متورم کند یا نیاز به جستجوهای پیچیده داشته باشد.

برای دسترسی به یک زیر مجموعه، .collection() را در سند والد فراخوانی کنید:

val subRef = firestore.collection("restaurants")
        .document("abc123")
        .collection("ratings")

شما می توانید مانند مجموعه های سطح بالا به یک زیرمجموعه دسترسی داشته باشید و آن را پرس و جو کنید، هیچ محدودیتی در اندازه یا تغییر عملکرد وجود ندارد. در اینجا می توانید اطلاعات بیشتری در مورد مدل داده Firestore بخوانید.

نوشتن داده ها در تراکنش

برای افزودن Rating به زیرمجموعه مناسب، فقط نیاز به فراخوانی .add() است، اما همچنین باید میانگین رتبه‌بندی شی Restaurant و تعداد رتبه‌بندی‌ها را به‌روزرسانی کنیم تا داده‌های جدید را منعکس کنیم. اگر از عملیات جداگانه برای ایجاد این دو تغییر استفاده کنیم، تعدادی شرایط مسابقه وجود دارد که می تواند منجر به داده های قدیمی یا نادرست شود.

برای اطمینان از اینکه رتبه‌بندی‌ها به درستی اضافه می‌شوند، از تراکنش برای افزودن رتبه‌بندی به یک رستوران استفاده می‌کنیم. این تراکنش چند عمل انجام می دهد:

  • رتبه فعلی رستوران را بخوانید و امتیاز جدید را محاسبه کنید
  • امتیاز را به زیر مجموعه اضافه کنید
  • میانگین رتبه‌بندی رستوران و تعداد رتبه‌بندی‌ها را به‌روزرسانی کنید

RestaurantDetailFragment.kt را باز کنید و تابع addRating را پیاده سازی کنید:

    private fun addRating(restaurantRef: DocumentReference, rating: Rating): Task<Void> {
        // Create reference for new rating, for use inside the transaction
        val ratingRef = restaurantRef.collection("ratings").document()

        // In a transaction, add the new rating and update the aggregate totals
        return firestore.runTransaction { transaction ->
            val restaurant = transaction.get(restaurantRef).toObject<Restaurant>()
                ?: throw Exception("Restaurant not found at ${restaurantRef.path}")

            // Compute new number of ratings
            val newNumRatings = restaurant.numRatings + 1

            // Compute new average rating
            val oldRatingTotal = restaurant.avgRating * restaurant.numRatings
            val newAvgRating = (oldRatingTotal + rating.rating) / newNumRatings

            // Set new restaurant info
            restaurant.numRatings = newNumRatings
            restaurant.avgRating = newAvgRating

            // Commit to Firestore
            transaction.set(restaurantRef, restaurant)
            transaction.set(ratingRef, rating)

            null
        }
    }

تابع addRating() یک Task را برمی گرداند که کل تراکنش را نشان می دهد. در تابع onRating() شنوندگان به وظیفه اضافه می شوند تا به نتیجه تراکنش پاسخ دهند.

حالا دوباره برنامه را اجرا کنید و روی یکی از رستوران ها کلیک کنید، که باید صفحه جزئیات رستوران ظاهر شود. برای شروع اضافه کردن نظر، روی دکمه + کلیک کنید. با انتخاب تعدادی ستاره و وارد کردن متن، نظری را اضافه کنید.

78fa16cdf8ef435a.png

با زدن گزینه Submit تراکنش آغاز می شود. وقتی تراکنش کامل شد، نظر خود را در زیر نمایش داده می‌شود و به‌روزرسانی تعداد بازبینی رستوران را مشاهده می‌کنید:

f9e670f40bd615b0.png

تبریک میگم اکنون یک برنامه بررسی رستوران اجتماعی، محلی و موبایل دارید که بر روی Cloud Firestore ساخته شده است. شنیده ام که این روزها بسیار محبوب هستند.

10. داده های خود را ایمن کنید

تا کنون امنیت این اپلیکیشن را در نظر نگرفته ایم. چگونه بفهمیم که کاربران فقط می توانند داده های صحیح خود را بخوانند و بنویسند؟ پایگاه داده های Firestore توسط یک فایل پیکربندی به نام قوانین امنیتی محافظت می شوند.

فایل firestore.rules را باز کنید، باید موارد زیر را مشاهده کنید:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      //
      // WARNING: These rules are insecure! We will replace them with
      // more secure rules later in the codelab
      //
      allow read, write: if request.auth != null;
    }
  }
}

بیایید این قوانین را تغییر دهیم تا از دسترسی یا تغییرات ناخواسته به داده ها جلوگیری کنیم، فایل firestore.rules را باز کرده و محتوا را با موارد زیر جایگزین کنیم:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Determine if the value of the field "key" is the same
    // before and after the request.
    function isUnchanged(key) {
      return (key in resource.data)
        && (key in request.resource.data)
        && (resource.data[key] == request.resource.data[key]);
    }

    // Restaurants
    match /restaurants/{restaurantId} {
      // Any signed-in user can read
      allow read: if request.auth != null;

      // Any signed-in user can create
      // WARNING: this rule is for demo purposes only!
      allow create: if request.auth != null;

      // Updates are allowed if no fields are added and name is unchanged
      allow update: if request.auth != null
                    && (request.resource.data.keys() == resource.data.keys())
                    && isUnchanged("name");

      // Deletes are not allowed.
      // Note: this is the default, there is no need to explicitly state this.
      allow delete: if false;

      // Ratings
      match /ratings/{ratingId} {
        // Any signed-in user can read
        allow read: if request.auth != null;

        // Any signed-in user can create if their uid matches the document
        allow create: if request.auth != null
                      && request.resource.data.userId == request.auth.uid;

        // Deletes and updates are not allowed (default)
        allow update, delete: if false;
      }
    }
  }
}

این قوانین دسترسی را محدود می کند تا اطمینان حاصل شود که مشتریان فقط تغییرات ایمن را ایجاد می کنند. به عنوان مثال به روزرسانی های یک سند رستوران فقط می تواند رتبه بندی ها را تغییر دهد ، نه نام یا هر داده تغییر ناپذیر دیگر. رتبه بندی فقط در صورتی ایجاد می شود که شناسه کاربر با کاربر وارد شده با کاربر وارد شود ، که از کلاهبرداری جلوگیری می کند.

برای مطالعه بیشتر در مورد قوانین امنیتی ، به اسناد مراجعه کنید.

11. نتیجه گیری

شما اکنون یک برنامه کاملاً برجسته در بالای Firestore ایجاد کرده اید. شما در مورد مهمترین ویژگی های آتش نشانی از جمله:

  • اسناد و مجموعه ها
  • خواندن و نوشتن داده ها
  • مرتب سازی و فیلتر با نمایش داده شد
  • زیر مجموعه
  • معاملات

بیشتر بدانید

برای ادامه یادگیری در مورد Firestore ، در اینجا مکانهای خوبی برای شروع وجود دارد:

برنامه رستوران در این CodeLab بر اساس برنامه "Eats Friendly Eats" ساخته شده است. می توانید در اینجا کد منبع را برای آن برنامه مرور کنید.

اختیاری: استقرار به تولید

تاکنون این برنامه فقط از مجموعه Emulator Firebase استفاده کرده است. اگر می خواهید یاد بگیرید که چگونه این برنامه را در یک پروژه واقعی Firebase مستقر کنید ، به مرحله بعدی ادامه دهید.

12. (اختیاری) برنامه خود را مستقر کنید

تاکنون این برنامه کاملاً محلی بوده است ، تمام داده ها در مجموعه Emulator Firebase موجود است. در این بخش یاد می گیرید که چگونه پروژه Firebase خود را پیکربندی کنید تا این برنامه در تولید کار کند.

احراز هویت Firebase

در کنسول Firebase به بخش احراز هویت بروید و روی شروع کار کلیک کنید. به برگه روش ورود به سیستم بروید و از ارائه دهندگان بومی گزینه ایمیل/رمز عبور را انتخاب کنید.

روش ورود به سیستم ایمیل/رمز عبور را فعال کرده و روی ذخیره کلیک کنید.

Sign-in-Providers.png

آتش نشانی

ایجاد پایگاه داده

به بخش پایگاه داده Firestore کنسول بروید و روی ایجاد پایگاه داده کلیک کنید:

  1. هنگامی که از قوانین امنیتی خواسته شد در حالت تولید شروع به کار کنید ، به زودی این قوانین را به روز می کنیم.
  2. مکان پایگاه داده را که می خواهید برای برنامه خود استفاده کنید انتخاب کنید. توجه داشته باشید که انتخاب یک مکان پایگاه داده یک تصمیم دائمی است و برای تغییر آن باید یک پروژه جدید ایجاد کنید. برای کسب اطلاعات بیشتر در مورد انتخاب مکان پروژه ، به اسناد مراجعه کنید.

اعزام قوانین

برای استقرار قوانین امنیتی که قبلاً نوشتید ، دستور زیر را در فهرست CodeLab اجرا کنید:

$ firebase deploy --only firestore:rules

با این کار محتوای firestore.rules به پروژه شما مستقر می شود ، که می توانید با پیمایش به برگه قوانین موجود در کنسول تأیید کنید.

شاخص ها را مستقر کنید

برنامه FriendlyEats دارای مرتب سازی و فیلتر پیچیده ای است که به تعدادی از شاخص های ترکیبی سفارشی نیاز دارد. اینها را می توان با دست در کنسول Firebase ایجاد کرد اما نوشتن تعاریف آنها در پرونده firestore.indexes.json ساده تر است و آنها را با استفاده از Firebase CLI مستقر می کند.

اگر پرونده firestore.indexes.json را باز کنید ، خواهید دید که فهرست های مورد نیاز قبلاً ارائه شده است:

{
  "indexes": [
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "city", "mode": "ASCENDING" },
        { "fieldPath": "avgRating", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "category", "mode": "ASCENDING" },
        { "fieldPath": "avgRating", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "price", "mode": "ASCENDING" },
        { "fieldPath": "avgRating", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "city", "mode": "ASCENDING" },
        { "fieldPath": "numRatings", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "category", "mode": "ASCENDING" },
        { "fieldPath": "numRatings", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "price", "mode": "ASCENDING" },
        { "fieldPath": "numRatings", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "city", "mode": "ASCENDING" },
        { "fieldPath": "price", "mode": "ASCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "fields": [
        { "fieldPath": "category", "mode": "ASCENDING" },
        { "fieldPath": "price", "mode": "ASCENDING" }
      ]
    }
  ],
  "fieldOverrides": []
}

برای استقرار این فهرست ها دستور زیر را اجرا کنید:

$ firebase deploy --only firestore:indexes

توجه داشته باشید که ایجاد شاخص فوری نیست ، می توانید پیشرفت در کنسول Firebase را کنترل کنید.

پیکربندی برنامه

در پرونده های util/FirestoreInitializer.kt و util/AuthInitializer.kt ما SDK Firebase را پیکربندی کردیم تا در حالت اشکال زدایی به شبیه سازها وصل شود:

    override fun create(context: Context): FirebaseFirestore {
        val firestore = Firebase.firestore
        // Use emulators only in debug builds
        if (BuildConfig.DEBUG) {
            firestore.useEmulator(FIRESTORE_EMULATOR_HOST, FIRESTORE_EMULATOR_PORT)
        }
        return firestore
    }

اگر می خواهید برنامه خود را با پروژه Firebase واقعی خود آزمایش کنید ، می توانید:

  1. برنامه را در حالت انتشار بسازید و آن را بر روی یک دستگاه اجرا کنید.
  2. BuildConfig.DEBUG به طور false جایگزین کرده و دوباره برنامه را اجرا کنید.

توجه داشته باشید که ممکن است لازم باشد از برنامه خارج شوید و دوباره وارد سیستم شوید تا به درستی به تولید متصل شوید.