motion-async is a gem for RubyMotion Android that provides a friendly Ruby wrapper around Android's AsyncTask:
AsyncTask enables proper and easy use of the UI thread. This class allows [you] to perform background operations and publish results on the UI thread without having to manipulate threads and/or handlers.
AsyncTask is designed to be a helper class around Thread and Handler and does not constitute a generic threading framework. AsyncTasks should ideally be used for short operations (a few seconds at the most.)
AsyncTask must be loaded on the UI thread, and must only be executed once. See the documentation for more details.
Gemfile:
gem "motion-async"then run bundle on the command line.
The main entry point is the MotionAsync.async function, which creates, and then executes the async code with the options you provide (see below for details).
MotionAsync.async do
# some long operation
endYou can also include MotionAsync to have access to the async function without the module prefix:
include MotionAsync
...
async do
# some long operation
endasync takes a block, which is the code that should be executed in the background. You can optionally specify callback blocks that are run at various points during the tasks's lifecycle:
:pre_execute: before the background task is executed:completion: when the task finishes:progress: wheneverprogressis called on the task object:cancelled: if the task is cancelled
These callbacks can be added with the on method, or passed in as options to async.
This:
async do
# some long operation
end.on(:completion) do |result|
# process result
endis the same as this:
async(
completion: -> (result) {
# process result
}
) do
# some long operation
endTo avoid the awkward syntax of the latter example, you can use the :background option to specify the async code:
async(
background: -> {
# some long operation
},
completion: -> (result) {
# process result
}
)Run a block of code in the background:
async do
# some long operation
endSpecify a block to execute when the operation completes. The return value of the async block is passed in as a parameter:
task = async do
some_expensive_calculation()
end
task.on :completion do |result|
p "The result was #{result}"
endAlternate syntax for the same example:
async do
some_expensive_calculation()
end.on(:completion) do |result|
p "The result was #{result}"
endFor progress updates, provide a :progress block, and periodically call #progress on the task object in the background block. The :progress block is executed on the main thread.
async do |task|
100.times do |i|
# do some work
task.progress i
end
end.on(:progress) do |progress_value|
p "Progress: #{progress_value + 1}% complete"
endCalls to on are chainable:
async do |task|
100.times do |i|
# do some work
task.progress i
end
end.on(:progress) do |progress_value|
p "Progress: #{progress_value + 1}% complete"
end.on(:completion) do |result|
p "The result was #{result}"
end:pre_execute is invoked before the async operation begins and :cancelled is called if the task is cancelled.
async do
# long operation
end.on(:pre_execute) do
p "About to run a long operation"
end.on(:cancelled) do
p "Operation cancelled."
endasync returns a reference to the task object (a subclass of AsyncTask); you can hold on to this
in case you want to cancel it later. You can see if a task has been cancelled by calling
cancelled? The Android docs recommend checking this value periodically during task execution
so you can exit gracefully.
@async_task = async do |task|
image_urls.each do |image_url|
images << load_image(image_url)
break if task.cancelled?
end
end
...
# e.g. in an Activity or Fragment
def onStop
@async_task.cancel(true) # passing in true indicates that the task should be interrupted
endtask.pending?
task.running?
task.finished?The after method works just like async, but takes a float as its first parameter to specify the number of seconds to delay before executing the async block:
after(2) do
p "This won't happen for another 2 seconds"
endThis works fine for relatively short delays (a few seconds at most), but you'd probably want to use a Handler for anything longer.
It's a little tricky to test background threads in a unit test context. I went through a number of blog posts and SO questions, but never could manage to get it to work.
So, we've got a few tests in main_spec.rb and then a bunch in main_activity.rb which are run simply by running the app in this codebase via rake. I'm not especially proud of this, but figured it was better than nothing. If anyone can show me a better way, I'd love to see it.
Many, many thanks to Todd Werth and Jamon Holmgren for helping to define the API. This is a much better gem than it would have been without their input.