Eoin Mcdonald
Eoin Mcdonald

Reputation: 31

Using coroutines in Kotlin to replace AsyncTask, Building a google maps Kotlin android app and I cant implement coroutines instead of AsyncTask

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:

Before, Using AsyncTask

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
        }

        }

My attempt at coroutines

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

    }

My Whole activity of what Im trying to build for reference

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

Answers (1)

Tenfour04
Tenfour04

Reputation: 93591

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

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

Related Questions