Xây dựng Car Location Tracking cho Android với Firebase

Bài viết được sự cho phép của tác giả Sơn Dương

Trong bài hướng dẫn này, chúng ta sẽ tạo một ứng dụng theo dõi vị trí xe (Car Location Tracking) giống như Grab và Uber. Bài viết này mình sẽ sử dụng Firebase Real-time Database. Tài xế chỉ cần gửi vị trí hiện tại về firebase và khách hàng sẽ cập nhật được vị trí của lái xe trên Google Map.

>>> Xem thêm: Firebase là gì

Mặc dù chúng ta sẽ không phát triển một ứng dụng hoàn thiện như Grab hay Uber. Nhưng mình sẽ hướng dẫn các bạn tự xây dựng một tính năng rất quan trọng đó là cập nhật thời gian thực, hiển thị thông tin tài xế trên ứng dụng khách hàng.

Bài viết sẽ chia làm 2 phần:

  • Phần 1: Xây dựng tính năng gửi location theo thời gian thực cho tài xế (dành cho tài xế).
  • Phần 2: Xây dựng tính năng hiển thị vị trí tài xế theo thời gian thực (dành cho khách hàng).

Kết quả của bài viết này sẽ là ứng dụng Car Location Tracking như bên dưới:

Từng bước xây dựng ứng dụng Car Location Tracking trên Android

#1. Yêu cầu trước khi bắt đầu xây dựng Car Location Tracking

  1. Bạn phải có Google Map API để hiển thị bản đồ. Xem đường link này để lấy API Key
  2. Cần có một Firebase project để sử dụng real-time database. Bạn có thể tạo Firebase project tại đây.

Sau khi bạn đã hoàn thành 2 bước trên thì chuyển tiếp sang bước bên dưới nhé.

#2. Lập trình ứng dụng cho tài xế(Driver App)

Như mình đã nói ở trên, tổng thể ứng dụng Car Location Tracking  sẽ chia làm 2 ứng dụng độc lập. Một ứng dụng dành riêng cho tài xế và một dành riêng cho khách hàng.

Vì vậy, phần 1 của bài viết này, chúng ta sẽ bắt đầu với ứng dụng dành cho tài xế, gọi là Driver App.

  AsyncTask trong Android – công cụ xử lý đa luồng hữu hiệu

  Sử dụng Intelligent constants trong lập trình Android

Cài đặt thư viện cần thiết

Đầu tiên, hãy thêm thư viện vào build.gradle

implementation 'com.google.android.gms:play-services-location:15.0.1'
implementation 'com.google.android.gms:play-services-maps:15.0.1'
implementation 'com.google.firebase:firebase-database:16.0.1'

Sau đó thêm những permission cần thiết cho ứng dụng. Trong 3 permissions này thì permission về quyền location là bạn cần phải được sự đồng ý của người dùng.

Bạn có thể tham khảo thêm bài viết của mình về cách xin cấp permission trong Android.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

Ngoài ra, để hiển thị Google Maps trong ứng dụng Car Location Tracking  chúng ta cần thêm các thẻ meta vào trong Manifest file.

<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="@string/map_api_key" /> // Change it with your Google Maps API key.

Tất cả những khâu chuẩn bị đã sẵn sàng cho việc hiển thị Google Maps và đọc vị trí của người dùng.

Xây dựng giao diện ứng dụng Car Location Tracking 

Ok, không chần chừ thêm nữa chúng ta hãy cùng bắt tay vào việc lập trình nào. Dưới đây là giao diện người dùng của DriverApp mà chúng ta sẽ tạo.

ung-dung-car-location-tracking-cho-android-1

Giao diện rất cơ bản, chúng ta có SwitchCompat dành cho cả tài xế trực tuyến và ngoại tuyến, bên dưới là Google Map.

Để tạo giao diện như trên, các bạn code như bên dưới đây (các bạn code vào file layout là activity_main.xml)

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <FrameLayout
        android:id="@+id/driverStatusLayout"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="@color/colorPrimary"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/driverStatusTextView"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_marginStart="15dp"
            android:gravity="center"
            android:text="@string/offline"
            android:textColor="@color/colorIcons"
            android:textSize="22sp" />

       <android.support.v7.widget.SwitchCompat
           android:id="@+id/driverStatusSwitch"
           android:layout_width="wrap_content"
           android:layout_height="match_parent"
           android:layout_gravity="end"
           android:layout_marginEnd="15dp"
           android:checked="false"
           android:theme="@style/SCBSwitch" />

      </FrameLayout>

      <fragment
          android:id="@+id/supportMap"
          android:name="com.google.android.gms.maps.SupportMapFragment"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:layout_below="@+id/driverStatusLayout"
tools:context="spartons.com.frisbeeGo.fragments.MapFragment" />

</RelativeLayout>

MainActivity

Sau khi đã có layout, chúng ta sẽ code để hiển thị map và lấy location. Dưới đây là code cho Activity chính

class MainActivity : AppCompatActivity() {

    companion object {
        private const val MY_PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION = 2200
    }

    private lateinit var googleMap: GoogleMap
    private lateinit var locationProviderClient: FusedLocationProviderClient
    private lateinit var locationRequest: LocationRequest
    private lateinit var locationCallback: LocationCallback
    private var locationFlag = true
    private var driverOnlineFlag = false
    private var currentPositionMarker: Marker? = null
    private val googleMapHelper = GoogleMapHelper()
    private val firebaseHelper = FirebaseHelper("0000")
    private val markerAnimationHelper = MarkerAnimationHelper()
    private val uiHelper = UiHelper()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val mapFragment: SupportMapFragment = supportFragmentManager.findFragmentById(R.id.supportMap) as SupportMapFragment
        mapFragment.getMapAsync { googleMap = it }
        createLocationCallback()
        locationProviderClient = LocationServices.getFusedLocationProviderClient(this)
        locationRequest = uiHelper.getLocationRequest()
        if (!uiHelper.isPlayServicesAvailable(this)) {
            Toast.makeText(this, "Play Services did not installed!", Toast.LENGTH_SHORT).show()
            finish()
        } else requestLocationUpdate()
        val driverStatusTextView = findViewById<TextView>(R.id.driverStatusTextView)
        findViewById<SwitchCompat>(R.id.driverStatusSwitch).setOnCheckedChangeListener { _, b ->
            driverOnlineFlag = b
            if (driverOnlineFlag) driverStatusTextView.text = resources.getString(R.string.online_driver)
            else {
                driverStatusTextView.text = resources.getString(R.string.offline)
                firebaseHelper.deleteDriver()
            }
        }
    }

    @SuppressLint("MissingPermission")
    private fun requestLocationUpdate() {
        if (!uiHelper.isHaveLocationPermission(this)) {
            ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), MY_PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION)
            return
        }
        if (uiHelper.isLocationProviderEnabled(this))
            uiHelper.showPositiveDialogWithListener(this, resources.getString(R.string.need_location), resources.getString(R.string.location_content), object : IPositiveNegativeListener {
                override fun onPositive() {
startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
                }
            }, "Turn On", false)
locationProviderClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper())
    }

    private fun createLocationCallback() {
        locationCallback = object : LocationCallback() {
            override fun onLocationResult(locationResult: LocationResult?) {
               super.onLocationResult(locationResult)
               if (locationResult!!.lastLocation == null) return
               val latLng = LatLng(locationResult.lastLocation.latitude, locationResult.lastLocation.longitude)
               Log.e("Location", latLng.latitude.toString() + " , " + latLng.longitude)
               if (locationFlag) {
                   locationFlag = false
                   animateCamera(latLng)
               }
               if (driverOnlineFlag) firebaseHelper.updateDriver(Driver(lat = latLng.latitude, lng = latLng.longitude))
               showOrAnimateMarker(latLng)
           }
       }
   }

   private fun showOrAnimateMarker(latLng: LatLng) {
       if (currentPositionMarker == null)
           currentPositionMarker = googleMap.addMarker(googleMapHelper.getDriverMarkerOptions(latLng))
       else markerAnimationHelper.animateMarkerToGB(currentPositionMarker!!, latLng, LatLngInterpolator.Spherical())
   }

   private fun animateCamera(latLng: LatLng) {
       val cameraUpdate = googleMapHelper.buildCameraUpdate(latLng)
       googleMap.animateCamera(cameraUpdate, 10, null)
   }

   override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
       super.onRequestPermissionsResult(requestCode, permissions, grantResults)
       if (requestCode == MY_PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION) {
           val value = grantResults[0]
           if (value == PERMISSION_DENIED) {
               Toast.makeText(this, "Location Permission denied", Toast.LENGTH_SHORT).show()
               finish()
           } else if (value == PERMISSION_GRANTED) requestLocationUpdate()
       }
    }
}

Mình sẽ giải thích cụ thể tác dụng của các hàm quan trọng:

1.createLocationCallback(): Chúng ta gọi hàm này từ hàm onCreate của MainActivity. Trong hàm LocationCallback, chúng ta sẽ lấy vị trí hiện tại của tài xế,và cập nhật trên Firebase Real-time Database nếu tài xế đang trực tuyến

2.requestLocationUpdates(): Gọi hàm này từ hàm onCreate của MainActivity nếu người dùng đã cài đặt GooglePlayService.

Trong hàm này, chúng ta sẽ cần đoạn mã để yêu cầu người dùng cấp quyền cho Location permission. Sau đó chúng tôi kiểm tra Location provider đã được bật lên hay chưa. Cuối cùng là bắt đầu cập nhật vị trí.

3. showOrAnimateMarker(): chúng ta sẽ kiểm tra xem thử Marker của xe tài xế đã có rồi hay chưa, nếu chưa thì tạo mới  một Marker vào Google Maps. Nếu đã có rồi thì tạo hiệu ứng chuyển động cho Marker đến vị trí mới.

4. animteCamera(): Mục đích chính của hàm này là tạo hiệu ứng và chuyển map về vị trí hiện tại

UiHelper

Class này mình tạo riêng với mục đích sẽ viết những hàm mà mình có thể tái sử dụng nhiều lần. Như tên của class, các hàm liên quan đến UI sẽ được mình để vào đây

class UiHelper {

    fun isPlayServicesAvailable(context: Context): Boolean {
        val googleApiAvailability = GoogleApiAvailability.getInstance()
        val status = googleApiAvailability.isGooglePlayServicesAvailable(context)
        return ConnectionResult.SUCCESS == status
    }

    fun isHaveLocationPermission(context: Context): Boolean {
        return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ActivityCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
    }

    fun isLocationProviderEnabled(context: Context): Boolean {
        val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
        return !locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) && !locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
    }

    fun showPositiveDialogWithListener(callingClassContext: Context, title: String, content: String, positiveNegativeListener: IPositiveNegativeListener, positiveText: String, cancelable: Boolean) {
        buildDialog(callingClassContext, title, content)
                .builder
                .positiveText(positiveText)
                .positiveColor(getColor(R.color.colorPrimary, callingClassContext))
                .onPositive { _, _ -> positiveNegativeListener.onPositive() }
                .cancelable(cancelable)
                .show()
    }

    private fun buildDialog(callingClassContext: Context, title: String, content: String): MaterialDialog {
         return MaterialDialog.Builder(callingClassContext)
                 .title(title)
                 .content(content)
                 .build()
    }


    private fun getColor(color: Int, context: Context): Int {
        return ContextCompat.getColor(context, color)
    }

    fun getLocationRequest() : LocationRequest {
        val locationRequest = LocationRequest.create()
        locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
        locationRequest.interval = 3000
        return locationRequest
    }
}

Mình sẽ giải thích một số hàm quan trọng:
isPlayServicesAvailable():Hàm này kiểm tra việc người dùng đã cài Google Play Services hay chưa.

isHaveLocationPermission():Kiểm tra xem người dùng có cấp quyền truy cập vị trí (location permission) hay không.

isLocationProviderEnabled(): Kiểm tra xem Location Provider đã được kích hoạt hay chưa. Nếu chưa thì mở Setting và bật Location Provider( Chọn GPS hay Network…)

showPositiveDialogWithListener(): Chức năng tiện ích để hiển thị Dialog khi điện thoại người dùng vì lý do nào đó mà tắt Location Provider

GoogleMapHelper

Class này sẽ gồm những hàm dàng riêng cho map cho Car Location Tracking .

class GoogleMapHelper {

    companion object {
        private const val ZOOM_LEVEL = 18
        private const val TILT_LEVEL = 25
    }

    /**
     * @param latLng in which position to Zoom the camera.
     * @return the [CameraUpdate] with Zoom and Tilt level added with the given position.
     */

    fun buildCameraUpdate(latLng: LatLng): CameraUpdate {
        val cameraPosition = CameraPosition.Builder()
                .target(latLng)
                .tilt(TILT_LEVEL.toFloat())
                .zoom(ZOOM_LEVEL.toFloat())
                .build()
        return CameraUpdateFactory.newCameraPosition(cameraPosition)
    }

    /**
     * @param position where to draw the [com.google.android.gms.maps.model.Marker]
     * @return the [MarkerOptions] with given properties added to it.
     */

    fun getDriverMarkerOptions(position: LatLng): MarkerOptions {
        val options = getMarkerOptions(R.drawable.car_icon, position)
        options.flat(true)
        return options
    }

    private fun getMarkerOptions(resource: Int, position: LatLng): MarkerOptions {
        return MarkerOptions()

 .icon(BitmapDescriptorFactory.fromResource(resource))
                 .position(position)
    }
}

MarkerAnimationHelper

Lớp MarkerAnimationHelper tạo hiệu ứng cho marker khi xe tài xế di chuyển từ vị trí cũ tới vị trí mới.

class MarkerAnimationHelper {

    fun animateMarkerToGB(marker: Marker, finalPosition: LatLng, latLngInterpolator: LatLngInterpolator) {
        val startPosition = marker.position
        val handler = Handler()
        val start = SystemClock.uptimeMillis()
        val interpolator = AccelerateDecelerateInterpolator()
        val durationInMs = 2000f
        handler.post(object : Runnable {
            var elapsed: Long = 0
            var t: Float = 0.toFloat()
            var v: Float = 0.toFloat()
            override fun run() {
                // Calculate progress using interpolator
                elapsed = SystemClock.uptimeMillis() - start
                t = elapsed / durationInMs
                v = interpolator.getInterpolation(t)
                marker.position = latLngInterpolator.interpolate(v, startPosition, finalPosition)
                // Repeat till progress is complete.
                if (t < 1) {
                    // Post again 16ms later.
                    handler.postDelayed(this, 16)
                }
            }
        })
    }
}

FirebaseHelper

Mình sẽ viết những hàm liên quan đến kết nôi Firebase tại class này:

class FirebaseHelper constructor(driverId: String) {

    companion object {
        private const val ONLINE_DRIVERS = "online_drivers"
    }

    private val onlineDriverDatabaseReference: DatabaseReference = FirebaseDatabase
            .getInstance()
            .reference
            .child(ONLINE_DRIVERS)
            .child(driverId)

    init {
        onlineDriverDatabaseReference
                .onDisconnect()
                .removeValue()
    }

    fun updateDriver(driver: Driver) {
        onlineDriverDatabaseReference
                .setValue(driver)
        Log.e("Driver Info", " Updated")
    }

    fun deleteDriver() {
        onlineDriverDatabaseReference
               .removeValue()
    }
}

Trước khi bắt đầu giải thích về class FirebaseHelper, mình muốn cho bạn thấy cấu trúc của Firebase Real-time Database.

ung-dung-car-location-tracking-cho-android-2

Mình sẽ giải thích một số hàm quan trọng trong FirebaseHelper.
onlineDriverDatabaseReference(): Khi tạo DatabaseReference, chúng ta cần thêm hai thư mục: một cho các điểm mà các drivers đang online khác, một cho bản thân driver đó.

Chúng ta cần thông báo firebase real-time database để cập nhật thông tin vị trí Driver. Đó chính là lý do tại sao mình lại thiết lập driverId như là top node và là một đối tượng Driver. Lưu ý driverId phải unique

updateDriver(): Cập nhật vị trí mới của Driver firebase real-time database.

deleteDriver(): Loại bỏ driver node khỏi firebase real-time database.

Driver Object

Class này đơn giản là model để mình định nghĩa object driver với các thuộc tính: driverId, lat, lng

data class Driver(val lat: Double, val lng: Double, val driverId: String = "0000")

Bạn có thể thay đổi driverId bằng mã khóa chính của người dùng hoặc bất kỳ thứ gì mà bạn cho là unique

Giải thích thêm: Cách tiếp cận mà mình sử dụng cho 2 mode của tài xế: online và offline tương tự cách mà các ứng dụng Social Media đang làm. Tức là hiển thị trạng thái online cuối cùng, kiểm tra xem người dùng có online hay không, v.v.

Tổng kết

Như vậy là chúng ta đã hoàn thành ứng dụng Car location Tracking phần danh cho tài xế. Toàn bộ source code, các bạn có thể download bên dưới.

Bài viết sau, mình sẽ tiếp tục hướng dẫn các bạn xây dựng phần hiển thị vị trí của tài xế, phần dành cho khách hàng.

Bài viết gốc được đăng tải tại vntalking.com

Xem thêm:

Tìm việc làm IT mọi cấp độ tại TopDev