Wednesday, December 7, 2011

Tips to Avoid Titanium Desktop Errors and Crashes

My P2P-Docs project is a desktop app currently written in Titanium where I can code the UI in HTML and JavaScript and the business logic in Ruby.  Unfortunately, it's very prone to errors and crashes when things get complicated; fortunately, the problems I've seen have been in the interface between JavaScript and Ruby, and I feel like I can keep progressing with this platform if I follow these conventions.
  • Only pass strings between JavaScript and Ruby, encoding in JSON strings if necessary.
    • In JavaScript, call Ruby methods with String arguments.  JavaScript ints become Ruby Floats, so just use Strings to avoid potential confusion; JSON arguments will probably work and become hashes, but then you'll have to beware of the types of all the nested objects... if I need multiple values then I either supply them as more arguments or I call the Ruby method multiple times.
    • In Ruby, check arguments for RubyKObject type.  Sometimes arguments don't have the expected String type, so you'll want to convert them:

      if (term.class.name == "RubyKObject")
        term = term.toString()
      end

    • In Ruby, return String objects; if necessary, encode in JSON.  Here's a simplistic method that works for me:

      # takes an argument which is any nesting of String, Array, Hash (and nil)
      # return a String holding JSON representation of the argument
      def self.strings_arrays_hashes_json(arg)
        if (arg == nil)
          "null"
        elsif (arg.class.name == "String")
          result = arg
          result = result.gsub("\"","\\\"")
          result = result.gsub("\\","\\\\")
          result = result.gsub("\/","\\/")
          result = result.gsub("\b","\\b")
          result = result.gsub("\f","\\f")
          result = result.gsub("\n","\\n")
          result = result.gsub("\r","\\r")
          result = result.gsub("\t","\\t")
          "\"" + result + "\""
        elsif (arg.class.name == "Array")
          recurse = arg.map { |elem| strings_arrays_hashes_json elem }
          "[" + recurse.join(", ") + "]"
        elsif (arg.class.name == "Hash")
          hashes = arg.to_a.map { |key, val|
            "\"#{key}\":#{strings_arrays_hashes_json(val)}" }
          "{" + hashes.join(", ") + "}"
        else
          "#{arg}"
        end
      end
      

  • In JavaScript, call any Ruby methods before playing with the DOM.  I've found that, if I have a lot of DOM manipulation, Ruby calls will work up to some point and then WHAM! after that point you get errors or crashes.

BTW, the direct-to-console debugging is your friend; the Ruby puts calls may not get flushed immediately, but this will:
Titanium.API.print("stuff\n");

When you start working this way, you'll notice that you're really pushed into a paradigm where you do simple visual manipulations in the browser as much as possible, but for any substantive changes to data you do a page-submit to pass the values to the same page, then you run your logic during the page initialization and then you rerender the whole page.  It's unfortunate, but now I never have troubles.  (... at least very few!  Just kidding.  I haven't had any mysterious Ruby errors or app crashes wherever I take these approaches.)

BTW, every time I hit a glitch, I look more closely at other approaches... but since I figure every tool will have its issues, I'm still sticking with the devil I know.  Feel free to ask me later if I'm still sticking to it.