motion-async
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.
Setup
Gemfile:
gem "motion-async"
then run bundle
on the command line.
Usage
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
end
You can also include MotionAsync
to have access to the async
function without the module prefix:
include MotionAsync
...
async do
end
async
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
: whenever progress
is 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
end.on(:completion) do |result|
end
is the same as this:
async(
completion: -> (result) {
}
) do
end
To avoid the awkward syntax of the latter example, you can use the :background
option to specify the async code:
async(
background: -> {
},
completion: -> (result) {
}
)
Examples
Run a block of code in the background:
async do
end
Specify 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}"
end
Alternate syntax for the same example:
async do
some_expensive_calculation()
end.on(:completion) do |result|
p "The result was #{result}"
end
Progress Indicators
For 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|
task.progress i
end
end.on(:progress) do |progress_value|
p "Progress: #{progress_value + 1}% complete"
end
Chaining
Calls to on
are chainable:
async do |task|
100.times do |i|
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
Other Callbacks
:pre_execute
is invoked before the async operation begins and :cancelled
is called if the task is cancelled.
async do
end.on(:pre_execute) do
p "About to run a long operation"
end.on(:cancelled) do
p "Operation cancelled."
end
Canceling a Task
async
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
...
def onStop
@async_task.cancel(true)
end
Other Task States
task.pending?
task.running?
task.finished?
Delaying Execution
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"
end
This works fine for relatively short delays (a few seconds at most), but you'd probably want to use a Handler for anything longer.
Development
Tests
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.
Credits
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.