A Better Callback Architecture For AppInventor
We have always been using Events
in builtin components as well as in extensions. They are quite great and for the most part get the job done. But I always had issues with such events in blockly based platforms. Usually I get frustrated when I have to make network requests, specially when using rest databases or reactive databases like firebase. Let’s talk about it.
Whats the issue?
Imagine you have to make multiple Api calls for reading, writing data to the database. Somewhere between those network calls, an error occurs and an event is dispatched. But you are not sure which call failed even though you may have access to error message and other details but still the issue remains and you don’t know what went wrong. And if another error occurs, you get even more confused. Moreover it gets difficult to handle errors properly since the only access point for errors is a single dispatched Event
.
Proposed solution
The proposed solution to this would be to pass callbacks to the functions being called. Below is a demonstration of the proposed solution:
In this loadPosts
function, we are passing an api url to load post. The next parameter onSuccess
is the callback function defined by you which will be called if data is loaded successfully. And the last parameter onError
is called if an error occurs during the network call.
loadTodos
has the same functionality as loadPosts
except it returns todos rather than posts and has respective callbacks to handle success and error states.
These are the callback functions that you define to handle success and failure states for each request you make. Inside these functions, you get access to data or error which you can then manipulate according to your needs.
How to pass functions if AppInventor doesn’t support them?
AppInventor doesn’t support passing functions as parameters. However you can pass function name which can then be used to trigger the callback using reflection. There is already an extension using function names to call functions from extension code.
Implementation in extensions
Implementing support for callbacks can be acieved using either Java
or Kotlin
by using linked files below:
Kotlin
package com.dreamers.listutils
import android.util.Log
import com.google.appinventor.components.runtime.Form
import com.google.appinventor.components.runtime.ReplForm
import com.google.appinventor.components.runtime.errors.IllegalArgumentError
import gnu.lists.LList
import gnu.mapping.ProcedureN
import gnu.mapping.SimpleSymbol
import gnu.mapping.Symbol
import kawa.standard.Scheme
class FunctionInvoker(private val form: Form) {
// Procedures are defined using the def syntax defined in runtime.scm on line 665. The def macro
// behaves differently in the REPL (Companion) versus a compiled app. The following two methods
// provide the appropriate lookup behavior depending on whether this extension is being used in
// the REPL environment or in the compiled environment.
private fun lookupProcedureInCompanion(procedureName: String): ProcedureN? {
val lang = Scheme.getInstance()
try {
// Since we're in the REPL, we can cheat and invoke the Scheme interpreter to get the method.
val result = lang.eval("(begin (require <com.google.youngandroid.runtime>)(get-var p$$procedureName))")
if (result is ProcedureN) {
return result
} else {
throwError(message = "Wanted a procedure, but got a ${result?.javaClass?.toString() ?: "null"}")
}
} catch (throwable: Throwable) {
throwError(throwable.message.toString(), throwable)
throwable.printStackTrace()
}
return null
}
private fun lookupProcedureInForm(procedureName: String): ProcedureN? {
try {
val globalVarEnvironment = form.javaClass.getField("global\$Mnvars\$Mnto\$Mncreate")
val vars = globalVarEnvironment[form] as LList
val procSym: Symbol = SimpleSymbol("p$$procedureName")
var result: Any? = null
for (pair in vars) {
if (LList.Empty != pair) {
val asPair = pair as LList
if ((asPair[0] as Symbol?)?.name == procSym.name) {
result = asPair[1]
break
}
}
}
if (result is ProcedureN) {
// The def syntax wraps the function definition in an additional lambda, which we evaluate
// here so that the return value of this is the lambda implementing the blocks logic.
// See runtime.scm#665
return result.apply0() as ProcedureN
} else {
throwError("Wanted a procedure, but got a ${result?.javaClass?.toString() ?: "null"}")
}
} catch (throwable: Throwable) {
throwError(throwable.message.toString(), throwable)
throwable.printStackTrace()
}
return null
}
fun invoke(procedureName: String, arguments: List<Any?>?): Any? {
val procedure = lookupProcedure(procedureName)
return call(procedure, arguments)
}
fun lookupProcedure(procedureName: String): ProcedureN {
val procedure = if (form is ReplForm) {
lookupProcedureInCompanion(procedureName)
} else {
lookupProcedureInForm(procedureName)
}
return procedure ?: throw IllegalArgumentError("Unable to locate procedure $procedureName in form $form")
}
fun call(procedure: ProcedureN, arguments: List<Any?>?): Any? {
return try {
if (arguments == null || procedure.numArgs() == 0) {
procedure.apply0()
} else {
val argArray = arrayOfNulls<Any>(arguments.size)
var i = 0
val it: Iterator<*> = arguments.iterator()
while (it.hasNext()) {
argArray[i++] = it.next()
}
procedure.applyN(argArray)
}
} catch (throwable: Throwable) {
throwError("an unknown error occurred", throwable)
throwable.printStackTrace()
}
}
private fun throwError(message: String, throwable: Throwable? = null) {
Log.e("ListUtils", "Function Invoker : $message", throwable)
}
}
Java
Example Usage
// Initialize function invoker at start
private val functionInvoker = FunctionInvoker(form)
fun GetData(url: String, onSuccess: String, onError: String) {
try {
// perform api call here...
val sampleData = listOf("Post1", "Post2")
functionInvoker.invoke(onSuccess, listOf(sampleData))
} catch (e: Exception) {
functionInvoker.invoke(onError, listOf(e.message))
}
}
Caveats with this approach
As you can clearly see that this can be useful while working with components and extensions where you have to call a function multiple times and depend on the result. There are some advantages to this approach listed below:
- It is difficult to get started using callbacks.
- Not beginners friendly.
- Requires users to pass function name manually which can cause unexpected errors if the spellings are not correct.
- Requires users to know the parameters and their sequence in order to use callbacks.
- As it is not builtin to the platform, its difficult to get started with it.