Lớp học lập trình Android Cloud Firestore

1. Tổng quan

Bàn thắng

Trong lớp học lập trình này, bạn sẽ xây dựng một ứng dụng đề xuất về nhà hàng trên Android được Cloud Firestore hỗ trợ. Bạn sẽ tìm hiểu cách:

  • Đọc và ghi dữ liệu vào Firestore từ một ứng dụng Android
  • Theo dõi các thay đổi trong dữ liệu Firestore theo thời gian thực
  • Sử dụng các quy tắc bảo mật và Xác thực Firebase để bảo mật dữ liệu Firestore
  • Viết các truy vấn phức tạp trên Firestore

Điều kiện tiên quyết

Trước khi bắt đầu lớp học lập trình này, hãy đảm bảo bạn đã:

  • Android Studio Flamingo trở lên
  • Trình mô phỏng Android có API 19 trở lên
  • Node.js phiên bản 16 trở lên
  • Phiên bản Java 17 trở lên

2. Tạo một dự án Firebase

  1. Đăng nhập vào bảng điều khiển của Firebase bằng Tài khoản Google của bạn.
  2. Trong bảng điều khiển của Firebase, hãy nhấp vào Thêm dự án.
  3. Như minh hoạ trong ảnh chụp màn hình bên dưới, hãy nhập tên cho dự án Firebase của bạn (ví dụ: "Consumer Eats") rồi nhấp vào Tiếp tục.

9d2f625aebcab6af.pngs

  1. Bạn có thể được yêu cầu bật Google Analytics. Vì mục đích của lớp học lập trình này, lựa chọn của bạn không quan trọng.
  2. Sau khoảng 1 phút, dự án Firebase của bạn sẽ sẵn sàng. Nhấp vào Tiếp tục.

3. Thiết lập dự án mẫu

Tải đoạn mã

Chạy lệnh sau để sao chép mã mẫu cho lớp học lập trình này. Thao tác này sẽ tạo một thư mục có tên friendlyeats-android trên máy của bạn:

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

Nếu không có git trên máy của mình, bạn cũng có thể tải mã này trực tiếp xuống qua GitHub.

Thêm cấu hình Firebase

  1. Trong bảng điều khiển của Firebase, hãy chọn Tổng quan về dự án trong bảng điều hướng bên trái. Nhấp vào nút Android để chọn nền tảng. Khi được nhắc nhập tên gói, hãy sử dụng com.google.firebase.example.fireeats

73d151ed16016421.pngS

  1. Nhấp vào Subscriptions App (Đăng ký ứng dụng) rồi làm theo hướng dẫn để tải tệp google-services.json xuống, rồi chuyển tệp này vào thư mục app/ của đoạn mã mà bạn vừa tải xuống. Sau đó, hãy nhấp vào Tiếp theo.

Nhập dự án

Mở Android Studio Nhấp vào File (Tệp) >. Mới > Import Project (Nhập dự án) rồi chọn thư mục friendlyeats-android.

4. Thiết lập Trình mô phỏng Firebase

Trong lớp học lập trình này, bạn sẽ sử dụng Bộ mô phỏng Firebase để mô phỏng cục bộ Cloud Firestore và các dịch vụ Firebase khác. Việc này mang đến một môi trường phát triển tại địa phương an toàn, nhanh chóng và miễn phí để tạo ứng dụng.

Cài đặt Firebase CLI

Trước tiên, bạn sẽ cần cài đặt Firebase CLI. Nếu đang sử dụng macOS hoặc Linux, bạn có thể chạy lệnh cURL sau:

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

Nếu bạn đang sử dụng Windows, hãy đọc hướng dẫn cài đặt để tải tệp nhị phân độc lập hoặc cài đặt qua npm.

Sau khi bạn cài đặt CLI, việc chạy firebase --version sẽ báo cáo phiên bản 9.0.0 trở lên:

$ firebase --version
9.0.0

Đăng nhập

Chạy firebase login để kết nối giao diện dòng lệnh (CLI) với Tài khoản Google của bạn. Một cửa sổ trình duyệt mới sẽ mở ra để hoàn tất quá trình đăng nhập. Hãy nhớ chọn chính tài khoản mà bạn đã dùng khi tạo dự án Firebase trước đó.

Trong thư mục friendlyeats-android, hãy chạy firebase use --add để kết nối dự án cục bộ với dự án Firebase của bạn. Làm theo lời nhắc để chọn dự án bạn đã tạo trước đó. Nếu được yêu cầu chọn một bí danh, hãy nhập default.

5. Chạy ứng dụng

Giờ là lúc bạn có thể chạy Bộ mô phỏng Firebase và ứng dụng Android friendlyEats lần đầu tiên.

Chạy trình mô phỏng

Trong thiết bị đầu cuối của bạn, từ trong thư mục friendlyeats-android, hãy chạy firebase emulators:start để khởi động Trình mô phỏng Firebase. Bạn sẽ thấy các nhật ký như sau:

$ 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.

Bây giờ, bạn đã có môi trường phát triển cục bộ hoàn chỉnh đang chạy trên máy của mình! Hãy nhớ tiếp tục chạy lệnh này trong phần còn lại của lớp học lập trình, ứng dụng Android của bạn sẽ cần kết nối với trình mô phỏng.

Kết nối ứng dụng với Trình mô phỏng

Mở các tệp util/FirestoreInitializer.ktutil/AuthInitializer.kt trong Android Studio. Các tệp này chứa logic để kết nối SDK Firebase với trình mô phỏng cục bộ chạy trên máy của bạn khi khởi động ứng dụng.

Trên phương thức create() của lớp FirestoreInitializer, hãy kiểm tra đoạn mã sau:

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

Chúng ta đang sử dụng BuildConfig để đảm bảo chỉ kết nối với trình mô phỏng khi ứng dụng đang chạy ở chế độ debug. Khi chúng ta biên dịch ứng dụng ở chế độ release, điều kiện này sẽ là false.

Chúng ta có thể thấy rằng công cụ này đang sử dụng phương thức useEmulator(host, port) để kết nối Firebase SDK với trình mô phỏng Firestore cục bộ. Trong toàn bộ ứng dụng, chúng ta sẽ dùng FirebaseUtil.getFirestore() để truy cập vào thực thể này của FirebaseFirestore. Điều này giúp chúng ta chắc chắn rằng chúng ta luôn kết nối với trình mô phỏng Firestore khi chạy ở chế độ debug.

Chạy ứng dụng

Nếu bạn đã thêm tệp google-services.json đúng cách thì dự án hiện sẽ được biên dịch. Trong Android Studio, hãy nhấp vào Build (Tạo) > Tạo lại dự án và đảm bảo không còn lỗi nào.

Trong Android Studio, hãy Chạy ứng dụng trên trình mô phỏng Android. Trước tiên, bạn sẽ thấy nút "Đăng nhập" màn hình. Bạn có thể sử dụng bất kỳ email và mật khẩu nào để đăng nhập vào ứng dụng. Quy trình đăng nhập này đang kết nối với Trình mô phỏng xác thực Firebase, nên không có thông tin đăng nhập thực nào được truyền.

Bây giờ, hãy mở giao diện người dùng của Trình mô phỏng bằng cách chuyển đến http://localhost:4000 trên trình duyệt web của bạn. Sau đó, hãy nhấp vào thẻ Xác thực và bạn sẽ thấy tài khoản mình vừa tạo:

Trình mô phỏng xác thực Firebase

Sau khi hoàn tất quá trình đăng nhập, bạn sẽ thấy màn hình chính của ứng dụng:

de06424023ffb4b9.png.

Chúng ta sẽ sớm thêm một số dữ liệu để đưa vào màn hình chính.

6. Ghi dữ liệu vào Firestore

Trong phần này, chúng ta sẽ ghi một số dữ liệu vào Firestore để có thể điền sẵn dữ liệu trên màn hình chính hiện đang trống.

Đối tượng mô hình chính trong ứng dụng của chúng ta là một nhà hàng (xem model/Restaurant.kt). Dữ liệu trên Firestore được chia thành các tài liệu, bộ sưu tập và tập hợp con. Chúng ta sẽ lưu trữ mỗi nhà hàng dưới dạng tài liệu trong tập hợp cấp cao nhất có tên là "restaurants". Để tìm hiểu thêm về mô hình dữ liệu Firestore, hãy tham khảo các tài liệu và tập hợp trong tài liệu này.

Để minh hoạ, chúng ta sẽ thêm chức năng vào ứng dụng để tạo 10 nhà hàng ngẫu nhiên khi nhấp vào "Thêm vật phẩm ngẫu nhiên" trong trình đơn mục bổ sung. Mở tệp MainFragment.kt rồi thay thế nội dung trong phương thức onAddItemsClicked() bằng:

    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)
        }
    }

Có một vài điều quan trọng cần lưu ý về mã ở trên:

  • Chúng tôi bắt đầu bằng cách tham chiếu đến bộ sưu tập "restaurants". Tập hợp được tạo hoàn toàn khi tài liệu được thêm vào, do đó, bạn không cần phải tạo tập hợp trước khi ghi dữ liệu.
  • Bạn có thể tạo tài liệu bằng các lớp dữ liệu trong Kotlin mà chúng ta sử dụng để tạo từng tài liệu về Nhà hàng.
  • Phương thức add() thêm một tài liệu vào một bộ sưu tập có mã nhận dạng được tạo tự động, nên chúng ta không cần chỉ định mã nhận dạng duy nhất cho mỗi Nhà hàng.

Bây giờ, hãy chạy lại ứng dụng và nhấp vào "Add Random Items" (Thêm các mục ngẫu nhiên) trong trình đơn mục bổ sung (ở góc trên cùng bên phải) để gọi mã bạn vừa viết:

95691e9b71ba55e3.png.

Bây giờ, hãy mở giao diện người dùng của Trình mô phỏng bằng cách chuyển đến http://localhost:4000 trên trình duyệt web của bạn. Sau đó, nhấp vào thẻ Firestore và bạn sẽ thấy dữ liệu mình vừa thêm:

Trình mô phỏng xác thực Firebase

Dữ liệu này được lưu trữ 100% trên máy của bạn. Trên thực tế, dự án thực tế của bạn thậm chí chưa có cơ sở dữ liệu Firestore! Điều này có nghĩa là bạn có thể thử nghiệm việc sửa đổi và xoá dữ liệu này mà không dẫn đến kết quả nào.

Xin chúc mừng, bạn vừa ghi dữ liệu vào Firestore! Ở bước tiếp theo, chúng ta sẽ tìm hiểu cách hiển thị dữ liệu này trong ứng dụng.

7. Hiển thị dữ liệu từ Firestore

Trong bước này, chúng ta sẽ tìm hiểu cách truy xuất dữ liệu từ Firestore và hiển thị dữ liệu đó trong ứng dụng. Bước đầu tiên để đọc dữ liệu từ Firestore là tạo Query. Mở tệp MainFragment.kt rồi thêm mã sau vào đầu phương thức onViewCreated():

        // Firestore
        firestore = Firebase.firestore

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

Bây giờ, chúng ta muốn lắng nghe truy vấn để nhận tất cả tài liệu phù hợp và được thông báo về các bản cập nhật trong tương lai theo thời gian thực. Vì mục tiêu cuối cùng của chúng ta là liên kết dữ liệu này với một RecyclerView, nên chúng ta cần tạo một lớp RecyclerView.Adapter để theo dõi dữ liệu.

Mở lớp FirestoreAdapter (đã được triển khai một phần). Trước tiên, hãy để bộ chuyển đổi triển khai EventListener và xác định hàm onEvent để hàm này có thể nhận thông tin cập nhật cho truy vấn 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()
    }
    
    // ...
}

Trong lần tải đầu tiên, trình nghe sẽ nhận được một sự kiện ADDED cho mỗi tài liệu mới. Do tập hợp truy vấn thay đổi theo thời gian, trình nghe sẽ nhận được nhiều sự kiện hơn có chứa thay đổi. Bây giờ, hãy hoàn tất việc triển khai trình nghe. Trước tiên, hãy thêm 3 phương thức mới: onDocumentAdded, onDocumentModifiedonDocumentRemoved:

    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)
    }

Sau đó, hãy gọi các phương thức mới này từ 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()
    }

Cuối cùng, hãy triển khai phương thức startListening() để đính kèm trình nghe:

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

Ứng dụng hiện đã được định cấu hình đầy đủ để đọc dữ liệu từ Firestore. Chạy lại ứng dụng và bạn sẽ thấy các nhà hàng đã thêm ở bước trước:

9e45f40faefce5d0.png.

Bây giờ, hãy quay lại giao diện người dùng của Trình mô phỏng trong trình duyệt và chỉnh sửa một trong các tên nhà hàng. Bạn sẽ thấy tính năng này thay đổi trong ứng dụng gần như ngay lập tức!

8. Sắp xếp và lọc dữ liệu

Ứng dụng hiện đang hiển thị các nhà hàng được xếp hạng cao nhất trên toàn bộ bộ sưu tập, nhưng trong một ứng dụng nhà hàng thực tế, người dùng lại muốn sắp xếp và lọc dữ liệu. Ví dụ: ứng dụng có thể hiển thị "Nhà hàng hải sản hàng đầu ở Philippines" hoặc "Pizza rẻ nhất".

Khi nhấp vào thanh màu trắng ở đầu ứng dụng, một hộp thoại bộ lọc sẽ xuất hiện. Trong phần này, chúng ta sẽ sử dụng các truy vấn trên Firestore để hộp thoại này hoạt động:

67898572a35672a5.pngS

Hãy chỉnh sửa phương thức onFilter() của MainFragment.kt. Phương thức này chấp nhận đối tượng Filters. Đây là đối tượng trợ giúp mà chúng ta đã tạo để thu thập kết quả của hộp thoại bộ lọc. Chúng ta sẽ thay đổi phương thức này để tạo một truy vấn từ các bộ lọc:

    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
    }

Trong đoạn mã trên, chúng ta tạo đối tượng Query bằng cách đính kèm mệnh đề whereorderBy để khớp với các bộ lọc đã cho.

Chạy lại ứng dụng rồi chọn bộ lọc sau để xem các nhà hàng giá rẻ phổ biến nhất:

7a67a8a400c80c50.pngS

Bây giờ, bạn sẽ thấy danh sách các nhà hàng đã lọc chỉ chứa các lựa chọn giá thấp:

a670188398c3c59.png.

Nếu đã đạt đến mục tiêu này thì bạn đã xây dựng được một ứng dụng xem đề xuất về nhà hàng với đầy đủ chức năng trên Firestore! Giờ đây, bạn có thể sắp xếp và lọc các nhà hàng theo thời gian thực. Trong vài phần tiếp theo, chúng ta sẽ thêm bài đánh giá về nhà hàng và thêm quy tắc bảo mật vào ứng dụng.

9. Sắp xếp dữ liệu trong các tập hợp con

Trong phần này, chúng ta sẽ thêm điểm xếp hạng vào ứng dụng để người dùng có thể đánh giá nhà hàng họ yêu thích (hoặc ít yêu thích nhất).

Bộ sưu tập và bộ sưu tập con

Cho đến nay, chúng tôi đã lưu trữ tất cả dữ liệu nhà hàng trong một tập hợp cấp cao nhất có tên là "nhà hàng". Khi người dùng xếp hạng một nhà hàng, chúng ta muốn thêm đối tượng Rating mới vào các nhà hàng đó. Đối với nhiệm vụ này, chúng ta sẽ sử dụng một bộ sưu tập con. Bạn có thể xem tập hợp con là một tập hợp được đính kèm vào một tài liệu. Vì vậy, mỗi tài liệu về nhà hàng sẽ có một tập hợp con về điểm xếp hạng chứa đầy đủ các tài liệu về điểm xếp hạng. Các bộ sưu tập phụ giúp sắp xếp dữ liệu mà không làm to tài liệu hoặc đòi hỏi các truy vấn phức tạp.

Để truy cập vào một tập hợp con, hãy gọi .collection() trên tài liệu gốc:

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

Bạn có thể truy cập và truy vấn một tập hợp con giống như tập hợp cấp cao nhất, không có giới hạn về kích thước hay thay đổi về hiệu suất. Bạn có thể đọc thêm về mô hình dữ liệu Firestore tại đây.

Ghi dữ liệu trong giao dịch

Việc thêm Rating vào tập hợp con thích hợp chỉ yêu cầu gọi .add(), nhưng chúng ta cũng cần cập nhật điểm xếp hạng trung bình và số lượng điểm xếp hạng của đối tượng Restaurant để phản ánh dữ liệu mới. Nếu chúng ta sử dụng các thao tác riêng biệt để thực hiện hai thay đổi này, thì một số điều kiện tương tranh có thể dẫn đến dữ liệu cũ hoặc không chính xác.

Để đảm bảo thêm điểm xếp hạng đúng cách, chúng tôi sẽ sử dụng giao dịch để thêm điểm xếp hạng cho nhà hàng. Giao dịch này sẽ thực hiện một số hành động:

  • Đọc điểm xếp hạng hiện tại của nhà hàng và tính điểm xếp hạng mới
  • Thêm điểm xếp hạng vào bộ sưu tập con
  • Cập nhật điểm xếp hạng trung bình và số lượng điểm xếp hạng của nhà hàng

Mở RestaurantDetailFragment.kt và triển khai hàm 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
        }
    }

Hàm addRating() trả về một Task đại diện cho toàn bộ giao dịch. Trong hàm onRating(), trình nghe được thêm vào tác vụ để phản hồi kết quả của giao dịch.

Bây giờ, hãy Chạy lại ứng dụng rồi nhấp vào một trong các nhà hàng. Thao tác này sẽ làm xuất hiện màn hình thông tin chi tiết về nhà hàng. Nhấp vào nút + để bắt đầu thêm bài đánh giá. Thêm bài đánh giá bằng cách chọn một số sao và nhập nội dung nào đó.

78fa16cdf8ef435a.png.

Thao tác nhấn Gửi sẽ bắt đầu giao dịch. Khi giao dịch hoàn tất, bạn sẽ thấy bài đánh giá của mình hiển thị bên dưới và thông tin cập nhật về số bài đánh giá của nhà hàng:

f9e670f40bd615b0.pngs

Xin chúc mừng! Bạn hiện đã có một ứng dụng đánh giá nhà hàng địa phương trên mạng xã hội trên thiết bị di động được xây dựng trên Cloud Firestore. Tôi nghe nói những trò chơi đó rất phổ biến hiện nay.

10. Bảo mật dữ liệu của bạn

Cho đến nay, chúng tôi chưa xem xét tính bảo mật của ứng dụng này. Làm cách nào để chúng ta biết rằng người dùng chỉ có thể đọc và ghi đúng dữ liệu của riêng họ? Cơ sở dữ liệu Firestore được bảo mật bằng một tệp cấu hình có tên là Security Rules (Quy tắc bảo mật).

Mở tệp firestore.rules, bạn sẽ thấy như sau:

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;
    }
  }
}

Hãy thay đổi các quy tắc này để ngăn chặn các quyền truy cập hoặc thay đổi dữ liệu không mong muốn, hãy mở tệp firestore.rules và thay thế nội dung bằng:

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;
      }
    }
  }
}

Các quy tắc này hạn chế quyền truy cập để đảm bảo rằng khách hàng chỉ thực hiện các thay đổi an toàn. Ví dụ: nội dung cập nhật cho tài liệu về nhà hàng chỉ có thể thay đổi điểm xếp hạng, chứ không thể thay đổi tên hay bất kỳ dữ liệu không thể thay đổi nào khác. Hệ thống chỉ có thể tạo mức phân loại nếu mã nhận dạng người dùng khớp với người dùng đã đăng nhập. Điều này giúp ngăn chặn hành vi giả mạo.

Để đọc thêm về Quy tắc bảo mật, hãy truy cập vào tài liệu này.

11. Kết luận

Giờ đây, bạn đã tạo một ứng dụng có đầy đủ tính năng trên Firestore. Bạn đã tìm hiểu về các tính năng quan trọng nhất của Firestore, bao gồm:

  • Tài liệu và bộ sưu tập
  • Đọc và ghi dữ liệu
  • Sắp xếp và lọc bằng truy vấn
  • Bộ sưu tập phụ
  • Giao dịch

Tìm hiểu thêm

Để tiếp tục tìm hiểu về Firestore, bạn có thể bắt đầu bằng một số nội dung dưới đây:

Ứng dụng nhà hàng trong lớp học lập trình này dựa trên chủ đề "Consumer Eats" ứng dụng mẫu. Bạn có thể duyệt qua mã nguồn cho ứng dụng đó tại đây.

Không bắt buộc: Triển khai vào phiên bản chính thức

Cho đến nay, ứng dụng này chỉ sử dụng Bộ mô phỏng Firebase. Nếu bạn muốn tìm hiểu cách triển khai ứng dụng này cho một dự án Firebase thực tế, hãy chuyển sang bước tiếp theo.

12. (Không bắt buộc) Triển khai ứng dụng

Cho đến nay, ứng dụng này hoàn toàn là dữ liệu cục bộ, tất cả dữ liệu đều nằm trong Bộ mô phỏng Firebase. Trong phần này, bạn sẽ tìm hiểu cách định cấu hình dự án Firebase để ứng dụng này có thể hoạt động chính thức.

Xác thực Firebase

Trong bảng điều khiển của Firebase, hãy chuyển đến phần Xác thực rồi nhấp vào Bắt đầu. Chuyển đến thẻ Phương thức đăng nhập rồi chọn tuỳ chọn Email/Mật khẩu từ Nhà cung cấp gốc.

Bật phương thức đăng nhập Email/Mật khẩu rồi nhấp vào Lưu.

nhà-cung-cấp-đăng-nhập.png

Firestore

Tạo cơ sở dữ liệu

Chuyển đến phần Firestore Database (Cơ sở dữ liệu khôi phục) trên bảng điều khiển và nhấp vào Create Database (Tạo cơ sở dữ liệu):

  1. Khi có thông báo nhắc về Quy tắc bảo mật chọn bắt đầu ở Chế độ phát hành công khai, chúng tôi sẽ sớm cập nhật các quy tắc đó.
  2. Chọn vị trí cơ sở dữ liệu mà bạn muốn dùng cho ứng dụng. Xin lưu ý rằng việc chọn vị trí cơ sở dữ liệu là quyết định vĩnh viễn và để thay đổi vị trí này, bạn sẽ phải tạo một dự án mới. Để biết thêm thông tin về cách chọn vị trí cho dự án, hãy xem tài liệu này.

Triển khai quy tắc

Để triển khai Quy tắc bảo mật mà bạn đã viết trước đó, hãy chạy lệnh sau trong thư mục của lớp học lập trình:

$ firebase deploy --only firestore:rules

Thao tác này sẽ triển khai nội dung của firestore.rules cho dự án của bạn. Bạn có thể xác nhận nội dung này bằng cách chuyển đến thẻ Quy tắc trong bảng điều khiển.

Triển khai chỉ mục

Ứng dụng BodyEats có tính năng sắp xếp và lọc phức tạp, đòi hỏi một số chỉ mục phức hợp tuỳ chỉnh. Bạn có thể tạo các định nghĩa này theo cách thủ công trong bảng điều khiển của Firebase, nhưng việc viết các định nghĩa trong tệp firestore.indexes.json và triển khai sẽ đơn giản hơn bằng cách sử dụng Giao diện dòng lệnh (CLI) của Firebase.

Nếu mở tệp firestore.indexes.json, bạn sẽ thấy các chỉ mục bắt buộc đã được cung cấp:

{
  "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": []
}

Để triển khai các chỉ mục này, hãy chạy lệnh sau:

$ firebase deploy --only firestore:indexes

Xin lưu ý rằng quá trình tạo chỉ mục sẽ không diễn ra ngay lập tức, bạn có thể theo dõi tiến trình trong bảng điều khiển của Firebase.

Định cấu hình ứng dụng

Trong các tệp util/FirestoreInitializer.ktutil/AuthInitializer.kt, chúng ta đã định cấu hình Firebase SDK để kết nối với trình mô phỏng khi ở chế độ gỡ lỗi:

    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
    }

Nếu muốn kiểm thử ứng dụng của mình với dự án Firebase thực tế, bạn có thể:

  1. Tạo ứng dụng ở chế độ phát hành và chạy ứng dụng trên thiết bị.
  2. Tạm thời thay thế BuildConfig.DEBUG bằng false và chạy lại ứng dụng.

Xin lưu ý rằng có thể bạn cần phải Đăng xuất khỏi ứng dụng rồi đăng nhập lại để kết nối đúng cách với kênh phát hành công khai.