Rhodes

From Rhomobile

(Redirected from Rhodes Specification)
Jump to: navigation, search

Contents

Introduction

Rhodes is a framework for building locally executing, device-optimized mobile applications. These applications are optimized for interacting with transactional enterprise application backends. It is also designed to work with synced local data using a local database such as SQLite or HSQLDB and a generic backend synchronization framework such as RhoSync (another open source component available from Rhomobile). It is available for iPhone, Research in Motion (Blackberry), Windows Mobile, Symbian and Android phones.

Rhodes takes much of its inspiration from web-oriented MVC style frameworks such as Ruby on Rails. However it has several simplifications and optimizations for the mobile scenario. As with developing apps on any good app framework, building a Rhodes mobile application will consist of building a set of files and putting them into the right directories for the respective models, controllers and views, as outlined in this tutorial

Sample Apps and Documentation

Here is a small and minimalistic sample app that demonstrates use of system specific API-s such as access to phonebook, camera, GPS, push, native mapping and other features. Here is a small conceptual and minimalist sample app that consists of SugarCRM cases, employees, and accounts.

See this tutorial for step by step instructions on how to build an application. The last part of the tutorial shows how to build for various platforms including how to release to the App Store. The latest RDoc is generated from source on each checkin and is posted here.

Architecture

The following diagram illustrates the Rhodes platform and how it interacts with the RhoSync server:

Image:RhodesArchitecture.jpg

Directory Structure

For example \sugar for an application that provides mobile access to SugarCRM. Beneath the app directory there will appear some .erb files. There will at least be an index.erb file as the default landing page. This default landing page will typically have links to the controllers for at least some of the data models. This index file is not associated with any controller. In case application needs default landing page to be associated with a controller, it is recommended to create a model/controller/view folder and use an action on this controller as a default start path. To do that, edit start_path property in the framework configuration file Please refer to the rhogen documentation for more information about the Rhodes directory structure.

Differences from Rails

The inspiration for the Rhodes application structure is the ease of use of web frameworks such as Rails and Merb. However we made some simplifications over the Rails directory structure. The most important changes and simplifications are:

  • a model is implicitly defined by a named subdirectory in the /app folder

It is assumed that there is a synced data in the local store associated with this model.

  • views files are in the model subdirectory

Maintaining the mapping between models and views is a little more difficult with distributed directories (e.g. subdirectories of the views subdirectory separate from the model directory). Hence all of the views files (index.erb, new.erb, create.erb, edit.erb, update.erb and new.rm) are placed right in the directory for the model.

  • many other directories available are not present in the Rhomobile app directory structure

The Rails and Merb frameworks have subdirectories such as helpers, test, vendor, lib, log, db. Their equivalents are generally in the root directory of the application.

  • lighter weight

Many of the differences from Rails are to make it easier to run on mobile devices with limited memory. Rhodes is lighter weight because it only providing the core necessary functions. Examples of the features that either aren't necessary in Rhodes or we haven't provided for space reasons are: web services, XML, pluralization, YAML, etc.

Controller Actions

Each Rhodes generated model controller has several actions to perform basic CRUD (create, read, update and delete) on the object. These actions are:

  • index - to list all objects
  • new - to display the editing form for creating a new object
  • create - to actually create the object
  • edit - to edit the actual object
  • update - to perform the update of the object
  • show - to view the object
  • delete - to delete the object

This set of actions and the associated URL paths follows the Rails scaffolding pattern for creating CRUD actions for objects and the associated "map resources" convention for routing to those actions.

Model (Since 1.4)

Your model class is located in the generated model's folder. For the model "UserBase" the file would be called "user_base.rb"

Example of added upper_name method to the model:

class UserBase
  #add model specifc code here
  def upper_name
    self.name.upcase
  end
end

Templates

We have referred to the template files used for views in the directory structure above. They all end in the extension .erb. Same as per Rails convention templates called index.erb (listing the data model objects), edit.erb (editing the object), show.erb (showing object values) and new.erb (creating a new object) are created by the rhogen generator. Inside the template, any valid HTML, javascript, and ruby can be used. Ruby code should be enclosed in <% and %> tags. See more information about ERB here.

'Loading' screen

There is ability to show custom 'Loading' screen on application start. Edit loading.html located in the folder <application-root>/app - it will be displayed while application initializing.
There is also ability to show splash screen instead of html page on application start (since Rhodes 1.5) - just place loading.png to the same location as loading.html.
Loading parameters in rhoconfig.txt:

  • delay - how long splash screen should be displayed (in seconds)
  • center,vcenter,hcenter - picture alignment
  • zoom,vzoom,hzoom

Example: splash_screen='delay=5;center' splash_screen='delay=5;hcenter;vzoom'

Layouts

Rhodes supports a layout mechanism based on ERB templates. The default layout template is called "layout.erb" and is located in the application root folder. Unless overridden, this layout is rendered on all non-ajax requests.

Customizing Layouts

If you would like to override or customize layout behavior, you can call the render function with the following parameters:

  render :action => 'index', :layout => 'mycustomlayout', :use_layout_on_ajax => false

The first argument is the action you would like to render. Next, the layout name is optionally provided which assumes the application root as a base directory. In the above example, rhodes would look for a "mycustomlayout.erb" in the application root directory (you also may call :layout => false to disable use of layout template). The use_layout_on_ajax argument tells rhodes whether or not to use the layout on ajax calls (default is false).

You can call layout method on controller to overwrite default layout name:

  layout :mycustomlayout

This will force render call to use mycustomlayout.erb in place of default layout file for all actions of this controller.

Menu Layout (since 1.2)

For menu-based platforms (currently RIM), the Rhodes framework provides the ability to change the native application menu items through the following simple api:

@default_menu = { "Item Label 1" => "/item1path", "Item Label 2" => "/item2path", ... } #=> overrides the rhodes default menu
 
@menu = { "Item Label 1" => "/item1path", "Item Label 2" => "/item2path", ... } #=> overrides the default menu in a specific action

Default Menu

To change the default menu (in application.rb):

class AppApplication < Rho::RhoApplication
  def initialize
    super
    @default_menu = { "Go Home" => :home, 
                      "View Accounts" => "/app/Account", 
                      "Do Refresh" => :refresh, 
                      "Perform Sync" => :sync, 
                      "App Options" => :options, 
                      "View Log" => :log }
  end
end

This will create a default menu with the following items (in top-down order):
Go Home
View Accounts
Do Refresh
Perform Sync
App Options
View Log

All of these menu items with the exception of "View Accounts" will call a reserved menu item. The "View Accounts" item when invoked will navigate to the path specified by the hash value, in this case /app/Account.

Controller Action Menu

To change the menu for a specific action (in controller.rb):

def index
  @accounts = Account.find(:all)
  @menu = { "Go Home" => :home, "Refresh" => :refresh, "Options" => :options, 
            :separator => nil, "Log" => :log, "New Account" => "/app/Account/new" }
  render
end

Note: The menu will reset to the application default menu as soon as the user navigates to a different action.

Reserved Menu Items

The following menu items are reserved and represented by symbols:

:home #=> navigate to configured start_path
:refresh #=> refresh the current page
:back #=> navigate to the previous page
:sync #=> trigger SyncEngine.dosync
:options #=> navigate to configured options_path
:log #=> load the native logging UI
:separator => nil #=> draw a separator line (if supported)
:close #=> close or put rhodes to background (depending on platform)

The following is the default Rhodes menu if none is provided in application.rb:

@default_menu = { "Home" => :home, 
                  "Refresh" => :refresh, 
                  "Sync" => :sync, 
                  "Options" => :options, 
                  "Log" => :log, 
                  :separator => nil, 
                  "Close" => :close }

Native Bar Control (since 1.2.2)

On iPhone (and Android since 1.5), Rhodes supports a tab-bar style layout in addition to the default toolbar-style layout. To use the tab-bar, all you have to do is define the tab items in your application.rb:

class AppApplication < Rho::RhoApplication
  def initialize
    # Tab items are loaded left->right, @tabs[0] is leftmost tab in the tab-bar
    @tabs = [{ :label => "Dashboard", :action => '/app', :icon => "/public/images/tabs/dashboard.png", :reload => true }, 
             { :label => "Accounts", :action => '/app/Account', :icon => "/public/images/tabs/accounts.png" },
             { :label => "Contacts", :action => '/app/Contact', :icon => "/public/images/tabs/contacts.png" },
             { :label => "Options", :action => '/app/Settings', :icon => "/public/images/tabs/options.png" }]
    # Important to call super _after_ you define @tabs!
    super
  end
end


Each tabbar item defined in the above sample defines three tab elements (all three are required):

:label #=> Visible label to display on the tabbar
:action #=> Path to your rhodes action (i.e. '/app/Account' would load the Account index action)
:icon #=> Relative path to tabbar item icon in your rhodes app (typically located in /public/images/)
:reload => true #=> Optional argument which tells rhodes to reload the tab's :action, defaults to false


Behind the scenes, Rho::RhoApplication will detect the @tabs array in its initialize method and build the native bar through the following function:

NativeBar.create(bar_type, bar_item_array)


The following Native Bar Types are allowed:

RhoApplication::TOOLBAR_TYPE #=> 0
RhoApplication::TABBAR_TYPE #=> 1
RhoApplication::NOBAR_TYPE #=> 2


To disable the navbar entirely:

class AppApplication < Rho::RhoApplication
  def initialize
    @@toolbar = nil
    super
  end
end


NOTE: Rhodes currently only supports up to 5 tabs in an application. An exception will be raised if you try to load more than 5 tabs at runtime.

Native bar runtime management (since 1.4)

Starting from version 1.4, Rhodes support creating and removing tab bars in runtime. Support of programmatic tab switching also added:

NativeBar.create(RhoApplication::TABBAR_TYPE, tabs) # Will remove existing toolbar/tabbar (if exists) and create new one
NativeBar.remove # Will remove current toolbar/tabbar. Does nothing if there is no active bar
NativeBar.switch_tab(1) # Will switch active tab to second (numeration is zero based i.e. 0 means first tab, 1 - second etc)

Note that after creating new toolbar or switching to another tab you need to call WebView.navigate method to get your webview correctly refreshed. For example:

NativeBar.create(RhoApplication::TOOLBAR_TYPE, nil)
WebView.navigate('app/Settings')

or

NativeBar.create(RhoApplication::TABBAR_TYPE, tabs)
NativeBar.switch_tab(3)
WebView.navigate('app/Settings', 3)

Helpers

link_to

Examples of how to use link_to method:

 link_to "Visit Other Site", "http://www.rhomobile.com/"
 ==> <a href=\"http://www.rhomobile.com/\" >Visit Other Site</a>
   
 link_to "Help", { :action => "help" }
 ==> <a href=\"/app/model/help\" >Help</a>
   
 link_to "Delete", { :action => "delete", :id => '{12}' }
 ==> <a href="/app/model/{12}/delete"  onclick="var f = document.createElement('form'); 
     f.style.display = 'none';this.parentNode.appendChild(f); f.method = 'POST'; 
     f.action = this.href;f.submit();return false;">Delete</a>
  
 link_to "Show", { :action => "show", :id => '{12}'},"style=\"height:4px;width:7px;border-width:0px;\"" 
 ==> <a href="/app/model/{12}/show" style="height:4px;width:7px;border-width:0px;">Show</a>
   
 link_to "Delete", { :action => "delete", :id => '{12}' }, "class=\"delete_link\""
 ==> <a href="/app/model/{12}/delete" class="delete_link" onclick="var f = document.createElement('form'); 
     f.style.display = 'none';this.parentNode.appendChild(f); f.method = 'POST'; 
     f.action = this.href;f.submit();return false;\">Delete</a>"
   
 link_to "Invate",:action => :invite, :query => {:name => 'John Smith', 'address' => "http://john.smith.com"}
 ==> <a href="/app/model/invite?name=John%20Smith&address=http%3A%2F%2Fjohn.smith.com" >Invate</a>

url_for

Examples of how to use url_for method:

 url_for '/some_url'
 ==> /some_url
   

When generating a new URL, missing values may be filled in from the current request's parameters. For example, if application name or model are not specifyed in the call parameters, they would be filled from the request.

 url_for :action => :index
 ==> /app/model
   
 url_for :action => :create
 ==> /app/model
  
 url_for :action => :new
 ==> /app/model/new
   
 url_for :action => :show, :id => '{12}'
 ==> /app/model/{12}/show
 
 url_for :model => :another_model, :action => :show, :id => '{12}'
 ==> /app/another_model/{12}/show
   
 url_for :controller => :another_controller, :action => :show, :id => '{12}'
 ==> /app/another_controller/{12}/show
   
 url_for :application => :another_app, :model => :another_model, :action => :show, :id => '{12}'
 ==> /another_app/another_model/{12}/show
    
 url_for :action => :create, :query => {:name => 'John Smith', 'address' => "http://john.smith.com"}
 ==> /app/model?name=John%20Smith&address=http%3A%2F%2Fjohn.smith.com
   
 url_for :action => :show, :id => '{12}', :fragment => "an-anchor" 
 ==> /app/model/{12}/show#an-anchor

Advanced Usage of Render

  • Render does not need to be called at the end of each method. If render was not called, then it will default to rendering the action of the method you are in.
  • Rendering of views works with no method in controller. If the method does not exist for an action, but a view exists for that action, then the view will be rendered.
  • Rendering of files. render :file => "Settings/wait.erb" will render that file with the current controller's instance. By default layout is false when rendering a file
  • Rendering of partials, with collections or locals. Either collections or locals must be provided. Only partials for the current controller is supported at this time.


render :partial => "ad", :collection => ["foo1","foo2","foo3"]

or

render :partial =>"ad", :locals => { :ad => "foo_ad" }

Will render the partial "_ad.erb" and the local variable "ad" will be available. With a collection, the partial will be rendered once per element.

Per Platform Files

On a per platform basis, you can use alternative files for any given file in your 'app' or 'public' folder. To do this, make a second file with the platform in the name of the file. For example:

 default.css
 index.erb

Will be replaced with

 default.android.css
 default.android.erb

When building for the android platform. This works for any file located within the 'app' or 'public' folder.

You must have have the base file exists for it to be replaced.

The valid platforms are: android, wm, bb, iphone

Device Capabilities / Native UI Elements

Rhodes will provide access to device specific capabilities such as GPS, PIM, camera, SMS, video player, accelerometer, proximity detector and native UI elements. Below is the device support matrix showing what release supports what device capabilities on a per device operating system basis.

Capability iPhone Windows Mobile BlackBerry Symbian Android
GeoLocation 0.3 0.3 0.3 1.1 1.0
PIM Contacts 0.3 0.3 0.3 1.0 1.0
Camera 1.0 1.0 1.0 1.1 1.0
Date/Time picker 1.2.2 tbd 1.2 tbd 1.2
Native Menu tbd tbd 1.2 tbd tbd
Tabs / Tab Bar 1.2.2 tbd tbd tbd tbd
Audio / Video capture 2.0 2.0 2.0 tbd tbd
Bluetooth 2.0 tbd 2.0 tbd 2.0
Calendar Integration 2.0 tbd 2.0 tbd 2.0
Accelerometer 2.0 tbd tbd tbd tbd
Push / SMS 1.2 2.0 1.2 tbd 2.0
Landscape 2.0 tbd 2.0 tbd tbd
Native Maps 1.4 tbd 1.4 tbd 1.5
Alerts / Audio File Playback 1.2 1.5 1.2 tbd 1.2


GPS

Geolocation information is available as an asynchronous service through ajax call to predefined local URL or in a controller through ruby calls to GeoLocation class.

In general, GPS receiver consumes significant amount of energy and therefore it switched off most of the time in order to preserve phone battery. Any call to get location information will power up GPS receiver. However, it usually takes a while to acquire current position; in some cases, it may take several minutes and may not work at all indoors.

Asynchronous ajax calls

Rhodes framework provides predefined url call to which will return current known position. To use it, include the appropriate javascript library on your page:

 /public/js/jquery-1.2.6.min.js and /public/js/rhogeolocation.js - for iPhone, Android, and Symbian
 /public/js/rhogeolocation-wm.js - for WM
 Blackberry webview control doesn't support ajax calls 

Then add one of the following tags in the appropriate location in your HTML: <geolocation/>, <geolatitude/> or <geolongitude/>. Included javascript will query predefined url and fill these tags with location information.

<geolocation/> - returns a string in the form [formatted position];[latitude];[longitude]. For example: 37.3317° North, 122.0307° West;37.331689;-122.030731

<geolatitude/> - returns just the latitude

<geolongitude/> - returns just the longitude

GeoLocation class

GeoLocation.latitude #=> returns current latitude 
 
GeoLocation.longitude #=> returns current longitude
 
GeoLocation.known_position? #=> returns true if system location system is up and acquired position
 
GeoLocation.set_notification(callback, callback_params="") #=> set callback to track location changes, it called only once, to continue call set_notification again 
callback parameters: known_position, latitude, longitude, available, status('error', 'ok'), error_code(from RhoError)
 
RhoController.set_geoview_notification(callback, callback_params="", update_timeout_sec) #=> set callback to track location changes on current view, this can be used instead of ajax call

Usage scenarios:

  • Application require location at startup:
class AppApplication < Rho::RhoApplication
  def on_activate_app
    #start geolocation
    GeoLocation.set_notification("/app/Settings/geo_callback", "")
  end
end
 
class SettingsController < Rho::RhoController
  def geo_callback
    puts "geo_callback : #{@params}"
  end
end
  • Application require location on specific view
  def show_location
    if !GeoLocation.known_position?
      GeoLocation.set_notification( url_for(:action => :geo_callback), "", 2)
      redirect wait
    else
      render
    end
  end
 
  def geo_callback
    WebView.navigate show_location if @params['known_position'].to_i != 0 && @params['status'] =='ok'
    WebView.navigate show_location_error if @params['available'].to_i == 0 || @params['status'] !='ok'
    #do nothing, still waiting for location 
  end
  • Application track location on specific view without ajax(for example Blackberry does not support ajax). view callback calls only while current view is active, after navigation from this view, callback will be deleted
  def show_location
    set_geoview_notification( url_for(:action => :geo_viewcallback), "", 2)  if System::get_property('platform') == 'Blackberry'
    render
  end
 
  def geo_viewcallback
    WebView.refresh if @params['known_position'].to_i != 0 && @params['status'] =='ok'
  end

Send mail and call number urls

  • mailto
   <a href="mailto:@?subject=testing123">Mailto</a>

NOTES: even for empty address add @ symbol

  • tel
   <a href="tel:1-555-531-3255!8335033#!#!9582#">Tel</a>
   <a href="wtai://wp/mc;5195551212" title="Call">Work Tel</a>
   <a href="wtai://wp/mc;5195551213" title="Call">Home Tel</a>

NOTES: Here is the link to WML tel description: http://na.blackberry.com/eng/devjournals/resources/journals/oct_2004/wml_101.jsp

Sample

See controller and view in the /app/GeoLocation folder of the system API sample application for more information

  • Testing on WM 6 Emulator

While developing your application on WM emulator, you may find FakeGPS utility usefull

  • Testing on BB simulator

On the BB simulator select menu Simulate -> GPS Location.

  • Providing Mock Location Data on Android

http://developer.android.com/guide/topics/location/index.html

PIM

Phonebook/Contacts

Rhodes provides access to the device's local phone book and stored the contacts provided by the Ruby class RhoContact.

The following methods are available on the RhoContact class:

Rho::RhoContact.find(:all) #=> return hash of hashes of all contacts stored in the phonebook. (index)
 
Rho::RhoContact.find(@params['id']) #=> return hash of all properties of the contact identified by the provided id. (show)
 
Rho::RhoContact.create!(@params['contact']) #=> create new contact in the phone book, set properties of the contact 
# from passed as parameter hash, and save created phonebook record. (create)
 
Rho::RhoContact.update_attributes(@params['contact']) #=> find contact record in the phonebook, update record properties 
# from the has passed as parameter and save updated record. Contact id passed in the hash. (update) 
 
Rho::RhoContact.destroy(@params['id']) #=> remove contact identified by the provided id from the phonebook. (delete)

Contact's properties currently supported are following: "id","first_name","last_name","mobile_number","home_number","business_number","email_address","company_name"

On iPhone, contact's properties currently supported are following:

  • General: "prefix", "first_name", "middle_name", "last_name", "suffix", "nickname", "birthday", "аnniversary" , 'created', 'updated', "spouse_name", "company_name", "job_title", "assistant_name", "assistant_number", 'personal_note'

(fields 'birthday', 'аnniversary', 'created', 'updated' expect date in the YYYY-MM-DD format)

  • Addresses:
    • 'street_address_1', 'city_1', 'state_1', 'zip_1', 'country_1'
    • 'street_address_2', 'city_2', 'state_2', 'zip_2', 'country_2'
    • 'street_address_3', 'city_3', 'state_3', 'zip_3', 'country_3'

(addres 1 is mapped to "work" address, 2 is to "home" address, 3 is to "other" address)

  • Emails: "email_address", "home_email_address", "other_email_address"
  • Phones: "business_number", "home_number", "mobile_number", "main_number", "pager_number", "home_fax", "work_fax"
  • Home page: 'home_page'

Sample

For examples on how to use the API provided by this class, see view and controller in the /app/Contacts folder in the system API samples application.

Camera

Camera API allow to:

  • take a picture
Camera::take_picture('/app/model/camera_callback')
  • chose a picture from album
Camera::choose_picture('/app/model/camera_callback')
  • check is camera available
System::get_property('has_camera')

Once user take/choose picture, callback url you specified will be called. Callback is a POST message; body of the message contains status and image_uri.

  • status may have following value: 'ok', 'cancel', or 'error'
  • image_uri points to the taken/selected image stored in the /public/db-files folder; image file will have auto-generated name

Sample

See controller and view in the /app/Camera folder of the system API sample application for more information.

Date/Time picker (since 1.2)

Date/Time picker API allow to choose date or time:

DateTimePicker.choose(callback, title, initial_time, fmt)

or

DateTimePicker.choose(callback, title, initial_time, fmt, opaque)

There is the fourth parameter which value can be one of the 0, 1 or 2. Otherwise exception throws.

  • 0 - mean full date and time input field
  • 1 - date only input field
  • 2 - time only input field

'opaque' is the optional parameter and should be string. It is non-interpreted (opaque) parameter which will be returned in callback as is.

Once user choose date/time and press OK or Cancel, callback url you specified will be called. Callback is a POST message; body of the message contains 'status', 'result' and, possibly, 'opaque'.

  • 'status' may have following value: 'ok' or 'cancel'
  • there is no 'result' in case if status is 'cancel'
  • 'result' is the string containing number of seconds since Epoch and ruby time can be created from it using Time::at method
  • 'opaque' - if exist, will contain passed to the method 'choose' data parameter.

NOTE: currently implemented for Android, iPhone and Blackberry

Sample

See controller.rb and index.erb view in the /app/DateTime folder of the system API sample application for more information. This example demonstrates each of the date/time picker types.

Ringtone manager (since 1.2)

Ringtone manager API allow to get list of installed ringtones and play any of them. Example:

@ringtones = RingtoneManager::get_all_ringtones

Here @ringtones will be a hash containing key/value pairs - user friendly name of ringtone and its full file name. This file name could be then passed to the method 'play':

RingtoneManager::play @ringtones['My Ringtone']

In case if 'play' called when another playing is active, it actually stop current playing and start new one.

RingtoneManager::stop

Method 'stop' stops playing at all. This method has no parameters and could be safely called even if there is no active playing.

NOTE: currently implemented for Android and Blackberry only. There is also known issue on Blackberry - only list of user installed ringtones accessible. System preinstalled ringtones are not accessible due to Blackberry limitations.

Sample

See controller and view in the /app/Ringtones folder of the system API sample application for more information.

Database (Rhom)

Rhom is a mini object mapper implemented in Ruby. It provides database-abstraction functionality to the Rhodes microframework. It allows simple models to be used with a "property bag" database.

Please see the Rhom page for more details.

Sources

Rhodes sources are also available by the rhodes ruby framework through the RhomSource object. You can use RhomSource just like a normal Rhom object with the exception that only the source_url field is writeable (below is the SourceController provided in the sample applications which demonstrates how to use RhomSource):

require 'rho/rhocontroller'
require 'rhom/rhom_source'
 
class SourceController < Rho::RhoController
  include Rhom
 
  def index
    @sources = RhomSource.find(:all)
    render :action => :index
  end
 
  def edit
    @source = RhomSource.find(@params['id'])
    render :action => :edit
  end
 
  def update
    @source = RhomSource.find(@params['source']['source_id'])
    @source.update_attributes(@params['source'])
    redirect :action => :index
  end
end

You can also access the following attributes on RhomSource:

@source = RhomSource.find(@params['id'])
puts @source.source_id #id of source
puts @source.name #name of source
puts @source.source_url #url of a source
puts @source.last_updated #returns Time object of last sync for the source (in Time.at format)
puts @source.last_inserted_size #returns number of records inserted on last sync
puts @source.last_deleted_size #returns number of records deleted on last sync
puts @source.last_sync_duration #returns duration of last sync for this source in seconds
puts @source.last_sync_success #returns 1 if sync was successful, 0 otherwise
puts @source.distinct_object #returns number of records

Show last sync time for Product source example:

<%= ::Rhom::RhomSource.find(Product.get_source_id).last_updated.strftime("%m/%d/%Y, %I:%M%p") %>

Database Crash Recovery

Rhodes provides the following functions for recovering the database from a bad or corrupt state

Rhom::Rhom.database_full_reset #deletes all records from object_values table and client_info table

Since 1.2, calling this method will also make a call to the "clientreset" action in rhosync. See the clientreset section for more information.

Image/Blob Sync

Synchronization of images to the rhosync server is provided automatically if your model handling camera images provides a source url.

Note: same mechanism may be used for any arbitrary files, not just images.

store image on client and synchronize it with backend server

Suppose you have a model called "Image". Then you have a camera callback function defined in the Image model's controller:

  def camera_callback
    if @params['status'] == 'ok'
      #create image record in the DB
      image = Image.new({'image_uri'=>@params['image_uri']})
      image.save
      SyncEngine.dosync
    end  
    #reply on the callback
    render :action => :ok, :layout => false
  end

This callback will trigger a sync after saving the image, which will subsequently push it up to the rhosync server. It is up to your image source adapter to handle the image (which is provided to you as a paperclip attachment called "blob":

  def create(name_value_list,blob)
    if blob
      obj = ObjectValue.find(:first, :conditions => "object = '#{blob.instance.object}' AND value = '#{name_value_list[0]["value"]}'")
      path = blob.path.gsub(/\/\//,"\/#{obj.id}\/")
      name = name_value_list[0]["value"]
 
      `cp #{path} #{File.join(RAILS_ROOT,'public','images',name)}`
    end
  end

In this example, we copy the blob to the rails public folder so that we can query for the list of images in the query method. See the "camera" sample source adapter that comes with rhosync.

fetch image/blob file from the backend server

In your query call, you should store the url to the new backend image so that the device can fetch the image when it syncs for the new records:

  def query
    @result={}
    Dir.entries(PATH).each do |entry|
      new_item = {'image_uri' => BASEURL+'/images/'+entry, 'attrib_type' => 'blob.url'}
      unless entry == '..' || entry == '.' || entry == '.keep'
        p "Found: #{entry}"
        @result[entry.hash.to_s] = new_item
      end
    end
    @result
  end

See rhosync/vendor/sync/SystemApiSamples/camera.rb for a complete rhosync adapter example.

Sample

See controller and view in the /app/Blob folder of the system API sample application for more information.

RhoError class

This class contains error codes and method message() to translate error code to text message. All callbacks return error code from this class. Status text in callback contain some internal error, so in most cases it should not be exposed to user. Currently RhoError contains the following error codes:

   ERR_NONE = 0
   ERR_NETWORK = 1
   ERR_REMOTESERVER = 2
   ERR_RUNTIME = 3
   ERR_UNEXPECTEDSERVERRESPONSE = 4
   ERR_DIFFDOMAINSINSYNCSRC = 5
   ERR_NOSERVERRESPONSE = 6
   ERR_CLIENTISNOTLOGGEDIN = 7
   ERR_CUSTOMSYNCSERVER = 8
   ERR_UNATHORIZED = 9

Sample

See Login/Logout Manager as an example.

Login/Logout Manager

Rhodes comes with a built-in login manager for handling authentication with RhoSync. The following functions are available to access the login manager:

# Pre 1.2
SyncEngine::login(@params['login'], @params['password']) #returns 1 if login was successful to all sources, otherwise 0
 
SyncEngine::logout #performs a logout on each source
SyncEngine::logged_in #returns 1 if true, 0 if false

Please see the "Settings" controller in your rhodes application for an example of how this is used.

Since 1.2 Rhodes uses a callback for login and the function has changed accordingly:

# Since 1.2 use this login method
SyncEngine.login(@params['login'], @params['password'], (url_for :action => :login_callback))
 
# Default callback that's provided by the generator
def login_callback
  err_code = @params['error_code'].to_i
  if err_code == 0
    # run sync if we were successful
    WebView.navigate Rho::RhoConfig.start_path
    SyncEngine.dosync
  else
      if errCode == Rho::RhoError::ERR_CUSTOMSYNCSERVER
        @msg = @params['error_message']
      end
 
      if !@msg || @msg.length == 0   
        @msg = Rho::RhoError.new(errCode).message
      end
 
    WebView.navigate ( url_for :action => :login, :query => {:msg => @msg} )
  end  
end

Sample

See controller and view in the /app/Settings folder of the system API sample application for more information.

Configuration

Each rhodes application contains a configuration file called "rhoconfig.txt". A typical rhoconfig.txt will look like the following when an app is generated:

# Startup page for your application
start_path = '/app'

# Path to the options page (in this case handled by javascript)
options_path = '/app/Settings'

# Location of bundle url (i.e. from rhohub.com); used by desktop win32 simulator
rhobundle_zip_url = ''

# Optional password to access bundle (usually not required); used by desktop win32 simulator 
rhobundle_zip_pwd = nil

# Rhodes log properties
MinSeverity  = 0
LogToOutput = 1
LogCategories = *
ExcludeLogCategories =

# Keep track of the last visited page
KeepTrackOfLastVisitedPage = 0 
LastVisitedPage = ''

# Sync server url.  This must be the full path to your sources.  For example: http://rhomobile.rhohub.com/apps/SugarCRM/sources/
syncserver = 'http://myrhosyncserver.com/apps/MyNewApp/sources/'

# open rhodes app in full screen mode. default 1
full_screen = 0

# show status bar on windows mobile. default 1
wm_show_statusbar = 0

Last Visited Page

Rhodes may keep track of the last visited page so next time you start your application framework will navigate to this page. To enable this feature, use KeepTrackOfLastVisitedPage=1

NB: Keep in mind, POST requests will be converted to GET-s.

Application database version

If you want next version of your application clean database from previous one and start form clean db: set app_db_version in rhoconfig.txt. When rhodes started it compare db version with app_db_version and clean db if they do not match.

Ruby API

Each of the configuration options shown above are available through the RhoConfig ruby api:

Rho::RhoConfig.options_path #=> returns '/app/Settings'
 
Rho::RhoConfig.options_path = '/app/MyObject' #=> assignment will be saved to rhoconfig.txt

Note: you may store any arbitrary options in the rhoconfig using ruby API. Just keep in mind, they are going to be lost next time user install new version of the application.

SyncEngine Configuration (since 1.2)

In the previous section, there is a "syncserver" option available. This option can be changed through the Rho::RhoConfig api as you would expect. However, since changing the syncserver requires updating the SyncEngine at runtime, another ruby call is available:

SyncEngine.set_syncserver('http://examplesyncserver.com/apps/MyApp/sources/') #=> nil

Calling the function above will do the following:

  1. update in-memory syncserver for the SyncEngine (all following synchronization will use the new syncserver)
  2. perform a logout to make sure session is removed for old syncserver
  3. write the new syncserver property to rhoconfig.txt

To disable auto sync you can call

SyncEngine.set_pollinterval(0)

OR set in rhoconfig.txt: sync_poll_interval=0

Logging

To show application log on the device using:

Rho::RhoConfig.show_log

To send log to sync server:

Rho::RhoConfig.send_log

Persistent storage (Blackberry only, since 1.2)

To enable using of RIM persistent storage API in rhodes, just add line

use_persistent_storage = 1

to your rhoconfig.txt. It will enable using of RIM persistent storage (instead of files) to store DB version, DB journal and database itself. The main goal of persistent storage is that all objects stored there by application will be automatically removed when application uninstalled.

SyncEngine class

  • SyncEngine.dosync(show_sync_status) # requests to start sync process; if optional parameter show_sync_status set to false, then sync status popup will not be displayed (default is true)
  • SyncEngine.dosync_source(source_id, show_sync_status) # requests to start sync process on the specified source; if optional parameter show_sync_status set to false, then sync status popup will not be displayed (default is true)
  • SyncEngine.lock_sync_mutex # wait to acquire sync engine lock (useful for performing batch operations)
  • SyncEngine.unlock_sync_mutex # release acquired sync engine lock (make sure you do this if you call lock!)
  • SyncEngine.login(login, password, callback) # authenticates user with sync server; cllback url will be called after login operation is completed, more info here
  • SyncEngine.logged_in # returns true if user is authenticated by the sync server, more info here
  • SyncEngine.logout # logout user from sync server, more info here
  • SyncEngine.stop_sync # stops any sync operations in progress
  • SyncEngine.set_notification(source_id, url, params) # more info here
  • SyncEngine.clear_notification(source_id) # more info here
  • SyncEngine.set_pollinterval(interval) # set sunc poll interval; 0 will disable polling for updates, sync still may be initiated by PUSH notifications
  • SyncEngine.set_syncserver(syncserver) #set sync server address and stores it in Rho Config
  • SyncEngine.set_objectnotify_url # more info here
  • SyncEngine.set_pagesize - set page size for server show request
  • SyncEngine.get_pagesize - set page size for server show request

Sync notification

Once sync happened on a source view which reflects data provided by the source should be refreshed. To accomplish that you may use sync notification mechanism.

To set a notification use one of the following ruby calls:

  Account.set_notification(url,params)
  SyncEngine.set_notification(source_id,url,params)
  SyncEngine.set_notification(-1,url,params) #set callback for all sources

For example:

  Account.set_notification("/app/Account/sync_notify", "sync_complete=true")

Once object(s) associated w/ Account source are changed, the view will be directed to sync_notify action (with params 'sync_complete=true') if user happens to be on the same page.

There 3 status in sync callback: Common parameters: source_id, source_name

  • in_progress. Parameters: total_count, processed_count, cumulative_count. You can use it to report sync progress to user.
  • error. Parameters: error_code, error_message
  • ok. Parameters: total_count, processed_count, cumulative_count
  def sync_notify
    status = @params['status'] ? @params['status'] : ""
    if status == "error"
      errCode = @params['error_code'].to_i
      if errCode == Rho::RhoError::ERR_CUSTOMSYNCSERVER
        @msg = @params['error_message']
      else 
        @msg = Rho::RhoError.new(errCode).message
      end
 
      WebView.navigate(url_for(:action => :server_error, :query => {:msg => @msg}))
    elsif status == "ok"
      if SyncEngine::logged_in > 0
        WebView.navigate "/app/Page"
      else
        # rhosync has logged us out
        WebView.navigate "/app/Page/authenticate_form"      
      end
    end  
  end

Note: if view updated using ajax calls, this mechanism may not work correctly as view location will not change from one ajax call to another.

Create objects errors

Server can return errors while creating objects. In this case sync notification callback receive the following parameter:
create_error - array of hashes. Each hash contain 'object' and 'error_message'.

Sync object notification

Application can receive notifications about object modifications by sync engine. So application can update current page if it displays modified object.

  • Call SyncEngine.set_objectnotify_url. You can call it once for example in AppApplication.initialize
  • In the controller method call add_objectnotify with array of objects or single object before render method.
  • If you use url_for in your erb-file: when select specific object to view, url contain object id, so this object id will added to notification map automatically.
  • Notification callback receive 3 arrays of hashes- 'deleted' , 'updated' and 'created'. Each hash contain 'object' and 'source_id'

Example

class AppApplication < Rho::RhoApplication
   def initialize
    super
 
    SyncEngine::set_objectnotify_url("/app/Settings/sync_object_notify")
   end
 
end
 
class ProductController < Rho::RhoController
 
  #GET /Product
  def index
    @products = Product.find(:all)
 
    add_objectnotify(@products)
    render
  end
end
 
def sync_object_notify
    #do something with notification data
    WebView.refresh
end

Asynchronous "ask" call

If your rhosync source adapter supports the "ask" call to directly query the backend, you can use the following function in your application to trigger an "ask" call to rhosync:

  Account.ask('bigco')

This will trigger a call to the ask method on the Account source on rhosync with params['question'], which returns a set of object_values back to the client.

Asynchronous search call

If you have big database on the server you don't have to load it to device. In such case you can use asynchronous search feature:

Somewhere in your controller:

Contact.search(
  :from => 'search',
  :search_params => { :FirstName => @params['FirstName'], :LastName => @params['LastName'], :Company => @params['Company'] },
  :offset => page*page_size,
  :max_results => page_size,
  :callback => '/app/Contact/search_callback',
  :callback_param => "" )

Rhodes 1-4 Note: Rhodes automatically add search_params to callback_param, so in callback it is available as: @params['search_params'][:FirstName] etc

Your callback would look something like this:

def search_callback    
  if (status && status == 'ok')
    WebView.navigate ( url_for( :action => :show_page, :query => @params['search_params']) )
  end
  #TODO: show error page if status == 'error'
  render :action => :ok
end

To make find faster use : http://wiki.rhomobile.com/index.php/Rhom#find.28.2Aargs.29_Advanced_proposal
And after callback is called and data is ready, you may render next page:

def show_page
  $contacts = Contact.find(:all,
    #:conditions => ["LOWER(FirstName) LIKE ? OR LOWER(LastName) LIKE ? OR LOWER(Company) LIKE ?",
    #  @params[:FirstName], @params[:LastName], @params[:Company]]
 
    :conditions => { 
      {:func=>'LOWER', :name=>'FirstName', :op=>'LIKE'}=>@params[:FirstName], 
      {:func=>'LOWER', :name=>'LastName', :op=>'LIKE'}=>@params[:LastName],
      {:func=>'LOWER', :name=>'Company', :op=>'LIKE'}=>@params[:Company],
    }, 
    :op => 'OR', 
 
    :select => ['FirstName','LastName', 'Company'],
    :per_page => page_size, :offset => page*page_size )    
  render :action => :show_page
end

To stop call callback return 'stop':

def search_callback    
  if (status && status == 'ok')
    WebView.navigate ( url_for :action => :show_page )
  end
  #TODO: show error page if status == 'error'
  'stop'
end

Asynchronous search call parameters

Search call takes hash of the following parameters. All parameters not named one of those below are assumed to be search parameters.

:from #=> sets the path or custom method that records will be fetched from (optional, default is 'search')  
:offset #=> starting record to be returned 
:max_results #=> max number of records to be returned 
:callback #=> callback to be called after search is completed
:callback_param #=> parameters to be passed to the callback (optional)
progress_step #=> optional parameter, define how often search callback will be called with 'in_progress' state

Call from example above will result in the following call to the RhoSync server:

 /search?conditions[FirstName]=Jon&conditions[LastName]=Smith&conditions[Company]=Acme,offset=30,max_results=10

AsyncHttp (since 1.5)

To make asynchronous calls to web service or any other http(s) server, you should use RhoHttp:

  • get(:url, :headers, :callback,:callback_params)
  • post(:url, :headers, :body, :callback,:callback_params)
  • download_file(:url, :headers, :filename, :callback, :callback_params)
  • upload_file(:url, :headers, :filename, :callback, :callback_params)
  • cancel(cancel_callback = '*') # cancel current http call, '*' is by default, means cancel all current http calls
  Rho::AsyncHttp.get(
  :url => 'http://www.example.com',
  :headers => {'Cookie' => cookie},
  :callback => (url_for :action => :httpget_callback),
  :callback_param => "" )
 
  Rho::AsyncHttp.post(
  :url => 'https://www.example.com',
  :headers => {'Cookie' => cookie},
  :body => 'Test',
  :callback => '/app/Contact/httpget_callback',
  :callback_param => "" )
 
  @@file_name = File.join(Rho::RhoApplication::get_base_app_path(), 'test.jpg')
  Rho::AsyncHttp.download_file(
    :url => 'http://rhomobile.com/wp-content/themes/rhomobile/img/imgs_21.jpg',
    :filename => @@file_name,
    :headers => {},
    :callback => (url_for :action => :httpdownload_callback),
    :callback_param => "" )
 
  @@file_name = File.join(Rho::RhoApplication::get_base_app_path(), 'rhoconfig.txt')
  Rho::AsyncHttp.upload_file(
    :url => 'http://dev.rhosync.rhohub.com/apps/SystemApiSamples/sources/client_log?client_id=19bdcf15-aca2-4e5a-9676-3c297c09bb11&device_pin=&   log_name=',
    :filename => @@file_name,
    :headers => {},
    :callback => (url_for :action => :httpupload_callback),
    :callback_param => "" )
 
  def httpget_callback
    #@params contain response. In case of json (ContentType=application/json) @params['body'] 
    #contain parsed body represented as hash of hashes
    #@params['headers'] contain response headers represented as hash
    #@params['cookies'] contain parsed cookies suitable for server request 
    #in case of unrecognized body type @params['body'] contain raw text body
    #in case of error usual RhoError info returned plus @params['http_error'] and @params['body'] contain server response
  end

NOTE: to parse xml (application/xml) use rexml ruby extension : http://wiki.rhomobile.com/index.php/Rhodes#rexml_library_support
See rhodes-system-api-samples\AsyncHttp as an example

PUSH notifications (since 1.2)

As of 1.2 release of Rhodes, PUSH support implemented on Blackberry and iPhone

Blackberry

Notifications to BB sent using PAP 2.0 message through BES/MDS server.

To listen for incoming messages on BB application will start when device powered up and run listener thread in background all the time. Use "push_port" option in the rhoconfig.txt to specify port on which client will listen for incoming push messages. If port is not specified, default will be 100.

iPhone

iPhone PUSH support uses Apple Push Notification service introduced in iPhone SDK 3.0

Payload

It is possible to combine more then one operation in a single message.

There are no required operations. There are default operations - if operation is not specified, no default operation will be performed.

In case of Blackberry if application in background, show_popup operation will bring application upfront; other operations will not. In case of iPhone, regardless of operation, user will be presented with option to activate application if it is not running.

Payload may include following operations which client will perform as it receive PUSH message:

  • do_sync
do_sync = http://source-url/1/

It will do sync on spec specified sync source; use "all" to sync all sources

  • show_popup
 show_popup = "Some message"

It will bring app upfront and show specified message

  • vibrate
 vibrate = duration in milliseconds

It will vibrate for the specified number of milliseconds, up to 25500; if 0 or no duration is specified, it will vibrate for 2500 millisecond.

  • play_file
 play_file = file-name.ext, [media-type]

It will play specified file if media type supported by the phone. iPhone will ignore media-type parameter.

File should be included to the application bundle.

In case of Blackberry, if file is in public folder, file name will be /apps/public/test-file.mp3 Media type should be either specified explicitly or may be recognized from file extension. Known file extensions are: .mp3 - audio/mpeg; .wav - audio/x-wav

In case of iPhone, audio files should be placed in the /public/alerts folder and build script will copy them into root of the application main bundle (iPhone wouldn't play file from any other place).

Alerts (since 1.2)

In your controller, you may call on system alert methods to show popup, vibrate, or play audio file.

  • show_popup
Alert.show_popup "Some message"

It will bring app upfront and show specified message

  • vibrate
Alert.vibrate(duration_in_milliseconds)

It will vibrate for the specified number of milliseconds, up to 25500; if 0 or no duration is specified, it will vibrate for 2500 millisecond.

  • play_file
Alert.play_file(file_name.ext, media_type)

It will play specified file if media type supported by the phone. File should be included to the application. For example, if file is in public folder, file name will be /apps/public/test-file.mp3 Media type should be either specified explicitly or may be recognized from file extension. Known file extensions are: .mp3 - audio/mpeg; .wav - audio/x-wav

Sample

See controller and view in the /app/Alert folder of the system API sample application for more information.

WebView class

It is possible to call on the WebView (browser) directly from your controllers. This API is recommended for use from callbacks, such as sync callback or camera callbacks.

WebView.refresh #=> will force WebView refresh current page
 
WebView.navigate(url) #=> will force WebView navigate to provided location (url)
 
# Since 1.2.2 WebView.navigate supports an optional index parameter (defaults to 0, useful for tabbed applications)
WebView.navigate(url, index)
 
WebView.current_location #=> will return location (url) of the currently displayed page
 
WebView.execute_js(js) #=> will try to execute javascript string in the context of the currently displayed page; iPhone only
 
WebView.active_tab #=> returns index of @tabs array for currently selected tab; iPhone only, all other platforms it returns 0

Sample

See controller and view in the /app/Camera folder of the system API sample application for some of the examples of how to use WebView in callbacks.

MapView

MapView class provides an embeddable map interface, similar to the one provided by the Maps application. The following code would go into your controller and the map appears on a whole page.

     map_params = {
          :settings => {:map_type => "hybrid",:region => [@params['latitude'], @params['longitude'], 0.2, 0.2],
                        :zoom_enabled => true,:scroll_enabled => true,:shows_user_location => false,
                        :api_key => 'Google Maps API Key'},
          :annotations => [{:latitude => @params['latitude'], :longitude => @params['longitude'], :title => "Current location", :subtitle => ""},
                           {:street_address => "Cupertino, CA 95014", :title => "Cupertino", :subtitle => "zip: 95014", 
                            :url => "/app/GeoLocation/show?city=Cupertino"},
                           {:street_address => "Santa Clara, CA 95051", :title => "Santa Clara", :subtitle => "zip: 95051", 
                            :url => "/app/GeoLocation/show?city=Santa%20Clara"}]
     }
     MapView.create map_params

Map settings:

  • map_type - widget may display maps of three types: standard, satellite, and hybrid
  • region - [latitude,longitude,latitudeDelta,longitudeDelta]. The area currently displayed by the map view. The region encompasses both the latitude and longitude point on which the map is centered and the span of coordinates to display. The span values provide an implicit zoom value for the map. The larger the displayed area, the lower the amount of zoom. Similarly, the smaller the displayed area, the greater the amount of zoom.
    • latitude,longitude - map coordinate of the region center
    • latitudeDelta - the amount of north-to-south distance (measured in degrees) to display on the map. Unlike longitudinal distances, which vary based on the latitude, one degree of latitude is always approximately 111 kilometers (69 miles).
    • longitudeDelta - the amount of east-to-west distance (measured in degrees) to display for the map region. The number of kilometers spanned by a longitude range varies based on the current latitude. For example, one degree of longitude spans a distance of approximately 111 kilometers (69 milies) at the equator but shrinks to 0 kilometers at the poles.
  • zoom_enabled - true if zoom of the map is enabled
  • scroll_enabled - true if scrool of the map is enabled
  • shows_user_location - true if current user location is displayed on the map
  • api_key - Google Maps API Key (sign up for it here: http://code.google.com/apis/maps/signup.html)

Annotations - array of map annotation objects (list of pins on the map). Annotation:

  • latitude,longitude - map coordinate of the annotation
  • street_address - if map coordinate is not specified, framework will attempt to obtain it using provided street address from google geo-coding service
  • title - title of the annotation callout
  • subtitle - subtitle of the annotation callout
  • url - url to follow when user click on the callout button

Sample

See GeoLocation/controller.rb of system API sample application for some of the examples of how to use MapView class.

File system access

Rhodes client file system structure

<rhodes root> #system-dependent path
 apps        #Rho::RhoApplication::get_base_app_path
  app        #Rho::RhoApplication::get_app_path('app') - contain models
   model1    #Rho::RhoApplication::get_model_path('app','model1')
  public     #contains files from application public folder
   db-files  #contains files stored in database(blobs)
 db          #contains schema and data files
 lib         #contains rho framework library files. Blackberry does not has this folder, library files are stored in jar
 RhoLog.txt  #application log

Read\write file example

  fileName = File.join(Rho::RhoApplication::get_base_app_path(), 'myfile.txt')
  File.open(fileName).each do |line|
  end
 
  fileNameW = File.join(Rho::RhoApplication::get_base_app_path(), 'tempfile.txt')
  f = File.new(fileNameW)
  f.write('test')
  f.close

Platform notes

Blackberry

iPhone

  • Simulator files folder - run search for RhoLog.txt from the drive root. Files are placed inside simulator folder.

Windows Mobile

  • Device/simulator files folder root - Program Files/<app name>/rho

System class

System ruby class provides access to system specific information.

System.get_property(property) #=> returns value of a named system property.
  • platform - APPLE, BLACKBERRY, WINDOWS, ANDROID, SYMBIAN
  • has_camera
  • screen_width
  • screen_height
  • has_network
  • phone_number

It will be extended with other properties which will detail system capabilities, such as availability of camera, or gps, or other system specific properties.

System.has_network #=> Obsolete, use get_property; returns true if phone has network connection
# Due to capabilities of particular OS-es this call implemented only for iPhone and Android
# Developers MUST use this on iPhone to pass on the AppStore.
 
System.get_screen_width  #=>  Obsolete, use get_property; returns device screen width
System.get_screen_height #=>  Obsolete, use get_property; returns device screen height

Sample

See layout.erb of system API sample application for some of the examples of how to use System class.

Advanced Topics

Supported extensions and libraries

To keep rhodes lightweight we didn't provide extensions and libraries beyond core Ruby functionality. Examples of the features that either aren't necessary in Rhodes or we haven't provided for space reasons are: web services, XML, pluralization, YAML, etc.

Our C/C++ implementation based on original Ruby C code, 1.9 release.
Our Java implementation is based on XRuby, which supports Ruby 1.8 (We didn't use JRuby because it is substantially bigger and required version of java which is not available on most of the target mobile platforms).

Both implementations support such core classes and module as:

BasicObject, Object, Module, Class, Integer, Float, Numeric, Bignum, Rational, Complex, Math, String, StringScanner, StringIO, 
Array, Hash, Struct, Regexp, RegexpError, MatchData, Data, NilClass, TrueClass, FalseClass, Comparable, Enumerable, Enumerator,
Converter, Marshal, IO, Dir, Time, Date, Signal, Mutex, Thread, ThreadGroup, Process, Fiber, FiberError, Method, UnboundMethod, 
Binding, RubyVM, GC, Exception, SystemExit, fatal, SignalException, Interrupt, StandardError, TypeError, ArgumentError, IndexError, 
KeyError, RangeError, ScriptError, SyntaxError, LoadError, NotImplementedError, NameError, NoMethodError, RuntimeError, 
SecurityError, NoMemoryError, EncodingError, CompatibilityError, SystemCallError, Errno, ZeroDivisionError, FloatDomainError, 
IOError, EOFError, ThreadError

We are in the process of adopting rubinius specs to test ruby compatibility across different platforms.

Localization

See rhodes-system-api-samples as an example.
Rhodes use localization_simplified library to support non-English languages. Add to build.yml:

  extensions: ["rholang"]

Note: in case of several extensions, insert space after extension name and comma:

  extensions: ["rholang", "net-http"]

Create utf-8 encoded file in app :

  <app_folder>/app/lang/lang_<lang_id>.rb

This file will be automatically loaded by rhodes based on current locale. For Example create lang_en.eb:

  module Localization
    Views = {
      :greeting          => "This is test"
    }
  end

And use this string in the view:

  • <%= Localization::Views[:greeting] %>
Details
  • All non-ascii symbols should be utf-8 encoded
  • To get current locale on the phone use System.get_locale method. It returns 'en', 'de' etc locale id's
  • To show localized Date and Time:
  Time.now.to_formatted_s(:long)
  Time.now.strftime("%B %d, %Y %H:%M") # all names will be localized
  Date.today.to_formatted_s(:long)
  Date.today.strftime("%B %e, %Y") # all names will be localized
  • To show currency:
  Rho::NumberHelper.number_to_currency #see rails analog for details

net/http(s) library support

Support discontinued since 1.5. Use AsyncHttp instead

Add to build.yml:

  extensions: ["net-http"]

Note: in case of several extensions, insert space after extension name and comma:

  extensions: ["rholang", "net-http"]

Http example:

   require 'net/http'
   Net::HTTP.get_print 'www.example.com', '/index.html'

Https example(not supported on Windows Mobile as of 1.4 ):

   require 'net/https'
   strurl = 'https://www.paypal.com/'
   uri = URI.parse(strurl)
   http = Net::HTTP.new(uri.host, uri.port)
   http.use_ssl = uri.scheme == 'https'
   http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl? and System::get_property('platform') != 'Blackberry'
   http.start {
     http.request_get(uri.request_uri) { |res| 

@get_result = res.body

     }
   }

See also rhodes-system-api-samples

json library support

Support discontinued since 1.5. Use Rho::AsyncHttp and Rho::JSON.parse instead

Add to build.yml:

  extensions: ["json"]

Note: in case of several extensions, insert space after extension name and comma:

  extensions: ["json", "net-http"]

Ruby code example:

   require 'json'
   JSON.parse("[{\"count\":10}]")

See also rhodes-system-api-samples

rexml library support

Add to build.yml:

  extensions: ["rexml", "set"]

Ruby code example:

   require 'rexml/document'
   file = File.new("bibliography.xml")
   doc = REXML::Document.new(file)
   puts doc

Detailed notes on Ruby standard library support

For iPhone the Date class is supported

   require 'date'
   puts Date.today.to_s

For Blackberry Date is still not supported. Use this instead:

   Time.now.strftime('%Y-%m-%d')

Adding Libraries to Your Rhodes Application

During the course of your app development you might need to add an external ruby library with extra features that the rhodes framework doesn't provide. While we don't guarantee that all ruby libraries will work on the mobile device, you can follow the steps below to add and test libraries as needed.

In Rhodes, the require path is relative to the "app" subdirectory, since this is what gets bundled with the rhodes client.

  • Assuming your application is called "mynewapp", create a directory under app called lib (or whatever you wish to call it):
cd mynewapp
mkdir app/lib
  • Add your ruby files to this directory:
cp /path/to/my_lib.rb app/lib/my_lib.rb
  • Now, in your application (controller.rb for example), you can load this library like the following:
require 'rho/rhocontroller'
require 'lib/my_lib'
 
class TicketController < Rho::RhoController
  def use_lib
    @a = MyLib.new
    ...
  end
end
  • Please note that "rubygems" are not loaded on the device Ruby VM because of size constraints, therefore all third-party ruby library source files must be put into the lib directory as described above.

Adding Libraries to Rhodes Framework

There are two ways to add Ruby libraries to the Rhodes framework, essentially dependent upon how you choose to build your Rhodes application.

If you are using Rhodes via the RubyGems installation, you must add external Ruby libraries to your RubyGems installation directory for the 'rhodes-framework' gem. Your RubyGems installation directory can be found with `gem env` in a terminal.

  • For example, a user on Linux might place additional libraries in the following directory:
/usr/local/lib/ruby/gems/1.8/gems/rhodes-x.x.x/lib/framework
  • Similarly, a user on Mac OSX 10.5 might place them here:
/Library/Ruby/Gems/1.8/gems/rhodes-x.x.x/lib/framework
  • For Windows, this location might be:
C:/ruby/lib/ruby/gems/1.8/gems/rhodes-x.x.x/lib/framework


  • If you are using a clone of the Rhodes Git repository, you can put additional libraries in the following directory (preferably on your own github fork):
<rhodes-clone>/lib/framework


Including the library into your application is simple once the library is in the proper directory.

  • Assuming the library has been added to the correct location, require the library from a controller in your Rhodes application:
require 'libname'


You can now use the added library to access additional functionality not provided by the Rhodes framework.

NOTE: Once again, it should be mentioned that not all libraries are guaranteed to work with Rhodes.

BlackBerry Browser Support

Check out the BlackBerry Browser Version 4.2 Content Developer Guide for the HTML, CSS and JavaScript that is supported. http://docs.blackberry.com/eng/deliverables//1143/browser_devguide.pdf

Testing/Logging/Reporting/Profiling

Logging

There are two methods to log messages.

From any controller you can log using the methods app_info and app_error. These methods take a string and will write that to rholog.txt with the category of your controllers name.

There also is a logging class called RhoLog. This class has methods info and error which take 2 strings. The first string is the category, the second string is the message.

In rholog.txt the lines appear as follows:

 <Timestamp> <category> | <message>


Debugging

You can debug your Rhodes app running on OSX using the Rhodes Debugger


Testing

How we test the framework here

Adding Unit Tests

When you generate a model, you will now get an _spec.rb file generated along with your controller.

Generating with model generator:
     ....
     [ADDED]  app/Person/person_spec.rb


This file contains tests for your controller and is in the mspec format: rubyspec.org

describe "Person" do
  #this test always fails, you really should have tests!

  it "should have tests" do
    true.should == false
  end
end

We use this test format internally as well. You can see our specs for the core framework here which use many more functions of mspec.

To run these tests however, you need the testing framework to be included in your app. To add this, you would run the rhogen task in your application folder:

 rhogen spec

You will then see the mspec framework added to your application:

Generating with spec generator:
     [ADDED]  app/SpecRunner
     [ADDED]  app/mspec
     [ADDED]  app/spec
     [ADDED]  app/fileutils.rb
     [ADDED]  app/mspec.rb
     [ADDED]  app/spec_runner.rb


You are now ready to run the tests. Simply add a link to the SpecRunner controller, and you will get a summary of number of passing/failing tests

In your index.erb:

<li><a href="SpecRunner">Run tests</a></li>

A summary of the results will be displayed on the screen.

Detailed results will be displayed in your rholog.txt:

I 01/15/2010 16:36:33 b0185000                  APP| FAIL: Product - Expected true
 to equal false

apps/app/mspec/expectations/expectations.rb:15:in `fail_with'
apps/app/mspec/matchers/base.rb:8:in `=='
apps/app/Product/product_spec.rb:5:in `block (2 levels) in <main>'
...

And finally, a summary will be printed in rholog.txt as well:

I 01/15/2010 16:36:33 b0185000                  APP| ***Total:  3
I 01/15/2010 16:36:33 b0185000                  APP| ***Passed: 1
I 01/15/2010 16:36:33 b0185000                  APP| ***Failed: 2

Device Support

Rhodes supports the following smartphone device releases.

BlackBerry

BlackBerry 4.2, 4.5, 4.6, 4.7, 5.0

Windows Mobile

Windows Mobile 6.1 Professional, 6.0 Standard

Android

Android 1.5, 1.6 and 2.0

iPhone

All versions of iPhone 3.0 or greater, iPad

Release Process

http://wiki.rhomobile.com/index.php/Release

Personal tools