Reputation: 31
I have previously asked this question and on advice of a user I am posting again this time with my attempt at implementing Coroutines. I am trying to build an app to plot a route using google maps and directions API and the tutorial I am following is old and uses AsyncTask but this of course is depreciated so now I have to implement coroutines and I am struggling with this. here is my code before with AsyncTask and after with my attempt at coroutines:
inner class ParserTask :
AsyncTask<String?, Int?, List<List<HashMap<String, String>>>?> () {
override fun doInBackground(vararg jsonData: String?): List<List<HashMap<String, String>>>? {
val jObject: JSONObject
var routes: List<List<HashMap<String, String>>>? =
null
try {
jObject = JSONObject(jsonData[0])
val parser = DataParser()
routes = parser.parse(jObject)
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
return routes
}
override fun onPostExecute(result: List<List<HashMap<String, String>>>?) {
val points = ArrayList<LatLng?>()
val lineOptions = PolylineOptions()
for (i in result!!.indices) {
val path =
result[i]
for (j in path.indices) {
val point = path[j]
val lat = point["lat"]!!.toDouble()
val lng = point["lng"]!!.toDouble()
val position = LatLng(lat, lng)
points.add(position)
}
lineOptions.addAll(points)
lineOptions.width(8f)
lineOptions.color(Color.RED)
lineOptions.geodesic(true)
}
if (points.size != 0) mMap!!.addPolyline(lineOptions)
}
}
inner class DownloadTask :
AsyncTask<String?, Void?, String>(){
override fun onPostExecute(result: String) {
super.onPostExecute(result)
val parserTask = ParserTask()
parserTask.execute(result)
}
override fun doInBackground(vararg url: String?): String {
var data = ""
try{
data = downloadUrl(url[0].toString()).toString()
} catch (e: java.lang.Exception) {
Log.d("Background Task", e.toString())
}
return data
}
}
private fun ParserTask (vararg jsonData: String?, result: List<List<HashMap<String, String>>>?): List<List<HashMap<String, String>>>? {
async {
val points = ArrayList<LatLng?>()
val lineOptions = PolylineOptions()
for (i in result!!.indices) {
val path =
result[i]
for (j in path.indices) {
val point = path[j]
val lat = point["lat"]!!.toDouble()
val lng = point["lng"]!!.toDouble()
val position = LatLng(lat, lng)
points.add(position)
}
lineOptions.addAll(points)
lineOptions.width(8f)
lineOptions.color(Color.RED)
lineOptions.geodesic(true)
}
if (points.size != 0) mMap!!.addPolyline(lineOptions)
}
val jObject: JSONObject
var routes: List<List<HashMap<String, String>>>? =
null
try {
jObject = JSONObject(jsonData[0])
val parser = DataParser()
routes = parser.parse(jObject)
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
return routes
}
private fun DownloadTask (vararg url: String?, result String): String {
async {
super.onPostExecute(result)
val parserTask = ParserTask()
parserTask.execute(result)
}
var data = ""
try{
data = downloadUrl(url[0].toString()).toString()
} catch (e: java.lang.Exception) {
Log.d("Background Task", e.toString())
}
return data
}
class MainActivity : AppCompatActivity(), OnMapReadyCallback, CoroutineScope by MainScope ()
//, LocationListener, GoogleMap.OnCameraMoveListener, GoogleMap.OnCameraMoveStartedListener, GoogleMap.OnCameraIdleListener
{
private var mMap: GoogleMap? = null
lateinit var mapView: MapView
private val MAP_VIEW_BUNDLE_KEY = "MapViewBundleKey"
private val DEFAULT_ZOOM = 15f
lateinit var B_search: Button
lateinit var tvCurrentAddress: TextView
private var fusedLocationProviderClient: FusedLocationProviderClient? = null
var end_latitude = 0.0
var end_longitude = 0.0
lateinit var origin: MarkerOptions
lateinit var destination: MarkerOptions
var latitude = 0.0
var longitude = 0.0
override fun onMapReady(googleMap: GoogleMap) {
mapView.onResume()
mMap = googleMap
//askPermissionLocation()
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_COARSE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
return
}
mMap!!.setMyLocationEnabled(true)
// mMap!!.setOnCameraMoveListener (this)
// mMap!!.setOnCameraMoveStartedListener(this)
// mMap!!.setOnCameraIdleListener(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mapView = findViewById<MapView>(R.id.map1)
tvCurrentAddress = findViewById<TextView>(R.id.tvAdd)
B_search = findViewById(R.id.B_search)
//askPermissionLocation()
var mapViewBundle: Bundle? = null
if (savedInstanceState != null) {
mapViewBundle = savedInstanceState.getBundle(MAP_VIEW_BUNDLE_KEY)
}
mapView.onCreate(mapViewBundle)
mapView.getMapAsync(this)
B_search.setOnClickListener {
searchArea()
}
}
private fun searchArea() {
val tf_location =
findViewById<View>(R.id.TF_location) as EditText
val location = tf_location.text.toString()
var addressList: List<Address>? = null
val markerOptions = MarkerOptions()
if (location != "") {
val geocoder = Geocoder(applicationContext)
try {
addressList = geocoder.getFromLocationName(location, 5)
} catch (e: IOException) {
e.printStackTrace()
}
if (addressList != null) {
for (i in addressList.indices) {
val myAddress = addressList[i]
val latLng =
LatLng(myAddress.latitude, myAddress.longitude)
markerOptions.position(latLng)
mMap!!.addMarker(markerOptions)
end_latitude = myAddress.latitude
end_longitude = myAddress.longitude
mMap!!.animateCamera(CameraUpdateFactory.newLatLng(latLng))
val mo = MarkerOptions()
mo.title("Distance")
val results = FloatArray(10)
Location.distanceBetween(
latitude,
longitude,
end_latitude,
end_longitude,
results
)
val s =
String.format("%.1f", results[0] / 1000)
//Setting marker to draw route between these two points
origin = MarkerOptions().position(LatLng(latitude, longitude))
.title("HSR Layout").snippet("origin")
destination =
MarkerOptions().position(LatLng(end_latitude, end_longitude))
.title(tf_location.text.toString())
.snippet("Distance = $s KM")
mMap!!.addMarker(destination)
mMap!!.addMarker(origin)
Toast.makeText(
this@MainActivity,
"Distance = $s KM",
Toast.LENGTH_SHORT
).show()
tvCurrentAddress!!.setText("Distance = $s KM")
//getting URL to the google Directions API
val url: String =
getDirectionsUrl(origin!!.getPosition(), destination!!.getPosition())!!
//val downloadTask = DownloadTask()
//start downloading the json data from google directions API
DownloadTask(url)
}
}
}
}
@Throws(IOException::class)
private fun downloadUrl(strUrl: String): String? {
var data = ""
var iStream: InputStream? = null
var urlConnection: HttpURLConnection? = null
try {
val url = URL(strUrl)
urlConnection = url.openConnection() as HttpURLConnection
urlConnection.connect()
iStream = urlConnection!!.inputStream
val br =
BufferedReader(InputStreamReader(iStream))
val sb = StringBuffer()
var line: String? = ""
while (br.readLine().also { line = it } != null) {
sb.append(line)
}
data = sb.toString()
br.close()
} catch (e: java.lang.Exception) {
Log.d("Exception", e.toString())
} finally {
iStream!!.close()
urlConnection!!.disconnect()
}
return data
}
//parsing into JSON format
private fun ParserTask (vararg jsonData: String?, result: List<List<HashMap<String, String>>>?): List<List<HashMap<String, String>>>? {
async {
val points = ArrayList<LatLng?>()
val lineOptions = PolylineOptions()
for (i in result!!.indices) {
val path =
result[i]
for (j in path.indices) {
val point = path[j]
val lat = point["lat"]!!.toDouble()
val lng = point["lng"]!!.toDouble()
val position = LatLng(lat, lng)
points.add(position)
}
lineOptions.addAll(points)
lineOptions.width(8f)
lineOptions.color(Color.RED)
lineOptions.geodesic(true)
}
if (points.size != 0) mMap!!.addPolyline(lineOptions)
}
val jObject: JSONObject
var routes: List<List<HashMap<String, String>>>? =
null
try {
jObject = JSONObject(jsonData[0])
val parser = DataParser()
routes = parser.parse(jObject)
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
return routes
}
private fun DownloadTask (vararg url: String?, result String): String {
async {
super.onPostExecute(result)
val parserTask = ParserTask()
parserTask.execute(result)
}
var data = ""
try{
data = downloadUrl(url[0].toString()).toString()
} catch (e: java.lang.Exception) {
Log.d("Background Task", e.toString())
}
return data
}
private fun getDirectionsUrl(origin: LatLng, dest: LatLng): String?{
//Origin of route
val str_origin = "origin=" + origin.latitude + "," + origin.longitude
//Destination of Route
val str_destination = "destination" + dest.latitude + "," + dest.longitude
//transportation mode
val mode = "mode=walking"
//building parameters of webservice
val parameters = "$str_origin&$str_destination&$mode"
//output format
val output = "json"
//building the url to the web service
return "https://maps.googleapis.com/maps/api/directions/$output?$parameters&key=AIzaSyCgraKSwPfUIyZLOmEDh_ptAbfRRAj7y1g"
}
public override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
//askPermissionLocation()
var mapViewBundle = outState.getBundle(MAP_VIEW_BUNDLE_KEY)
if (mapViewBundle == null){
mapViewBundle = Bundle()
outState.putBundle(MAP_VIEW_BUNDLE_KEY, mapViewBundle)
}
mapView.onSaveInstanceState(mapViewBundle)
}
/*private fun askPermissionLocation(){
askPermission(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
){
getCurrentLocation()
// mapView.getMapAsync(this@MyNavigationActivity)
}
}*/
private fun getCurrentLocation() {
fusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(this@MainActivity)
try {
@SuppressLint ("MissingPermission")
val location =
fusedLocationProviderClient!!.getLastLocation()
location.addOnCompleteListener(object : OnCompleteListener<Location> {
override fun onComplete(loc: Task<Location>) {
if (loc.isSuccessful) {
val currentLocation = loc.result as Location?
if (currentLocation != null) {
moveCamera(
LatLng(currentLocation.latitude, currentLocation.longitude),
DEFAULT_ZOOM
)
latitude = currentLocation.latitude
longitude = currentLocation.longitude
}
} else {
//askPermissionLocation()
}
}
})
} catch (se: Exception) {
Log.e("TAG", "Security Exception")
}
}
private fun moveCamera(latLng: LatLng, zoom: Float) {
// mMap!!.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, zoom))
}
// override fun onLocationChanged(location: Location?) {
// val geocoder = Geocoder (this, Locale.getDefault())
// var addresses: List<Address>? = null
// try {
// addresses = geocoder.getFromLocation(location!!.latitude, location.longitude, 1)
// } catch (e: IOException) {
// e.printStackTrace()
// }
// setAddress(addresses!![0])
// }
//
// private fun setAddress(addresses: Address) {
// if (addresses != null) {
//
// if (addresses.getAddressLine(0) != null) {
// tvCurrentAddress!!.setText(addresses.getAddressLine(0))
// }
// if (addresses.getAddressLine(1) != null) {
// tvCurrentAddress!!.setText(
// tvCurrentAddress.getText().toString() + addresses.getAddressLine(1)
// )
// }
// }
// }
//
//
//
// override fun onStatusChanged(p0: String?, p1: Int, p2: Bundle?) {
//
// }
//
// override fun onProviderEnabled(p0: String?) {
//
// }
//
// override fun onProviderDisabled(p0: String?) {
//
// }
//
// override fun onCameraMove() {
//
// }
//
// override fun onCameraMoveStarted(p0: Int) {
//
// }
//
// override fun onCameraIdle() {
// var addresses: List<Address>? = null
// val geocoder = Geocoder (this, Locale.getDefault())
// try {
// addresses = geocoder.getFromLocation( mMap!!.getCameraPosition().target.latitude, mMap!!.getCameraPosition().target.longitude, 1)
//
// setAddress(addresses!![0])
//
// } catch (e: IndexOutOfBoundsException) {
// e.printStackTrace()
// } catch (e: IOException) {
// e.printStackTrace()
// }
// }
}
Upvotes: 2
Views: 2277
Reputation: 93591
Don't do this: CoroutineScope by MainScope()
. This pattern is from a long time ago when coroutines were brand new and the Android framework didn't have proper coroutine scopes provided yet. You should launch your coroutines from the provided lifecycleScope
, or viewModelScope
if it's in a ViewModel. These provided coroutine scopes have proper lifecycles and cancel their current work automatically when the associated lifecycle object (e.g. the Activity) is destroyed to avoid leaking memory. This is one of the big benefits of using coroutines. Your AsyncTask implementations above are leaking memory when the Activity is destroyed. Proper use of an AsyncTask would involve cancelling any ongoing tasks in onDestroy()
, and is not trivial to do like it is with coroutines.
Don't do this:
fun someFunction() {
async {
//...
}
// some code that's supposed to happen after the asynchronous work
}
When you call async
, you are firing off an asynchronous coroutine. The code beneath the async
block will run on the current thread immediately, likely before the coroutine code inside the async
block. Instead, make your whole function a suspend
function, and wrap the long-running/blocking code in withContext
using an appropriate Dispatcher. This allows all the code in your function to run synchronously (top-to-bottom in order) without blocking the main thread.
For AsyncTasks that only utilized doInBackground
and onPostExecute
, here is how they can be converted into proper suspend functions:
suspend fun foo(/* The parameters you would have passed to AsyncTask constructor */) {
val backgroundResult = withContext(Dispatchers.Default) {
// The code you would have had in doInBackground.
// Last line of withContext lambda should evaluate to your result, what you would have
// returned in doInBackground.
}
withContext(Dispatchers.Main) {
// The code you would have had in onPostExecute. You can use the value of
// backgroundResult here.
}
}
Replace Dispatchers.Default
with Dispatchers.IO
if your background work is primarily IO-based (network or disk access) instead of CPU-based.
Since you have converted these tasks into suspend
functions, somewhere you will have to launch a coroutine to be able to call them. So you will launch a coroutine somewhere using lifecycleScope.launch { }
to call these. Just remember that launching a coroutine with launch
or async
fires it off asynchronously. It is only synchronous inside the coroutine. Alternatively, if you're only using these tasks from one spot and they don't have to return anything when they're finished, you could make them into regular functions that launch a coroutine internally. And if you are launching from lifecycleScope
, you don't need to worry about specifying Dispatchers.Main since it is the default of the scope.
fun foo(/* The parameters you would have passed to AsyncTask constructor */) {
lifecycleScope.launch {
val backgroundResult = withContext(Dispatchers.Default) {
// The code you would have had in doInBackground.
// Last line of withContext lambda should evaluate to your result, what you would have
// returned in doInBackground.
}
// The code you would have had in onPostExecute. You can use the value of
// backgroundResult here.
}
}
Upvotes: 2