In Capacitor you can create your own plugins. Sometimes you need to create a plugin method which resolves the multiple times, e.g. for geolocation.
To return values from a plugin method repeatedly, you need to:
Write annotation #PluginMethod(returnType = PluginMethod.RETURN_CALLBACK)
Invoke PluginCall.keepAlive = true before any PluginCall.resolve()/PluginCall.reject()
Invoke PluginCall.resolve() multiple times.
When you want to release the call, you need to invoke PluginCall.release(Bridge).
But in the native-bridge.ts there is returnResult with some logic for handling responses from Native to the UI, where there is the following if statement:
const returnResult = (result: any) => {
const storedCall = callbacks.get(result.callbackId);
...
if (result.save === false) {
callbacks.delete(result.callbackId);
}
...
};
Problem is if you just invoke PluginCall.release(Bridge) you do not clear the saved UI callback, because returnResult is not invoked again with the result.save === false check. There is a memory leak, isn't it?
#PluginMethod(returnType = PluginMethod.RETURN_CALLBACK)
fun echo(call: PluginCall) {
call.keepAlive = true
call.resolve(some useful data...)
call.resolve(some useful data...)
call.release(this.bridge) // keepAlive = false and Bridge.releaseCall(id) under the hood
// native-bridge.ts still holds the callback from UI
}
Workaround is to invoke call.resolve() with any data after the call is released, for example:
#PluginMethod(returnType = PluginMethod.RETURN_CALLBACK)
fun echo(call: PluginCall) {
call.keepAlive = true
call.resolve(some useful data...)
call.resolve(some useful data...)
call.release(this.bridge) // keepAlive = false and Bridge.releaseCall(id) under the hood
call.resolve() // redundant resolve() is needed to avoid a memory leak
}
But it looks like so weird.
How to properly release the PluginCall to avoid memory leaks?
Related
I have a question... sometimes, I need to get data from ViewModel directly. For example, Let's say there's a isChecked() method in ViewModel. And I want to use it in the if condition.
if(viewModel.isChecked()){
// TODO:
}
So, what I am doing right now is:
fun isChecked(): Boolean = runBlocking {
val result = dbRepo.getData()
val response = apiRepo.check(result)
return response.isSuccessful
}
It uses runBlocking. So, it runs on MainThread. I don't think it's a good way because it can freeze the screen. But yes, if the condition needs to run, it needs to wait it until it gets the data from DB and Network.
Another way that I can think of is using LiveData. However, I can't use it in the condition. So, I needs to move the condition in the observer block. But sometimes, this can't be done because there can be something before the condition. And it doesn't seem to look direct but writing code here and there and finally get that data.
So, Is there any simpler way than this?
Your best bet if you have something slow or blocking like that is to rethink how you are using the data entirely. Instead of trying to return it, use LiveData or callbacks to handle the response asynchronously without causing your UI to hang or become laggy. In these cases you really only have three options:
Use a callback to handle when the response is received
Use observable data like LiveData to handle when the response is received
Change the method to a suspend function and call it from a coroutine
Forcing a method to wait to return on the main thread without using one of these is going to cause the app to hang.
Callback to get state
It's hard to say definitely what the best solution for you is without more details about how you are using isChecked(), but one pattern that could work would be to use a callback to handle what you were formerly putting in the if statement, like this (in the ViewModel):
fun getCheckedState(callback: (Boolean)->Unit) {
viewModelScope.launch {
// do long-running task to get checked state,
// using an appropriate dispatcher if needed
val result = dbRepo.getData()
val response = apiRepo.check(result)
// pass "response.isSuccessful" to the callback, to be
// used as "isChecked" below
callback(response.isSuccessful)
}
}
You would call that from the activity or fragment like this:
viewModel.getCheckedState { isChecked ->
if( isChecked ) {
// do something
}
else {
// do something else
}
}
// CAUTION: Do NOT try to use variables you set inside
// the callback out here!
A word of caution - the code inside the callback you pass to getCheckedState does not run right away. Do not try to use things you set inside there outside the callback scope or you fall into this common issue
Simpler Callback
Alternately, if you only want to run some code when isChecked is true, you could simplify the callback like this
fun runIfChecked(callback: ()->Unit) {
viewModelScope.launch {
// do long-running task to get checked state,
// using an appropriate dispatcher if needed
val result = dbRepo.getData()
val response = apiRepo.check(result)
// only call the callback when it's true
if( response.isSuccessful ) {
callback()
}
}
}
and call it with
viewModel.runIfChecked {
// do something
}
// Again, don't try to use things from the callback out here!
Use lifecyclescope.launch(Dispatcher.IO) instead of runblocking
Try this code on your ViewModel class:
suspend fun isChecked(): Boolean {
val response: Response? = null
viewModelScope.launch(Dispatchers.IO) {
val result = dbRepo.getData()
response = apiRepo.check(result)
}.join()
return response?.isSuccessful
}
From Activity:
// Suppose you have a button
findViewById<Button>(R.id.btn).setOnClickListener({
CoroutineScope(Dispatchers.Main).launch {
if (viewModel.isChecked()) {
Log.d("CT", "Do your others staff")
}
}
})
Hope it work file. If no let me comment
I have still a little bit of trouble putting all information together about the thread-safety of using coroutines to launch network requests.
Let's say we have following use-case, there is a list of users we get and for each of those users, I will do some specific check which has to run over a network request to the API, giving me some information back about this user.
The userCheck happens inside a library, which doesn't expose suspend functions but rather still uses a callback.
Inside of this library, I have seen code like this to launch each of the network requests:
internal suspend fun <T> doNetworkRequest(request: suspend () -> Response<T>): NetworkResult<T> {
return withContext(Dispatchers.IO) {
try {
val response = request.invoke()
...
According to the documentation, Dispatchers.IO can use multiple threads for the execution of the code, also the request function is simply a function from a Retrofit API.
So what I did is to launch the request for each user, and use a single resultHandler object, which will add the results to a list and check if the length of the result list equals the length of the user list, if so, then all userChecks are done and I know that I can do something with the results, which need to be returned all together.
val userList: List<String>? = getUsers()
val userCheckResultList = mutableListOf<UserCheckResult>()
val handler = object : UserCheckResultHandler {
override fun onResult(
userCheckResult: UserCheckResult?
) {
userCheckResult?.let {
userCheckResultList.add(
it
)
}
if (userCheckResultList.size == userList?.size) {
doSomethingWithResultList()
print("SUCCESS")
}
}
}
userList?.forEach {
checkUser(it, handler)
}
My question is: Is this implementation thread-safe? As far as I know, Kotlin objects should be thread safe, but I have gotten feedback that this is possibly not the best implementation :D
But in theory, even if the requests get launched asynchronous and multiple at the same time, only one at a time can access the lock of the thread the result handler is running on and there will be no race condition or problems with adding items to the list and comparing the sizes.
Am I wrong about this?
Is there any way to handle this scenario in a better way?
If you are executing multiple request in parallel - it's not. List is not thread safe. But it's simple fix for that. Create a Mutex object and then just wrap your operation on list in lock, like that:
val lock = Mutex()
val userList: List<String>? = getUsers()
val userCheckResultList = mutableListOf<UserCheckResult>()
val handler = object : UserCheckResultHandler {
override fun onResult(
userCheckResult: UserCheckResult?
) {
lock.withLock {
userCheckResult?.let {
userCheckResultList.add(
it
)
}
if (userCheckResultList.size == userList?.size) {
doSomethingWithResultList()
print("SUCCESS")
}
}
}
}
userList?.forEach {
checkUser(it, handler)
}
I have to add that this whole solution seems very hacky. I would go completely other route. Run all of your requests wrapping those in async { // network request } which will return Deferred object. Add this object to some list. After that wait for all of those deferred objects using awaitAll(). Like that:
val jobs = mutableListOf<Job>()
userList?.forEach {
// i assume checkUser is suspendable here
jobs += async { checkUser(it, handler) }
}
// wait for all requests
jobs.awaitAll()
// After that you can access all results like this:
val resultOfJob0 = jobs[0].getCompleted()
I have an issue with my android app. I'm fairly new with it and have some issues with finding the correct documentation for a asynchronous. I'm using the kohttp library to help me a bit.
The thing is, you can't run this on the main UI thread so I want to make this request Async. I can't find a clear reference in the documentation and I don't really know how to do this in plain Kotlin.
This is what I come up with; in a separate class named LoginCall. I tried other answers, this however didn't result in success. How can I run this on a new thread and still use the response?
class LoginCall {
fun callLoginRequest(a:String, b:String): Any {
val response: Response = httpPost {
host = "XXX"
path = "XXX"
param { }
header { }
body {
form {
"email" to a
"password" to b
}
}
}
return response
}
}
There are many ways to achieve this, if you're using android as the underlying platform, you can use the native component called AsyncTask a good SO post on how to use it.
If you wish to leverage kotlin as a language and the features provided by it, you can try using coroutines ref.
Personally, i would recommend coroutines, it simplifies exception and error handling, also prevents callback hell.
here's a sample of the same code in a coroutine,
// global
private val mainScope = CoroutineScope(Dispatchers.MAIN + SupervisorJob())
// inside a method
mainScope.launch{
withContext(Dispatchers.IO){
// do your async task here, as you can see, you're doing this in an IO thread scope.
}
}
Also, you can create an issue to implement asyncHttpPost.
Since kohttp 0.10.0 you can use async methods for such cases. You can try them.
Code example:
suspend fun callLoginRequest(a:String, b:String): Any {
val response: Differed<Response> = httpPostAsync {
host = "XXX"
path = "XXX"
param { }
header { }
body {
form {
"email" to a
"password" to b
}
}
}
// for further usage in coroutines
return response.await()
}
And call this function from coroutine
Google is putting its Android API for accessing Google services (i.E. Google Drive) to rest and is replacing it with REST.
And while there is a 'migration guides', it fails to build a APK package ready for installation, because of 'Duplicate Class definition' or something.
For some reason it is incredibly hard to find some comprehensive information about how to access a Google Service using REST via Android (preferably using methods natively available to the OS).
After a lot of searching, puzzling, scratching my head, occasional swearing and a lot of learning about things I really didn't want to care about, I'd like to share a few pieces of code, that are actually working for me.
Disclaimer: I'm a rookie Android programmer (who really doesn't how to pick his battles), so if there are things in here, that have the real Android wizards shaking their heads, I hope you'll forgive me.
All code samples are written in Kotlin and Android Studio.
Worth noting: Only the 'application data folder' is queried in this little tutorial, you will need to adjust the requested scopes if you want to do something else.
Necessary preparations
Create a project and an OAuth key for your application as described here. Many of the information I gathered for authorization came from that place, so expect to find some similarities.
The Dashboard for your project may be found at https://console.developers.google.com/apis/dashboard
Add implementation "com.google.android.gms:play-services-auth:16.0.1" to your applications gradle file. This dependency will be used for authentication purposes.
Add 'internet' support to your applications manifest
<uses-permission android:name="android.permission.INTERNET"/>
Authenticating
The beginning of our journey is the authentication.
For this purpose, I used the GoogleSignIn Framework.
Create an activity (or use your main activity, your choice) and override the onActivityResult method there.
Add a block like this:
if (requestCode == RC_SIGN_IN) {
GoogleSignIn.getSignedInAccountFromIntent(data)
.addOnSuccessListener(::evaluateResponse)
.addOnFailureListener { e ->
Log.w(RecipeList.TAG, "signInResult:failed =" + e.toString())
evaluateResponse(null)
}
}
RC_REQUEST_CODE is an arbitrarily chosen ID value defined in the companion object as constant.
Once you want to perform authentication (i.E. by clicking of a button), you will need to start the activity we have just declared the callback for.
For this purpose, you need to prepare the authentication request first.
GoogleSignIn.getClient(this, GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken("YourClientIDGoesHere.apps.googleusercontent.com")
.requestScopes(Scope(Scopes.DRIVE_APPFOLDER))
.build())
This request gives you a client object you can start using straight away by calling.
startActivityForResult(client.signInIntent, RC_SIGN_IN)
This call will cause the authorization screen to pop up (if necessary), allow the user to select an account and then close itself again, passing the data to onActivityResult
To fetch the previously signed in user (without starting a new activity), you can also use the GoogleSignIn.getLastSignedInAccount(this); method in the background.
On failure either of these methods return null, so be ready to deal with that.
Now that we have an authenticated user, what do we do with it?
We ask for an auth token.
Right now, we only have an idToken in our account object, which is absolutely useless for what we want to do, because it doesn't allow us to call the API.
But Google comes to the rescue once more and supplies us with the GoogleAuthUtil.getToken(this, account.account, "oauth2:https://www.googleapis.com/auth/drive.appdata") call.
This call will forward the account information and return a String if all goes right: The auth token we need.
To be noted: This method performs a network request, meaning that it will throw up in your face, if you attempt to execute it in your UI thread.
I created a helper class which mimics the behavior (and API) of Googles 'Task' object, which takes care of the nitty gritty of calling a method on a thread and notifying the calling thread that it is done.
Save the auth token somewhere you can find it again, authorization is (finally) done with.
Querying the API
This part is far more straightforward than the previous one and goes hand in hand with the Google Drive REST API
All network requests need to be executed on a 'non-UI' thread, which is why I wrapped them up in my helper class to notify me once there is data to display.
private fun performNet(url: String, method: String, onSuccess: (JSONObject) -> Unit)
{
ThreadedTask<String>()
.addOnSuccess { onSuccess(JSONObject(it)) }
.addOnFailure { Log.w("DriveSync", "Sync failure $it") }
.execute(executor) {
val url = URL(url)
with (url.openConnection() as HttpURLConnection)
{
requestMethod = method
useCaches = false
doInput = true
doOutput = false
setRequestProperty("Authorization", "Bearer $authToken")
processNetResponse(responseCode, this)
}
}
}
private fun processNetResponse(responseCode: Int, connection: HttpURLConnection) : String
{
var responseData = "No Data"
val requestOK = (responseCode == HttpURLConnection.HTTP_OK)
BufferedReader(InputStreamReader(if (requestOK) connection.inputStream else connection.errorStream))
.use {
val response = StringBuffer()
var inputLine = it.readLine()
while (inputLine != null) {
response.append(inputLine)
inputLine = it.readLine()
}
responseData = response.toString()
}
if (!requestOK)
throw Exception("Bad request: $responseCode ($responseData)")
return responseData
}
This block of code is a rather generic helper function I put together from various sources and essentially just takes the URL to query, the method to perform (GET, POST, PATCH, DELETE) and constructs a HTTP request from it.
The auth token we got earlier during the authorization is passed as a header to the request to authenticate and identify ourselves as 'the user' to Google.
Google will, if everything is OK, reply with HTTP_OK (200) and onSuccess will be called, which will translate the JSON reply to a JSONObject, which will then be passed to the evaluation function we registered earlier.
Fetching the list of files
performNet("https://www.googleapis.com/drive/v3/files?spaces=appDataFolder", "GET")
The spaces parameter serves to tell Google, that we don't want to see the root folder but the application data folder. Without this parameter, the request would fail, because we only requested access to the appDataFolder.
The response should contain a JSONArray under the files key, which you then can parse and draw whatever information you want.
The ThreadTask class
This helper class encapsulates the steps necessary to perform an operation on a different context and perform a callback on the instantiating thread upon completion.
I am not claiming that this is THE way to this, it's just my 'Simply doesn't know any better'-way.
import android.os.Handler
import android.os.Looper
import android.os.Message
import java.lang.Exception
import java.util.concurrent.Executor
class ThreadedTask<T> {
private val onSuccess = mutableListOf<(T) -> Unit>()
private val onFailure = mutableListOf<(String) -> Unit>()
private val onComplete = mutableListOf<() -> Unit>()
fun addOnSuccess(handler: (T) -> Unit) : ThreadedTask<T> { onSuccess.add(handler); return this; }
fun addOnFailure(handler: (String) -> Unit) : ThreadedTask<T> { onFailure.add(handler); return this; }
fun addOnComplete(handler: () -> Unit) : ThreadedTask<T> { onComplete.add(handler);return this; }
/**
* Performs the passed code in a threaded context and executes Success/Failure/Complete handler respectively on the calling thread.
* If any (uncaught) exception is triggered, the task is considered 'failed'.
* Call this method last in the chain to avoid race conditions while adding the handlers.
*
*/
fun execute(executor: Executor, code: () -> T)
{
val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
publishResult(msg.what, msg.obj)
}
}
executor.execute {
try {
handler.obtainMessage(TASK_SUCCESS, code()).sendToTarget()
} catch (exception: Exception) {
handler.obtainMessage(TASK_FAILED, exception.toString()).sendToTarget()
}
}
}
private fun publishResult(returnCode: Int, returnValue: Any)
{
if (returnCode == TASK_FAILED)
onFailure.forEach { it(returnValue as String) }
else
onSuccess.forEach { it(returnValue as T) }
onComplete.forEach { it() }
// Removes all handlers, cleaning up potential retain cycles.
onFailure.clear()
onSuccess.clear()
onComplete.clear()
}
companion object {
private const val TASK_SUCCESS = 0
private const val TASK_FAILED = 1
}
}
The order of execution is important in this case.
You first need to add the callbacks to the class object and at the end you need to call execute and supply it with the executor you want to run the thread with and of course the code you want to execute.
It is not everything you can do with Google Drive, but it's a start and I hope this little compilation will save someone else some grief in the future.
I am quite frustrated at the amount documentation that says explicitly you should use asynchronous code for networking.
I even read an article which outright states 'Networking is an inherently asynchronous operation.'
https://react-native.org/doc/network.html
So first there is a difference between background processing and asynchronous code.
For example, running code asynchronously doesn't necessarily mean it is on the background. For this we can actually use a background thread.
When you write an iOS app, and you have several view controllers, each accessing the same data which is downloaded by the Model, when you download the data for code asynchronously, you have a frustrating tangle of callbacks and asynchronous messages being passed throughout the application.
When I have multiple view controllers using the same data this poses a problem, how do I make sure I don't have the view controller which is opened accessing the data before it is downloaded? You might not be able to tell which controller was opened first so this poses an issue, how do you make sure they don't access the data until it is finished downloading?
I suppose you solve this using a completion handler and a model which then fires off a Key Value Observing notification calling the controller when it is finished downloading (a push model).
But what happens if the said controller is not loaded when the notification is posted, does this mean it never gets the data? Wouldn't it make more sense to use a pull model, so when the controller is loaded it can check if the data is available, if so how do you handle this with an async paradigm?
But any notification callback cannot access the outlying scope of the rest of the controller.
However I have written some entirely synchronous code which uses locks and semaphores. The model downloads the data synchronously in a background thread. The controller classes (if they are loaded) check the Model class to see if the data is available. Locks mean that the code cannot access the data if the data is not downloaded. The App signals when the data has finished downloading and while all controllers and models use the same shared synchronous DispatchQueue this prevents the controllers accessing the data arrays when they are empty or the data is being downloaded.
Async code commonly produces weakly coupled code that can't access the scope of the rest of the class and you have methods firing at different times and in different places in the app which i think is difficult to keep track of. So why is networking 'an inherently asynchronous operation'?
Can anyone provide sound scientific reasons why asynchronous code is better, or reasons why I should not do what I have done with synchronous code, and also methods of how you can make asynchronous code safer, less spaghetti like, easier to work with and easier to read?
CODE:
Table View Controller
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !fromSelectionCtrllr {
let downloader = Downloader.sharedInstance
let group = downloader.group
group.notify(queue: .main, execute: {
let defaults : UserDefaults = UserDefaults.standard
let firstLaunch = defaults.bool(forKey: "firstLaunch")
if firstLaunch {
self.arrayOfData = Model.sharedInstance.provideData()
} else {
self.arrayOfData = Model.sharedInstance.provideNewData()
}
for object in self.arrayOfData {
if let deviceName = object.chargeDeviceName {
let theSubscript = deviceName.prefix(1)
let theString = String(theSubscript)
if !self.sectionTitles.contains(theString) {
self.sectionTitles.append(theString)
}
} else {
self.sectionTitles.append("")
}
if let deviceName = object.chargeDeviceName {
let string = String(describing: deviceName.prefix(1))
var arry = self.chargingPointDict[string]
if arry == nil {
arry = []
}
arry?.append(object)
self.chargingPointDict.updateValue(arry!, forKey: string)
} else {
self.chargingPointDict[" "]?.append(object)
}
}
self.sectionTitles = self.removeDuplicates(array: self.sectionTitles)
self.sectionTitles = self.sectionTitles.sorted( by: { $0 < $1 })
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.myTableView.reloadData()
}
})
}
fromSelectionCtrllr = false
}
CellForRowAtIndexPath
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
if (searchController.searchBar.text?.isEmpty)! {
if self.sectionTitles.isEmpty {
cell.textLabel?.text = "Nothing to display"
return cell
} else {
let mySectionIndex = self.sectionTitles[indexPath.section]
if mySectionIndex != "" {
let arrayOfPoints : [ChargingPoint] = self.chargingPointDict[mySectionIndex]!
let object : ChargingPoint = arrayOfPoints[indexPath.row]
cell.textLabel?.text = object.chargeDeviceName
return cell
} else {
return cell
}
}
} else {
let object : ChargingPoint = self.filteredPoints[indexPath.row]
cell.textLabel?.text = object.chargeDeviceName
return cell
}
}
Model Class
class Model: NSObject {
var currentChargingPointArray : [ChargingPoint] = []
var newChargingPointArray : [ChargingPoint] = []
var latitude : Double?
var longitude : Double?
var annotationArray : [ChargingPointAnnotation] = []
var newAnnotationArray : [ChargingPointAnnotation] = []
static let downloader = Downloader.sharedInstance
var savedRegion : MKCoordinateRegion? = nil
/* The model class is a singleton */
static let sharedInstance : Model = {
let instance = Model()
return instance
}()
fileprivate override init( ) {} //This prevents others from using the default '()' initializer for this class.
func setLocation(lat: Double, long: Double) {
self.latitude = lat
self.longitude = long
}
func returnData(array: Array<ChargingPoint>) {
currentChargingPointArray = []
var seen = Set<String>()
var unique = [ChargingPoint]()
for point in array {
if !seen.contains(point.chargeDeviceId!) {
unique.append(point)
seen.insert(point.chargeDeviceId!)
}
}
currentChargingPointArray = unique
}
func returnNewData(array: Array<ChargingPoint>) {
newChargingPointArray = []
var seen = Set<String>()
var unique = [ChargingPoint]()
for point in array {
if !seen.contains(point.chargeDeviceId!) {
unique.append(point)
seen.insert(point.chargeDeviceId!)
}
}
newChargingPointArray = unique
}
func provideData() -> [ChargingPoint] {
return currentChargingPointArray
}
func provideNewData() -> [ChargingPoint] {
return newChargingPointArray
}
func makeAnnotations() -> [ChargingPointAnnotation] {
let queue = DispatchQueue(label: "com.jackspacie.ChargeFinder", qos: .background, attributes: [])
queue.sync {
self.annotationArray = []
for chargingPoint in currentChargingPointArray {
let location = CLLocationCoordinate2D( latitude: chargingPoint.latitude!, longitude: chargingPoint.longitude!)
let annotation = ChargingPointAnnotation(location: location)
annotation?.title = chargingPoint.chargeDeviceName
annotation?.pointTitle = chargingPoint.chargeDeviceName
annotation?.chargingPoint = chargingPoint
self.annotationArray.append(annotation!)
}
}
return self.annotationArray
}
func makeNewAnnotations() -> [ChargingPointAnnotation] {
let queue = DispatchQueue(label: "com.jackspacie.ChargeFinder", qos: .background, attributes: [])
queue.sync {
self.newAnnotationArray = []
for chargingPoint in newChargingPointArray {
let location = CLLocationCoordinate2D( latitude: chargingPoint.latitude!, longitude: chargingPoint.longitude!)
let annotation = ChargingPointAnnotation(location: location)
annotation?.title = chargingPoint.chargeDeviceName
annotation?.pointTitle = chargingPoint.chargeDeviceName
annotation?.chargingPoint = chargingPoint
self.newAnnotationArray.append(annotation!)
}
}
return self.newAnnotationArray
}
Downloader Class
var group = DispatchGroup()
var model = Model.sharedInstance
/* The downloader class is a singleton */
static let sharedInstance : Downloader = {
let instance = Downloader()
return instance
}()
fileprivate override init() {} //This prevents others from using the default '()' initializer for this class.
func download(lat: Double, long: Double, dist: Int) {
func recursive(lat: Double, long: Double, dist: Int) {
var chargeDeviceArray : [ChargingPoint] = []
let url = URL(string: “https://www.blah.com/lat/\(lat)/long/\(long)/dist/\(dist)/")!
let semaphore = DispatchSemaphore(value: 0)
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if error != nil {
print("urlSession Error")
recursive(lat: lat, long: long, dist: dist)
return
} else {
guard let unwrappedData = data else { return }
do {
let jsonDict : [String: Any] = try JSONSerialization.jsonObject(with: unwrappedData, options: [] ) as! [String : Any]
let arrayOfDicts = jsonDict["ChargeDevice"] as? [[String: Any]]
for value in arrayOfDicts! {
let chargePoint = ChargingPoint()
// process data into objects.
chargeDeviceArray.append(chargePoint)
}
var seen = Set<String>()
var unique = [ChargingPoint]()
for point in chargeDeviceArray {
if !seen.contains(point.chargeDeviceId!) {
unique.append(point)
seen.insert(point.chargeDeviceId!)
}
}
if self.model.currentChargingPointArray.isEmpty {
self.model.returnData(array: unique)
} else {
self.model.returnNewData(array: unique)
}
} catch {
print("json error: \(error)")
}
semaphore.signal()
}
//print(response)
}
task.resume()
semaphore.wait(timeout: .distantFuture)
}
self.group.enter()
let queue = DispatchQueue(label: "com.myapp.charge”, qos: .background, attributes: [])
queue.sync {
recursive(lat: lat, long: long, dist: dist)
}
self.group.leave()
}
In short, when it comes to synchronous networking there are very limited cases where you would prefer this. Why?
Because 'A synchronous request blocks the client until operation completes' and in general you don't want the client to freeze. The user can't do anything at that time since we are waiting for all the synchronous actions to complete before enabling the client.
When using asynchronous requests you can build UI, show spinners and depending on whether you already cached older data or have other functionalities, still let the user use your client.
When I have multiple view controllers using the same data this poses a problem, how do I make sure I don't have the view controller which
is opened accessing the data before it is downloaded? You might not be
able to tell which controller was opened first so this poses an issue,
how do you make sure they don't access the data until it is finished
downloading?
I suppose you solve this using a completion handler and a model which
then fires off a Key Value Observing notification calling the
controller when it is finished downloading (a push model).
But I ask this, what happens if the said controller is not loaded when
the notification is posted, does this mean it never gets the data?
Wouldn't it make more sense to use a pull model, so when the
controller is loaded it can check if the dat is available, if so how
do you handle this with an async paradigm?
Load UI-> Show Progress Dialog -> Perform ASYNC(LoadData,SetData intoController)-> DismissDialog
Idk if this is what you're refering
I really dont see an oportunity where you will prefer block UI Thread and freeze App until it loads the data synchronically
The article states that "networking is an inherently asynchronous operation" because networking is an inherently asynchronous operation.
Synchronous means "happening at the same time". Specifically in computing it means with reference to a specific time period or clock signal. For example, electrically, the CPU operates synchronously, which is why we talk about the clock speed of a computer.
Synchronous does not mean "blocking" although that is a common (mis)interpretation. In fact, Apple hasn't helped here with their function names. Technically they should be something like DispatchQueue.nonBlockingOperation() and DispatchQueue.blockingOperation() rather than async/sync.
Synchronous systems can operate at much higher speeds but require a very controlled environment, which is why you find synchronous operation at the core of a computer but not so much outside it.
Your code blocks a background queue waiting for the download to complete, but the download still completes asynchronously. If it were completing synchronously you wouldn't need the semaphore. You would know that the data would be available at the specified point in time (say 0.2 seconds from now or whatever).
Your Downloader class also still notifies the view controller about the available data asynchronously via the dispatch group notify.
From what I can see, you have added a bunch of complexity to the solution (By adding a dispatch group and a semaphore) and introduced the potential for deadlocks but you still have asynchronous code. You block the background queue waiting for the completion of the download, but you still notify the data consumers asynchronously.
The same outcome could be achieved using the standard delegation or completion handler patterns with much less complexity. If there are potentially multiple parties interested in knowing about the new data then you can use the NotificationCentre to "broadcast" that information.
By the way, loosely coupled code is generally considered more desirable.