Làm quen với Places SDK for Android (Kotlin)

1. Trước khi bắt đầu

Lớp học lập trình này hướng dẫn bạn cách tích hợp Places SDK for Android với ứng dụng của mình và sử dụng từng tính năng của Places SDK.

Ứng dụng minh hoạ Places

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

  • Có kiến thức cơ bản về Kotlin và phát triển Android

Kiến thức bạn sẽ học được

  • Cách cài đặt Places SDK for Android bằng Tiện ích Kotlin.
  • Cách tải Thông tin chi tiết về địa điểm cho một địa điểm cụ thể.
  • Cách thêm tiện ích Tự động hoàn thành địa điểm vào ứng dụng.
  • Cách tải Địa điểm hiện tại dựa trên vị trí hiện được báo cáo của thiết bị.

Bạn cần có

Để hoàn tất lớp học lập trình này, bạn cần có các tài khoản, dịch vụ và công cụ sau:

2. Bắt đầu thiết lập

Đối với bước bật bên dưới, hãy bật Places APIMaps SDK for Android.

Thiết lập Nền tảng Google Maps

Nếu bạn chưa có tài khoản Google Cloud Platform và dự án có bật tính năng thanh toán, vui lòng xem hướng dẫn Bắt đầu sử dụng Google Maps Platform để tạo tài khoản thanh toán và dự án.

  1. Trong Cloud Console, hãy nhấp vào trình đơn thả xuống dự án rồi chọn dự án mà bạn muốn sử dụng cho lớp học lập trình này.

  1. Bật các API và SDK của Google Maps Platform cần thiết cho lớp học lập trình này trong Google Cloud Marketplace. Để làm như vậy, hãy làm theo các bước trong video này hoặc tài liệu này.
  2. Tạo khoá API trong trang Thông tin xác thực của Cloud Console. Bạn có thể làm theo các bước trong video này hoặc tài liệu này. Tất cả các yêu cầu gửi đến Nền tảng Google Maps đều cần có khoá API.

3. Bắt đầu nhanh

Để bạn có thể bắt đầu nhanh chóng, hãy tải mã khởi đầu xuống để giúp bạn theo dõi lớp học lập trình này. Bạn có thể chuyển ngay đến giải pháp, nhưng nếu muốn làm theo tất cả các bước để tự xây dựng, hãy tiếp tục đọc.

  1. Sao chép kho lưu trữ nếu bạn đã cài đặt git.
git clone https://github.com/googlemaps/codelab-places-101-android-kotlin.git

Ngoài ra, bạn có thể nhấp vào nút này để tải mã nguồn xuống.

  1. Sau khi tải mã xuống, hãy mở dự án trong thư mục /starter trong Android Studio. Dự án này bao gồm cấu trúc tệp cơ bản mà bạn cần để hoàn tất lớp học lập trình. Mọi thứ bạn cần để làm việc đều nằm trong thư mục /starter.

Nếu muốn xem mã giải pháp đầy đủ đang chạy, bạn có thể xem mã hoàn chỉnh trong thư mục /solution.

4. Thêm khoá API vào dự án

Phần này mô tả cách lưu trữ khoá API để ứng dụng của bạn có thể tham chiếu một cách an toàn. Bạn không nên kiểm tra khoá API trong hệ thống kiểm soát phiên bản, vì vậy, bạn nên lưu trữ khoá này trong tệp secrets.properties. Tệp này sẽ được đặt trong bản sao cục bộ của thư mục gốc của dự án. Để biết thêm thông tin về tệp secrets.properties, hãy xem phần Tệp thuộc tính Gradle.

Để đơn giản hoá tác vụ này, bạn nên sử dụng Trình bổ trợ Secrets Gradle cho Android.

Cách cài đặt Trình bổ trợ Secrets Gradle cho Android trong dự án Google Maps:

  1. Trong Android Studio, hãy mở tệp build.gradle.kts hoặc build.gradle cấp cao nhất rồi thêm mã sau vào phần tử dependencies trong buildscript.

Nếu bạn sử dụng build.gradle.kts, hãy thêm:

buildscript {
    dependencies {
        classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1")
    }
}

Nếu bạn sử dụng build.gradle, hãy thêm:

buildscript {
    dependencies {
        classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1"
    }
}
  1. Mở tệp build.gradle.kts hoặc build.gradle ở cấp mô-đun rồi thêm đoạn mã sau vào phần tử plugins.

Nếu bạn sử dụng build.gradle.kts, hãy thêm:

plugins {
    // ...
    id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
}

Nếu bạn sử dụng build.gradle, hãy thêm:

plugins {
    // ...
    id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
}
  1. Trong tệp build.gradle.kts hoặc build.gradle ở cấp mô-đun, hãy đảm bảo rằng targetSdkcompileSdk được đặt thành 34.
  2. Lưu tệp và đồng bộ hoá dự án với Gradle.
  3. Mở tệp secrets.properties trong thư mục cấp cao nhất, sau đó thêm đoạn mã sau. Thay thế YOUR_API_KEY bằng khoá API của bạn. Lưu trữ khoá của bạn trong tệp này vì secrets.properties không được đưa vào hệ thống quản lý phiên bản.
PLACES_API_KEY=YOUR_API_KEY
  1. Lưu tệp.
  2. Tạo tệp local.defaults.properties trong thư mục cấp cao nhất (cùng thư mục với tệp secrets.properties), rồi thêm mã sau.
PLACES_API_KEY=DEFAULT_API_KEY

Mục đích của tệp này là cung cấp một vị trí dự phòng cho khoá API nếu không tìm thấy tệp secrets.properties để các bản dựng không bị lỗi. Điều này có thể xảy ra nếu bạn sao chép ứng dụng từ một hệ thống kiểm soát phiên bản bỏ qua secrets.properties và bạn chưa tạo tệp secrets.properties cục bộ để cung cấp khoá API.

  1. Lưu tệp.
  2. Trong Android Studio, hãy mở tệp build.gradle.kts hoặc build.gradle ở cấp mô-đun rồi chỉnh sửa thuộc tính secrets. Nếu thuộc tính secrets không tồn tại, hãy thêm thuộc tính đó.

Chỉnh sửa các thuộc tính của trình bổ trợ để đặt propertiesFileName thành secrets.properties, đặt defaultPropertiesFileName thành local.defaults.properties và đặt mọi thuộc tính khác.

secrets {
    // Optionally specify a different file name containing your secrets.
    // The plugin defaults to "local.properties"
    propertiesFileName = "secrets.properties"

    // A properties file containing default secret values. This file can be
    // checked in version control.
    defaultPropertiesFileName = "local.defaults.properties"
}

5. Cài đặt Places SDK for Android

Trong phần này, bạn sẽ thêm Places SDK for Android vào các phần phụ thuộc của ứng dụng.

  1. Giờ đây, khi khoá API của bạn có thể truy cập được bên trong ứng dụng, hãy thêm phần phụ thuộc Places SDK for Android vào tệp build.gradle của ứng dụng.

Sửa đổi tệp build.gradle ở cấp ứng dụng để thêm phần phụ thuộc cho Places SDK cho Android:

build.gradle ở cấp ứng dụng

dependencies {
   // Dependency to include Places SDK for Android
   implementation 'com.google.android.libraries.places:places:3.4.0'
}
  1. Chạy ứng dụng.

Lúc này, bạn sẽ thấy một ứng dụng có màn hình trống. Tiếp tục điền thông tin vào màn hình này bằng 3 bản minh hoạ.

6. Cài đặt Places Android KTX

Đối với các ứng dụng Kotlin sử dụng một hoặc nhiều SDK Android của Nền tảng Google Maps, các thư viện tiện ích Kotlin (KTX) cho phép bạn tận dụng các tính năng của ngôn ngữ Kotlin, chẳng hạn như coroutine, thuộc tính/hàm mở rộng và nhiều tính năng khác. Mỗi Google Maps SDK đều có một thư viện KTX tương ứng như minh hoạ bên dưới:

Sơ đồ KTX của Nền tảng Google Maps

Trong nhiệm vụ này, hãy sử dụng thư viện Places Android KTX để dùng các tính năng ngôn ngữ dành riêng cho Kotlin trong ứng dụng của bạn.

Thêm phần phụ thuộc Places Android KTX

Để tận dụng các tính năng dành riêng cho Kotlin, hãy thêm thư viện KTX tương ứng cho SDK này vào tệp build.gradle ở cấp ứng dụng.

build.gradle

dependencies {
    // ...

    // Places SDK for Android KTX Library
    implementation 'com.google.maps.android:places-ktx:3.1.1'
}

7. Khởi chạy Places Client

Khởi chạy Places SDK cho phạm vi ứng dụng

Trong tệp DemoApplication.kt của thư mục app/src/main/java/com/google/codelabs/maps/placesdemo, hãy khởi chạy Places SDK for Android. Dán các dòng bên dưới vào cuối hàm onCreate:

        // Initialize the SDK with the Google Maps Platform API key
        Places.initialize(this, BuildConfig.PLACES_API_KEY)

Khi bạn tạo ứng dụng, Trình bổ trợ Secrets Gradle cho Android sẽ cung cấp khoá API trong tệp secrets.properties dưới dạng BuildConfig.PLACES_API_KEY.

Thêm tệp ứng dụng vào tệp kê khai

Vì bạn đã mở rộng Application bằng DemoApplication, nên bạn cần cập nhật tệp kê khai. Thêm thuộc tính android:name vào phần tử application trong tệp AndroidManifest.xml, nằm trong app/src/main:

    <application
        android:name=".DemoApplication"
        ...
    </application>

Mã này trỏ tệp kê khai ứng dụng đến lớp DemoApplication trong thư mục src/main/java/com/google/codelabs/maps/placesdemo/.

8. Tìm nạp thông tin chi tiết về địa điểm

Tạo màn hình Chi tiết

Bố cục activity_details.xmlLinearLayout trống có trong thư mục app/src/main/res/layout/. Điền vào bố cục tuyến tính bằng cách thêm mã sau đây giữa các dấu ngoặc <LinearLayout>.

    <com.google.android.material.textfield.TextInputLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/details_input"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/details_input_hint"
            android:text="@string/details_input_default" />

    </com.google.android.material.textfield.TextInputLayout>

    <Button
        android:id="@+id/details_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/button_details" />

    <TextView
        android:id="@+id/details_response_content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingTop="16dp"
        android:textIsSelectable="true" />

Đoạn mã này thêm một trường nhập văn bản để người dùng có thể nhập Mã địa điểm bất kỳ hoặc sử dụng giá trị mặc định được cung cấp, một nút để bắt đầu yêu cầu Chi tiết về địa điểm và một TextView để hiển thị thông tin từ phản hồi. Các chuỗi được liên kết sẽ được xác định cho bạn trong tệp src/main/res/values/strings.xml.

Tạo một hoạt động Chi tiết

  1. Tạo một tệp DetailsActivity.kt trong thư mục src/main/java/com/google/codelabs/maps/placesdemo/ và liên kết tệp đó với bố cục bạn vừa tạo. Dán mã này vào tệp:
@ExperimentalCoroutinesApi
class DetailsActivity : AppCompatActivity() {
    private lateinit var placesClient: PlacesClient
    private lateinit var detailsButton: Button
    private lateinit var detailsInput: TextInputEditText
    private lateinit var responseView: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_details)

        // Set up view objects
        detailsInput = findViewById(R.id.details_input)
        detailsButton = findViewById(R.id.details_button)
        responseView = findViewById(R.id.details_response_content)

        val apiKey = BuildConfig.PLACES_API_KEY

        // Log an error if apiKey is not set.
        if (apiKey.isEmpty() || apiKey == "DEFAULT_API_KEY") {
            Log.e(TAG, "No api key")
            finish()
            return
        }
    }
}
  1. Tạo một Places Client để sử dụng với hoạt động này. Dán mã này sau mã để kiểm tra khoá API trong hàm onCreate.
        // Retrieve a PlacesClient (previously initialized - see DemoApplication)
        placesClient = Places.createClient(this)
  1. Sau khi thiết lập Places Client, hãy đính kèm một trình nghe lượt nhấp vào nút. Dán mã này sau khi tạo Places Client trong hàm onCreate.
        // Upon button click, fetch and display the Place Details
        detailsButton.setOnClickListener { button ->
            button.isEnabled = false
            val placeId = detailsInput.text.toString()
            val placeFields = listOf(
                Place.Field.NAME,
                Place.Field.ID,
                Place.Field.LAT_LNG,
                Place.Field.ADDRESS
            )
            lifecycleScope.launch {
                try {
                    val response = placesClient.awaitFetchPlace(placeId, placeFields)
                    responseView.text = response.prettyPrint()
                } catch (e: Exception) {
                    e.printStackTrace()
                    responseView.text = e.message
                }
                button.isEnabled = true
            }
        }

Mã này truy xuất Mã địa điểm đã được nhập vào trường nhập, xác định những trường cần yêu cầu cho địa điểm, tạo FetchPlaceRequest, bắt đầu tác vụ và theo dõi xem có thành công hay không. Nếu yêu cầu thành công, hàm sẽ điền thông tin chi tiết được yêu cầu vào TextView.

Thêm hoạt động Chi tiết vào tệp kê khai

Thêm phần tử <activity> cho DetailsActivity dưới dạng phần tử con của phần tử <application> trong tệp AndroidManifest.xml, nằm trong app/src/main:

        <activity android:name=".DetailsActivity" android:label="@string/details_demo_title" />

Thêm hoạt động Chi tiết vào trình đơn minh hoạ

Một mô-đun Demo trống được cung cấp để liệt kê các bản minh hoạ có sẵn trên màn hình chính. Sau khi tạo một hoạt động Chi tiết về địa điểm, hãy thêm hoạt động đó vào tệp Demo.kt trong thư mục src/main/java/com/google/codelabs/maps/placesdemo/ bằng mã sau:

    DETAILS_FRAGMENT_DEMO(
        R.string.details_demo_title,
        R.string.details_demo_description,
        DetailsActivity::class.java
    ),

Các chuỗi liên kết được xác định trong tệp src/main/res/values/strings.xml.

Kiểm tra MainActivity.kt và quan sát thấy rằng nó tạo ra một ListView được điền bằng cách lặp lại nội dung của mô-đun Demo. Nếu người dùng nhấn vào một mục trong danh sách, trình nghe sự kiện nhấp chuột sẽ mở hoạt động được liên kết.

Chạy ứng dụng

  1. Chạy ứng dụng. Lần này, bạn sẽ thấy một mục trong danh sách trình bày bản minh hoạ Chi tiết về địa điểm.
  2. Nhấn vào văn bản Chi tiết về địa điểm. Bạn sẽ thấy khung hiển thị mà bạn đã tạo cùng với một trường nhập liệu và nút.
  3. Nhấn vào nút "XEM CHI TIẾT". Nếu đã sử dụng Place ID mặc định, bạn sẽ thấy tên địa điểm, địa chỉ và toạ độ trên bản đồ xuất hiện, như minh hoạ trong Hình 1.

Hoạt động Chi tiết về địa điểm có phản hồi

Hình 1. Hoạt động Chi tiết về địa điểm có phản hồi hiển thị.

9. Thêm tính năng Place Autocomplete

Tạo màn hình Tự động hoàn thành

Bố cục activity_autocomplete.xmlLinearLayout trống được cung cấp trong thư mục app/src/main/res/layout/. Điền vào bố cục tuyến tính bằng cách thêm mã này vào giữa dấu ngoặc <LinearLayout>.

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/autocomplete_fragment"
        android:background="@android:color/white"
        android:name="com.google.android.libraries.places.widget.AutocompleteSupportFragment"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/autocomplete_response_content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingTop="16dp"
        android:textIsSelectable="true" />

Đoạn mã này thêm một tiện ích AutocompleteSupportFragment và một TextView để hiển thị thông tin từ phản hồi. Các chuỗi liên kết được xác định trong tệp src/main/res/values/strings.xml.

Tạo một hoạt động Tự động hoàn thành

  1. Tạo tệp AutocompleteActivity.kt trong thư mục src/main/java/com/google/codelabs/maps/placesdemo/ rồi xác định tệp đó bằng đoạn mã sau:
@ExperimentalCoroutinesApi
class AutocompleteActivity : AppCompatActivity() {
    private lateinit var responseView: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_autocomplete)

        // Set up view objects
        responseView = findViewById(R.id.autocomplete_response_content)
        val autocompleteFragment =
            supportFragmentManager.findFragmentById(R.id.autocomplete_fragment)
                    as AutocompleteSupportFragment
    }
}

Mã này liên kết hoạt động với các khung hiển thị và AutocompleteSupportFramgent mà bạn đã xác định trong tệp bố cục.

  1. Tiếp theo, hãy xác định điều gì sẽ xảy ra khi người dùng chọn một trong các kết quả dự đoán do tính năng Tự động hoàn thành địa điểm cung cấp. Thêm mã này vào cuối hàm onCreate:
        val placeFields: List<Place.Field> =
            listOf(Place.Field.NAME, Place.Field.ID, Place.Field.ADDRESS, Place.Field.LAT_LNG)
        autocompleteFragment.setPlaceFields(placeFields)

        // Listen to place selection events
        lifecycleScope.launchWhenCreated {
            autocompleteFragment.placeSelectionEvents().collect { event ->
                when (event) {
                    is PlaceSelectionSuccess -> {
                        val place = event.place
                        responseView.text = prettyPrintAutocompleteWidget(place, false)
                    }

                    is PlaceSelectionError -> Toast.makeText(
                        this@AutocompleteActivity,
                        "Failed to get place '${event.status.statusMessage}'",
                        Toast.LENGTH_SHORT
                    ).show()
                }
            }
        }

Mã này xác định những trường cần yêu cầu cho địa điểm, theo dõi sự kiện chọn địa điểm và theo dõi trạng thái thành công hoặc thất bại. Nếu yêu cầu thành công, hàm sẽ điền thông tin chi tiết về địa điểm vào TextView. Xin lưu ý rằng tính năng Tự động hoàn thành địa điểm trả về một đối tượng Địa điểm; bạn không cần đưa ra một yêu cầu riêng về Chi tiết địa điểm khi sử dụng tiện ích Tự động hoàn thành địa điểm.

Thêm hoạt động Tự động hoàn thành vào tệp kê khai

Thêm phần tử <activity> cho AutocompleteActivity dưới dạng phần tử con của phần tử <application> trong tệp AndroidManifest.xml, nằm trong app/src/main:

        <activity android:name=".AutocompleteActivity" android:label="@string/autocomplete_fragment_demo_title" />

Thêm hoạt động Tự động hoàn thành vào trình đơn minh hoạ

Giống như trước đây, hãy thêm bản minh hoạ tính năng Tự động hoàn thành địa điểm vào màn hình chính bằng cách thêm bản minh hoạ đó vào danh sách trong mô-đun Demo. Giờ đây, bạn đã tạo một hoạt động Tự động hoàn thành địa điểm, hãy thêm hoạt động đó vào tệp Demo.kt trong thư mục src/main/java/com/google/codelabs/maps/placesdemo/. Dán đoạn mã này ngay sau mục DETAILS_FRAGMENT_DEMO:

    AUTOCOMPLETE_FRAGMENT_DEMO(
        R.string.autocomplete_fragment_demo_title,
        R.string.autocomplete_fragment_demo_description,
        AutocompleteActivity::class.java
    ),

Các chuỗi liên kết được xác định trong tệp src/main/res/values/strings.xml.

Chạy ứng dụng

  1. Chạy ứng dụng. Lần này, bạn sẽ thấy 2 mục trong danh sách trên màn hình chính.
  2. Nhấn vào hàng Place Autocomplete (Tự động hoàn thành địa điểm). Bạn sẽ thấy một cửa sổ bật lên đầu vào của tính năng Tự động hoàn thành địa điểm như trong Hình 2.
  3. Bắt đầu nhập tên của một địa điểm. Đây có thể là tên cơ sở, địa chỉ hoặc khu vực địa lý. Các từ dự đoán sẽ xuất hiện khi bạn nhập.
  4. Chọn một trong các cụm từ dự đoán. Các đề xuất sẽ biến mất và TextView hiện sẽ hiển thị thông tin chi tiết về địa điểm đã chọn như trong Hình 3.

Hoạt động tự động hoàn thành sau khi người dùng nhấn vào trường nhập

Hình 2. Hoạt động tự động hoàn thành sau khi người dùng nhấn vào trường nhập.

Tự động hoàn thành hoạt động sau khi người dùng nhập và chọn &quot;Thác Niagara&quot;

Hình 3. Hoạt động tự động hoàn thành hiển thị Chi tiết về địa điểm sau khi người dùng nhập và chọn "Thác Niagara".

10. Nhận Địa điểm hiện tại của thiết bị

Tạo màn hình Địa điểm hiện tại

Bố cục activity_current.xmlLinearLayout trống đã được cung cấp trong thư mục app/src/main/res/layout/. Điền vào bố cục tuyến tính bằng cách thêm mã sau vào giữa các dấu ngoặc <LinearLayout>.

    <Button
        android:id="@+id/current_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/current_button" />

    <TextView
        android:id="@+id/current_response_content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingTop="16dp"
        android:scrollbars = "vertical"
        android:textIsSelectable="true" />

Tạo một hoạt động Địa điểm hiện tại

  1. Tạo tệp CurrentPlaceActivity.kt trong thư mục src/main/java/com/google/codelabs/maps/placesdemo/ rồi xác định tệp đó bằng mã sau:
@ExperimentalCoroutinesApi
class CurrentPlaceActivity : AppCompatActivity(), OnMapReadyCallback {
    private lateinit var placesClient: PlacesClient
    private lateinit var currentButton: Button
    private lateinit var responseView: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_current)

        // Retrieve a PlacesClient (previously initialized - see DemoApplication)
        placesClient = Places.createClient(this)

        // Set view objects
        currentButton = findViewById(R.id.current_button)
        responseView = findViewById(R.id.current_response_content)

        // Set listener for initiating Current Place
        currentButton.setOnClickListener {
            checkPermissionThenFindCurrentPlace()
        }
    }
}

Đoạn mã này liên kết hoạt động với các khung hiển thị mà bạn đã xác định trong tệp bố cục. Thao tác này cũng thêm một trình nghe lượt nhấp vào nút để gọi hàm checkPermissionThenFindCurrentPlace khi người dùng nhấp vào nút.

  1. Xác định checkPermissionThenFindCurrentPlace() để kiểm tra quyền truy cập thông tin vị trí chính xác và yêu cầu cấp quyền nếu quyền này chưa được cấp. Dán mã này sau hàm onCreate.
    /**
     * Checks that the user has granted permission for fine or coarse location.
     * If granted, finds current Place.
     * If not yet granted, launches the permission request.
     * See https://developer.android.com/training/permissions/requesting
     */
    private fun checkPermissionThenFindCurrentPlace() {
        when {
            (ContextCompat.checkSelfPermission(
                this,
                ACCESS_FINE_LOCATION
            ) == PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(
                this,
                ACCESS_COARSE_LOCATION
            ) == PackageManager.PERMISSION_GRANTED) -> {
                // You can use the API that requires the permission.
                findCurrentPlace()
            }

            shouldShowRequestPermissionRationale(ACCESS_FINE_LOCATION)
            -> {
                Log.d(TAG, "Showing permission rationale dialog")
                // TODO: In an educational UI, explain to the user why your app requires this
                // permission for a specific feature to behave as expected. In this UI,
                // include a "cancel" or "no thanks" button that allows the user to
                // continue using your app without granting the permission.
            }

            else -> {
                // Ask for both the ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION permissions.
                ActivityCompat.requestPermissions(
                    this,
                    arrayOf(
                        ACCESS_FINE_LOCATION,
                        ACCESS_COARSE_LOCATION
                    ),
                    PERMISSION_REQUEST_CODE
                )
            }
        }
    }

    companion object {
        private const val TAG = "CurrentPlaceActivity"
        private const val PERMISSION_REQUEST_CODE = 9
    }
  1. Khi nhánh else của hàm checkPermissionThenFindCurrentPlace gọi requestPermissions, ứng dụng sẽ hiển thị hộp thoại yêu cầu cấp quyền cho người dùng. Nếu đang dùng thiết bị chạy hệ điều hành thấp hơn Android 12, thì người dùng chỉ có thể cấp quyền truy cập vị trí chính xác (chi tiết). Nếu đang dùng thiết bị chạy Android 12 trở lên, người dùng sẽ có lựa chọn cung cấp vị trí ước chừng (không chính xác) thay vì vị trí chính xác, như minh hoạ trong Hình 4.

Yêu cầu quyền của người dùng trên thiết bị chạy Android 12 trở lên

Hình 4. Khi yêu cầu người dùng cấp quyền trên thiết bị chạy Android 12 trở lên, bạn có thể chọn cấp quyền truy cập vào vị trí chính xác hoặc vị trí ước chừng.

Sau khi người dùng phản hồi hộp thoại cấp quyền của hệ thống, hệ thống sẽ gọi chế độ triển khai onRequestPermissionsResult của ứng dụng. Hệ thống sẽ chuyển phản hồi người dùng tới hộp thoại cấp quyền, cũng như mã yêu cầu mà bạn đã xác định. Ghi đè onRequestPermissionResult để xử lý mã yêu cầu cho quyền truy cập thông tin vị trí liên quan đến hoạt động Current Place này bằng cách dán mã sau đây bên dưới checkPermissionThenFindCurrentPlace.

    @SuppressLint("MissingPermission")
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>, grantResults: IntArray
    ) {
        if (requestCode != PERMISSION_REQUEST_CODE) {
            super.onRequestPermissionsResult(
                requestCode,
                permissions,
                grantResults
            )
            return
        } else if (
            permissions.toList().zip(grantResults.toList())
                .firstOrNull { (permission, grantResult) ->
                    grantResult == PackageManager.PERMISSION_GRANTED && (permission == ACCESS_FINE_LOCATION || permission == ACCESS_COARSE_LOCATION)
                } != null
        )
        // At least one location permission has been granted, so proceed with Find Current Place
        findCurrentPlace()
    }
  1. Sau khi được cấp quyền, hàm findCurrentPlace sẽ chạy. Xác định hàm bằng mã này sau hàm onRequestPermissionsResult.
    /**
     * Fetches a list of [PlaceLikelihood] instances that represent the Places the user is
     * most likely to be at currently.
     */
    @RequiresPermission(anyOf = [ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION])
    private fun findCurrentPlace() {
        // Use fields to define the data types to return.
        val placeFields: List<Place.Field> =
            listOf(Place.Field.NAME, Place.Field.ID, Place.Field.ADDRESS, Place.Field.LAT_LNG)

        // Call findCurrentPlace and handle the response (first check that the user has granted permission).
        if (ContextCompat.checkSelfPermission(this, ACCESS_FINE_LOCATION) ==
            PackageManager.PERMISSION_GRANTED ||
            ContextCompat.checkSelfPermission(this, ACCESS_COARSE_LOCATION) ==
            PackageManager.PERMISSION_GRANTED
        ) {
            // Retrieve likely places based on the device's current location
            currentButton.isEnabled = false
            lifecycleScope.launch {
                val response = placesClient.awaitFindCurrentPlace(placeFields)

                responseView.text = response.prettyPrint()

                // Enable scrolling on the long list of likely places
                val movementMethod = ScrollingMovementMethod()
                responseView.movementMethod = movementMethod
            }
        } else {
            Log.d(TAG, "LOCATION permission not granted")
            checkPermissionThenFindCurrentPlace()
        }
    }

Đoạn mã này xác định những trường cần yêu cầu cho các địa điểm có khả năng xuất hiện, tạo một FindCurrentPlaceRequest, bắt đầu tác vụ và điền thông tin chi tiết được yêu cầu vào TextView.

Thêm hoạt động Địa điểm hiện tại vào tệp kê khai

Thêm phần tử <activity> cho CurrentPlaceActivity dưới dạng phần tử con của phần tử <application> trong tệp AndroidManifest.xml, nằm trong app/src/main:

        <activity android:name=".CurrentPlaceActivity" android:label="@string/current_demo_title" />

Thêm hoạt động Địa điểm hiện tại vào trình đơn minh hoạ

Giống như trước đây, hãy thêm bản minh hoạ Current Place vào màn hình chính bằng cách thêm bản minh hoạ đó vào danh sách trong mô-đun Demo. Bây giờ, sau khi bạn đã tạo một hoạt động Current Place, hãy thêm hoạt động đó vào tệp Demo.kt trong thư mục src/main/java/com/google/codelabs/maps/placesdemo/. Dán đoạn mã này ngay sau mục AUTOCOMPLETE_FRAGMENT_DEMO:

    CURRENT_FRAGMENT_DEMO(
        R.string.current_demo_title,
        R.string.current_demo_description,
        CurrentPlaceActivity::class.java
    ),

Các chuỗi liên kết được xác định trong tệp src/main/res/values/strings.xml.

Chạy ứng dụng

  1. Chạy ứng dụng. Lần này, bạn sẽ thấy 3 mục trong danh sách trên màn hình chính.
  2. Nhấn vào hàng Địa điểm hiện tại. Bạn sẽ thấy một nút trên màn hình.
  3. Nhấn vào nút. Nếu bạn chưa cấp quyền truy cập thông tin vị trí cho ứng dụng này trước đây, thì một yêu cầu cấp quyền sẽ xuất hiện.
  4. Cấp quyền cho ứng dụng truy cập vào thông tin vị trí của thiết bị.
  5. Nhấn vào nút đó một lần nữa. Lần này, một danh sách gồm tối đa 20 địa điểm lân cận và xác suất của các địa điểm đó sẽ xuất hiện, như minh hoạ trong Hình 5.

Đưa ra các kết quả phù hợp có thể là Địa điểm hiện tại cho vị trí mà thiết bị báo cáo

Hình 5. Đưa ra các kết quả phù hợp có khả năng là Địa điểm hiện tại cho vị trí được báo cáo của thiết bị.

11. Hiển thị Địa điểm hiện tại trên bản đồ

Thêm phần phụ thuộc Map

Trong tệp build.gradle ở cấp mô-đun, hãy thêm phần phụ thuộc Dịch vụ Google Play cho Maps SDK cho Android.

app/build.gradle

dependencies {
    // ...
    implementation 'com.google.android.gms:play-services-maps:18.2.0'
}

Cập nhật tệp kê khai Android để tính đến các bản đồ

Thêm các phần tử meta-data sau đây vào phần tử application.

Các thư viện này nhúng phiên bản Dịch vụ Google Play mà ứng dụng được biên dịch và chỉ định khoá API của bạn.

AndroidManifest.xml

        <meta-data
            android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version" />

        <meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="${MAPS_API_KEY}" />

Thêm khoá API vào secrets.properties

Mở tệp secrets.properties trong thư mục cấp cao nhất, sau đó thêm đoạn mã sau. Thay thế YOUR_API_KEY bằng khoá API của bạn.

MAPS_API_KEY=YOUR_API_KEY

Mở tệp local.defaults.properties trong thư mục cấp cao nhất (cùng thư mục với tệp secrets.properties), sau đó thêm đoạn mã sau.

MAPS_API_KEY=DEFAULT_API_KEY

Kiểm tra khoá API

Trong onCreate(), ứng dụng sẽ kiểm tra khoá API Maps và khởi động mảnh hỗ trợ Maps. getMapAsync() dùng để đăng ký lệnh gọi lại bản đồ.

Thêm mã sau để thực hiện việc này.

CurrentPlaceActivity.kt

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_current)

        val apiKey = BuildConfig.MAPS_API_KEY

        // Log an error if apiKey is not set.
        if (apiKey.isEmpty() || apiKey == "DEFAULT_API_KEY") {
            Log.e("Places test", "No api key")
            finish()
            return
        }

        // Retrieve a PlacesClient (previously initialized - see DemoApplication)
        placesClient = Places.createClient(this)
        (supportFragmentManager
            .findFragmentById(R.id.map) as SupportMapFragment?)?.getMapAsync(this)

        // ...
    }

Tạo bố cục bản đồ

  1. Trong thư mục app/src/main/res/layout/, hãy tạo tệp bố cục fragment_map.xml rồi điền đoạn mã sau vào bố cục.

res/layout/fragment_map.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/map"
    android:name="com.google.android.gms.maps.SupportMapFragment"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    tools:context="com.google.codelabs.maps.placesdemo.CurrentPlaceActivity" />

Thao tác này xác định một SupportMapFragment để đóng vai trò là vùng chứa cho bản đồ và cung cấp quyền truy cập vào đối tượng GoogleMap.

  1. Trong bố cục activity_current.xml có trong thư mục app/src/main/res/layout/, hãy thêm đoạn mã sau vào cuối bố cục tuyến tính.

res/layout/activity_current.xml

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingTop="16dp"
        android:paddingBottom="16dp"
        android:text="@string/showing_most_likely_place"
        style="@style/TextAppearance.AppCompat.Title"/>

    <include layout="@layout/fragment_map"/>

TextView được thêm vào sẽ tham chiếu đến một tài nguyên chuỗi mới cần được tạo.

  1. Trong app/src/main/res/values/strings.xml, hãy thêm tài nguyên chuỗi sau.

res/values/strings.xml

<string name="showing_most_likely_place">Showing most likely place</string>

Vì các khung hiển thị bổ sung đã được thêm vào bản đồ, nên TextView hiển thị danh sách địa điểm cần được đặt chiều cao để các khung hiển thị này vẫn xuất hiện.

  1. Thêm thuộc tính maxHeight vào TextView bằng mã nhận dạng current_response_content

res/layout/activity_current.xml

android:maxHeight="200dp"

Triển khai OnMapReadyCallback

Triển khai giao diện OnMapReadyCallback bằng cách thêm giao diện này vào nội dung khai báo lớp và ghi đè phương thức onMapReady() để thiết lập bản đồ khi đối tượng GoogleMap có sẵn:

CurrentPlaceActivity.kt

class CurrentPlaceActivity : AppCompatActivity(), OnMapReadyCallback {

Thêm mã sau vào cuối lớp:

CurrentPlaceActivity.kt

    override fun onMapReady(map: GoogleMap) {
        this.map = map
        lastKnownLocation?.let { location ->
            map.moveCamera(
                CameraUpdateFactory.newLatLngZoom(
                    location,
                    DEFAULT_ZOOM
                )
            )
        }
    }

Lệnh gọi lại yêu cầu một số biến lớp để hoạt động đúng cách. Ngay sau tiêu đề lớp, hãy thêm nội dung sau:

CurrentPlaceActivity.kt

private var map: GoogleMap? = null
private var lastKnownLocation: LatLng? = null

Thêm mã sau vào đối tượng đồng hành của lớp:

CurrentPlaceActivity.kt

private const val DEFAULT_ZOOM = 15f

Chạy ứng dụng

  1. Chạy ứng dụng.
  2. Nhấn vào hàng Địa điểm hiện tại. Bạn sẽ thấy một nút trên màn hình.
  3. Nhấn vào nút. Nếu bạn chưa cấp quyền truy cập thông tin vị trí cho ứng dụng này trước đây, thì một yêu cầu cấp quyền sẽ xuất hiện.
  4. Cấp quyền cho ứng dụng truy cập vào thông tin vị trí của thiết bị.
  5. Nhấn vào nút đó một lần nữa. Bản đồ sẽ xuất hiện.

Hoạt động Hiện tại cho thấy bản đồ

Hình 6. Hoạt động Hiện tại cho thấy bản đồ.

Cập nhật bản đồ bằng một địa điểm

Thêm mã sau vào cuối lớp:

CurrentPlaceActivity.kt

private data class LikelyPlace(
    val name: String,
    val address: String,
    val attribution: List<String>,
    val latLng: LatLng
)

private fun PlaceLikelihood.toLikelyPlace(): LikelyPlace? {
    val name = this.place.name
    val address = this.place.address
    val latLng = this.place.latLng
    val attributions = this.place.attributions ?: emptyList()

    return if (name != null && address != null && latLng != null) {
        LikelyPlace(name, address, attributions, latLng)
    } else {
        null
    }
}

Các đối tượng này được dùng để lưu trữ dữ liệu của Địa điểm và định dạng dữ liệu đó.

Ở đầu lớp, hãy thêm đoạn mã sau để tạo một biến dùng để lưu trữ dữ liệu của Địa điểm được trả về.

CurrentPlaceActivity.kt

private val likelyPlaces = mutableListOf<LikelyPlace>()

Trong bước này , mã sẽ được thay đổi để người dùng thấy danh sách Địa điểm và họ sẽ chọn một địa điểm để hiển thị trên bản đồ. Tất cả dữ liệu về Địa điểm đều xuất hiện trong một danh sách trên màn hình.

Trong hàm findCurrentPlace, trong khối lifecycleScope.launch trước dòng mã này

CurrentPlaceActivity.kt

responseView.text = response.prettyPrint()

thêm mã sau:

CurrentPlaceActivity.kt

                likelyPlaces.clear()

                likelyPlaces.addAll(
                    response.placeLikelihoods.take(M_MAX_ENTRIES).mapNotNull { placeLikelihood ->
                        placeLikelihood.toLikelyPlace()
                    }
                )

                openPlacesDialog()

Mã này yêu cầu một hằng số cho số lượng địa điểm tối đa cần hiển thị.

Trong đối tượng đồng hành, hãy thêm mã cho hằng số đó.

CurrentPlaceActivity.kt

private const val M_MAX_ENTRIES = 5

Thêm mã sau đây để tạo hộp thoại cho phép người dùng chọn một địa điểm.

CurrentPlaceActivity.kt

    /**
     * Displays a form allowing the user to select a place from a list of likely places.
     */
    private fun openPlacesDialog() {
        // Ask the user to choose the place where they are now.
        val listener =
            DialogInterface.OnClickListener { _, which -> // The "which" argument contains the position of the selected item.
                val likelyPlace = likelyPlaces[which]
                lastKnownLocation = likelyPlace.latLng

                val snippet = buildString {
                    append(likelyPlace.address)
                    if (likelyPlace.attribution.isNotEmpty()) {
                        append("\n")
                        append(likelyPlace.attribution.joinToString(", "))
                    }
                }

                val place = Place.builder().apply {
                    name = likelyPlace.name
                    latLng = likelyPlace.latLng
                }.build()

                map?.clear()

                setPlaceOnMap(place, snippet)
            }

        // Display the dialog.
        AlertDialog.Builder(this)
            .setTitle(R.string.pick_place)
            .setItems(likelyPlaces.map { it.name }.toTypedArray(), listener)
            .setOnDismissListener {
                currentButton.isEnabled = true
            }
            .show()
    }

Theo các phương pháp hay nhất của Android, hộp thoại này tham chiếu đến một tài nguyên chuỗi cần được thêm vào tệp tài nguyên strings.xml nằm trong thư mục app/src/main/res/values/.

Thêm nội dung sau vào strings.xml:

res/values/strings.xml

    <string name="pick_place">Choose a place</string>

Sau đó, các hàm này sẽ gọi hàm setPlaceOnMap để di chuyển camera và đặt một điểm đánh dấu tại vị trí đã chọn.

Thêm mã sau:

CurrentPlaceActivity.kt

    private fun setPlaceOnMap(place: Place?, markerSnippet: String?) {
        val latLng = place?.latLng ?: defaultLocation
        map?.moveCamera(
            CameraUpdateFactory.newLatLngZoom(
                latLng,
                DEFAULT_ZOOM
            )
        )
        map?.addMarker(
            MarkerOptions()
                .position(latLng)
                .title(place?.name)
                .snippet(markerSnippet)
        )
    }

Bạn cũng nên lưu và khôi phục trạng thái của bản đồ.

Để lưu trạng thái của thành phần này, hãy ghi đè hàm onSaveInstanceState và thêm mã sau:

CurrentPlaceActivity.kt

    /**
     * Saves the state of the map when the activity is paused.
     */
    override fun onSaveInstanceState(outState: Bundle) {
        outState.putParcelable(KEY_LOCATION, lastKnownLocation)
        super.onSaveInstanceState(outState)
    }

Để khôi phục trạng thái của thành phần này, trong onCreate, hãy thêm mã sau sau lệnh gọi đến setContentView:

CurrentPlaceActivity.kt

        if (savedInstanceState != null) {
            lastKnownLocation = savedInstanceState.getParcelable(KEY_LOCATION)
        }

Để lưu và khôi phục, bạn cần có một khoá. Đây là một hằng số từ đối tượng đồng hành.

Trong khối đối tượng đồng hành, hãy thêm nội dung sau:

CurrentPlaceActivity.kt

        // Key for storing activity state.
        private const val KEY_LOCATION = "location"

Chạy ứng dụng

  1. Chạy ứng dụng.
  2. Nhấn vào hàng Địa điểm hiện tại. Bạn sẽ thấy một nút trên màn hình.
  3. Nhấn vào nút. Nếu bạn chưa cấp quyền truy cập thông tin vị trí cho ứng dụng này trước đây, thì một yêu cầu cấp quyền sẽ xuất hiện.
  4. Cấp quyền cho ứng dụng truy cập vào thông tin vị trí của thiết bị.
  5. Nhấn vào nút đó một lần nữa.
  6. Chọn một địa điểm bằng cách nhấn vào địa điểm đó. Bản đồ sẽ được phóng to và căn giữa với một điểm đánh dấu được đặt tại vị trí đã chọn.

Bản đồ có điểm đánh dấu tại vị trí đã chọn

Hình 7. Bản đồ có điểm đánh dấu tại vị trí đã chọn.

12. Xin chúc mừng

Bạn đã tạo thành công một ứng dụng Android bằng Places SDK for Android.

Kiến thức bạn học được

Tiếp theo là gì?

  • Khám phá hoặc phân nhánh kho lưu trữ android-places-demos GitHub gồm các mẫu và bản minh hoạ để có thêm ý tưởng.
  • Tìm hiểu qua các lớp học lập trình khác về Kotlin để tạo ứng dụng Android bằng Nền tảng Google Maps.
  • Hãy giúp chúng tôi tạo nội dung hữu ích nhất cho bạn bằng cách trả lời câu hỏi sau:

Bạn muốn xem những lớp học lập trình nào khác?

Trực quan hoá dữ liệu trên bản đồ Tìm hiểu thêm về cách tuỳ chỉnh kiểu bản đồ Tạo các tương tác 3D trên bản đồ

Lớp học lập trình bạn muốn không có trong danh sách? Yêu cầu cấp lại bằng cách báo lỗi mới tại đây.