A Mobl Blog App 1

May 31, 2011

Today’s exercise: build an application in mobl for this blog, which supports offline reading of blog posts. I’ll record my progress here.

Setup

I created a mobl project using the Eclipse wizard and put the project directory in the mobile subdirectory of the blog project.

application mobile

import mobl::ui::generic

screen root() {
	header("mobile blog")
	"test"
}

For the time being I extended my Makefile to copy the files generated in the www subdirectory to the tomcat directory for the WebDSL application. I need to find a better deployment route later.

Deployment

So, the target can be configured in the config.mobl file.

configuration
title "Blog"
release output "../WebContent"
output "www"

For quick deployment I’m still sticking with copying the contents of www/ to my tomcat dir for now.

Reading Data from WebDSL Application

The next step is testing the connection with the WebDSL application on the server. First of all I create a basic service for the blog:

module blog/blog-service
imports blog/blog-model
section blog
  extend entity Blog { 
    function json():  JSONObject {
      var obj := JSONObject();
      obj.put("id", id);
      obj.put("title", title);
      obj.put("about", about);
      obj.put("description", description);
      obj.put("modified", modified);
      return obj;
    }
  }
  service apiblog() {
    return mainBlog().json();
  }

Now the URL http://localhost:8080/blog/apiblog produces

{"id":"795fe67c-3e3a-46a2-b20b-c18c2b9c8d45",
 "title":"Rethinking | Eelco Visser",
  "description":"",
  "about":"This is the blog of [[root()|Eelco Visser]].",
  "modified":"2011-05-07 13:33:13.0"
}

Then a basic mobl app that reads and displays these data:

application mobile
import mobl::ui::generic
screen root() {
	var title = "blog"
	header(title)
	var blog = async(Blog.data())
	whenLoaded(blog) {
		label(blog.about)
		label(blog.description)
	}
}
service Blog {
	resource data(): JSON {
		uri = "/apiblog"
	}
}

However, when I run this, I get a non-terminating “Loading” screen. How do we debug this? … Aha, the problem is that I specified the uri as /apiblog whereas it is actually published at /blog/apiblog. So if I fix the service definition as follows, I get actual data from the server in the mobl app:

service Blog {
	resource data(): JSON {
		uri = "/blog/apiblog"
	}
}

However, this is somewhat unfortunate. I would really like to specify the uri relative to the application root of the WebDSL application. While in my test set-up I deploy the WebDSL application at /blog, but in the production version it is a ROOT application and the uri will have to be /apiblog. Will need to figure out how to factor this out later.

At least I have the core of an application up and running. Let’s see if I can display some blog posts.

Post Data

On the WebDSL side I create a function to marshal Posts as JSON objects:

section post
  extend entity Post {
    function json(): JSONObject {
      var obj := JSONObject();
      obj.put("id", id);
      obj.put("number", number);
      obj.put("key", key);
      obj.put("urlTitle", urlTitle);
      obj.put("title", title);
      obj.put("description", description);
      obj.put("content", content);
      obj.put("extended", extended);
      obj.put("created", created);
      obj.put("modified", modified);
      return obj;
    }
  }

This is a bit boring. We have been discussing automating this. However, it is typically not desirable to include all properties in the JSON data. Some control is needed. But it could be syntactically lighter weight.

The following function is due to WebDSL’s lack of polymorphism, which needs to be fixed soon. At least we have function overloading so that we don’t have to invent a new name for each of these maps.

  function json(posts: List<Post>): JSONArray {
    var array := JSONArray();
    for(p: Post in posts) { array.put(p.json()); }
    return array;
  }

Finally, the recent posts can be published as a service:

  service apirecentposts() {
    return json(mainBlog().recentPublicPosts(0, 5));
  }

On the mobl side, I extend the service definition with a recentposts resource:

service Blog {
  resource data(): JSON{ 
    uri = "/blog/apiblog"
  }
  resource recentposts(): JSON {
    uri = "/blog/apirecentposts"
  }
}

Listing Recent Posts

Mobl Blog 1

Now we can change the root screen of the mobl app to display a list of titles of recent posts:

screen root() {
	var title = "blog"
	header(title)
	var posts = async(Blog.recentposts())
	whenLoaded(posts) {
	  list(post in posts) {
	    item{ label(post.title) }
	  }
	}
}

Mapping to Types

In order to get a better handle on the data obtained from the server, we map it to proper mobl entities. First we define an entity Post to mirror the entity on the WebDSL side:

entity Post {
  number      : Num
  key         : String // symbolic identity
  urlTitle    : String 
  title       : String (searchable)
  description : String (searchable)
  content     : String (searchable)
  extended    : String (searchable) 
  created     : DateTime 
  modified    : DateTime  
}

Then we define a mapper function that converts the JSON array to a list of Posts:

function postsMapper(json : JSON) : [Post] {
  return json;
}

Finally, we update the recentposts resource to return a list of Posts instead of a JSON object:

service Blog {
  resource recentposts(): [Post] {
    uri = "/blog/apirecentposts"
    mapper = postsMapper
  }
}

Now all references to Post objects in the view are type checked.

Intermezzo: Generating Services

The support for definition and use of services in WebDSL and mobl are catering for unknown applications connecting to the service. But for the case where we are co-developing a WebDSL and mobl application, we can clearly automate much of the mappings defined above. Given a WebDSL data model we can generate the corresponding mobl data model and service definitions.

Master Detail View

Next we want to see more than just the titles of the blog posts. We use the masterDetail control from the mobl library to show a list of blog post titles that can be clicked to reveal the content of each post:

screen root() {
	var title = "blog"
	header(title)
	var posts = async(postsCollection(Blog.recentposts()))
	whenLoaded(posts) { 
	  masterDetail(posts order by created desc, postItem, postDetail) { }
	}
}
control postItem(p: Post) {
  label(p.title)
}
control postDetail(p: Post) {
  label(p.title)
  block{ label(p.content) }
}

Since the masterDetail control expects a Collection<Post> we need a little conversion function (which should not be necessary actually):

function postsCollection(ps: [Post]): Collection<Post> {
  var pcol : Collection<Post> = Collection<Post>();
  foreach(p: Post in ps) { pcol.add(p); }
  return pcol;
}

The result:

<img src=”http://farm4.static.flickr.com/3565/5783562996_1a0102a6de.jpg” width=”266” height=”500” alt=”Screen shot 2011-05-31 at 2.08.23 PM” “>

Screen shot 2011-05-31 at 2.08.31 PM

Unfortunately, the content is not very pretty. The service that we defined sends us the raw markdown. In WebDSL this is automatically displayed as HTML, but mobl doesn’t know about this. We’ll have to fix this.

But it is time for a break. In the next post I’ll continue we’re I’ve left off.

Status: 60 lines of mobl code and 55 lines of WebDSL code, most of which was spent on services on both sides.