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ênGoogle Map.
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:
Các bạn đã sẵn sàng chưa, chúng ta bắt đầu nhé
Nội dung chính của bài viết
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
- Bạn phải có Google Map API để hiển thị bản đồ. Xem đường link này để lấy API Key
- 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.
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.
Giờ thì, chúng ta cần thêm file google-services.json. Ở bài viết này, mình sẽ không hướng dẫn chi tiết cách tạo firebase project và đăng ký ứng dụng Android vào firebase. Những bước này, các bạn có thể tham khảo ở video bên dưới nhé:
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.
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>
♥ Đọc thêm: Toàn tập về Activity trong Android
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
♥ Đọc thêm: Tạo tính năng xác thực số điện thoại bằng Firebase
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.
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
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.
Hi vọng bài viết có ích cho các bạn, đừng quen comment và chia sẻ bài viết nhé.
♥ Tham khảo: AR là gì? Cách phát triển ứng dụng AR trên Android
Thanks em nhiều , bài viết hay và rõ ràng. Đang lót dép ngồi hóng phần 2 của em nè. Chúc em vui khoẻ nha
Em cám ơn anh đã ủng hộ ạ.
Anh ơi cho em hỏi đã có phần 2 chưa ạ
Hi
Mình sẽ update code trong ngày mai bạn nhé.
Hi, great tutorial, have you completed the part 2
Hi,
I will provide code for part 2 by tomorrow.
Anh ơi khi nào ra phần 2 dành cho user
Hi Đức Minh,
Sắp rồi bạn nhé.
Cảm ơn anh, Nhờ anh chỉnh sửa lại giúp phần .xml :
– android:theme=”@style/SCBSwitch” /> tạo như thế nào à?
Hi Quyết,
Bạn tạo một file style.xml trong folder res/values nhé.
bạn tham khảo thêm tại đây cách tạo nhé:
https://developer.android.com/guide/topics/resources/style-resource
Cảm ơn anh nhé :)) Phần cấu hình anh nên nói rõ hơn chút cho dễ hiểu