Cross-Platform mobile dev, pure badassery – pt 2

By | October 15, 2012

This is part 2 of a 5-part series in which I explore and demonstrate cross-platform app creation for mobile and desktop platforms using iFactr, an enterprise-grade framework, and Monocross, the open-source core of iFactr. The other articles are easily accessible at the bottom of each post.

In my previous post, I showed you how quickly and easily you can create one central application (business logic, controller) layer and then have iFactr automatically wrap that for various platform/targets, called containers.

Today, we’ll dive a bit deeper and make the application just a bit more than “Hello World”. I’m also going to add a couple more platforms for further emphasis on just how cool this technology is.

If you’re an independent developer and want something that affords you the business-logic abstraction of iFactr, check out Monocross and the book written by its authors, some of my colleagues here at ITR.

Popcorn ready? Good.

Heading back in to our main MonkeySpace2 iFactr application, let’s open up Layer1.cs, currently it looks like this:

	public class Layer1 : Layer
	{
		public override void Load (Dictionary<string, string> parameters)
		{
			// TODO: set layer title.&nbsp; This text will appear on the header of your layer.
			Title = "MyLayer";

			// TODO: construct your layer from iFactr.Core controls.
			iBlock&nbsp;block = new iBlock ();
			block.Append ("Hello World!");
			Items.Add (block);
		}
	}

but let’s evolve this in to something with a bit more substance. First, for ease of use let’s create a Singleton data class that facilitates accessing the public data from the MonkeySpace API:

	internal class Data
	{
		private static Data _instance;
		public static Data Instance
		{
			get
			{
				if (_instance == null) {
					_instance = new Data ();
				}
				return _instance;

			}
		}

		Data ()
		{
			Speakers = SerializerFactory.Create<Speaker> (SerializationFormat.JSON).DeserializeList (Network.Get ("http://monkeyspace.org/data/speakers.json"));
			Sessions = SerializerFactory.Create<Session> (SerializationFormat.JSON).DeserializeList (Network.Get ("http://monkeyspace.org/data/sessions.json"));
			Schedule = SerializerFactory.Create<Schedule> (SerializationFormat.JSON).DeserializeObject (Network.Get ("http://monkeyspace.org/data/schedule.json"));
		}

		public List<Speaker> Speakers { get; set; }

		public List<Session> Sessions { get; set; }

		public Schedule Schedule { get; set; }
	}

Of course we also have to include the definitions for Speaker, Session, and Schedule:

	public class Speaker
	{
		public int Id { get; set; }
		public string Name { get; set; }
		public string TwitterHandle { get; set; }
		public string HeadshotUrl { get; set; }
		public string Bio { get; set; }
	}

	public class Session
	{
		public int Id { get; set; }
		public string Title { get; set; }
		public string Location { get; set; }
		public DateTime Begins { get; set; }
		public DateTime Ends { get; set; }
		public Speaker SessionSpeaker { get; set; }
	}

	public class Schedule
	{
		public List<Day> Days { get; set; }
	}

	public class Day
	{
		public DateTime Date { get; set; }
		public List<Session> Sessions { get; set; }
	}

Next, let’s use this data in Layer1 to show a list of the speakers pulled from the API:

		public override void Load (Dictionary<string, string> parameters)
		{
			var list = new iList ();
			foreach (var s in Data.Instance.Speakers) {
				var entry = new ShopItem (string.Concat ("Speakers/", s.Id), s.Name, s.Bio, s.TwitterHandle)
				{
					Icon = new iFactr.Core.Controls.Icon (string.Concat ("http://monkeyspace.org", s.HeadshotUrl))
				};
				list.Add (entry);
			}

			this.Items.Add (list);
		}

And now for a little explanation:

In the Data class, we are making use of some iFactr constructs (contained in a Utilities DLL which we’re now referencing) that make it very easy to perform an HTTP GET on a RESTful service. Namely, these are the Network.Get(url) calls you see above. We then take the resulting string value retrieved by the call, and Deserialize it in to a List of Type T where T is the Generic Type specified in the Create() call off SerializerFactory. Note the difference between the 3rd line and the others. The first two are explicitly calling out that they’ll be deserializing a list of type T, where the 3rd one explicitly states it’ll be deserialized in to a single instance of type T. Looking at the names of the Properties on our Data class we can pretty easily distinguish why.

In the Layer, we’re pulling the data from the Speakers collection, looping through each of them, and creating a “ShopItem” for each speaker. This item is most easily described as what you’d see in a listing of products on something like Amazon.com, it contains an icon, main text, sub text, and a headline. So this is a pretty good one to use to list out a quick synopsis of each speaker at MonkeySpace this year.
While we loop through and create these ShopItems from each speaker, we’re also adding them to an iList. This is an object that is suited to list items on a screen. Think of it as perhaps something like a Vertical StackPanel for all my WP7 readers. But remember, iFactr is going to take this iList and convert it in to whatever is appropriate for the platform targeted in our container via the “bindings”.
Finally, we take the iList and add it to the Items collection on the layer, as being the only item, so our layer will only have a list of speakers present.

With that in mind, let’s make a few tweaks:

public class SpeakerList : Layer

and

		public override void Load (Dictionary<string, string> parameters)
		{
			this.Title = "Speakers";

much better. Rename the file to match while you’re at it. Here’s how our project looks now:

image

Notice Data.cs also in the project, containing the aforementioned Data class to pull the data from the MonkeySpace APIs as well as the classes under Models which contain all the… models along w/ the Utilities DLL to run the data fetching.

Build it.

image

What the frack… let’s have a look:

image

So…

image

but why? What is this “Navigation Map” and what’s it doing w/ references to my Layers?

If you’ve worked with MVC before, this is going to come very easy to you. However if you haven’t, I’ll take a bit of time to explain it.

The NavigationMap holds a list of URI references, along with the Layers that each URI should instantiate when it is navigated to. In this manner, we often see things like this w/ implementations of iFactr:

// Add navigation mappings
NavigationMap.Add ("TripList", new TripList ());
NavigationMap.Add ("TripList/{ActionType}", new TripList ());
NavigationMap.Add ("TripDetail/{TripID}", new TripDetail ());
NavigationMap.Add ("TripDetail/{TripID}/{Action}", new TripDetail ());
NavigationMap.Add ("TripDetailDirection/{Step}", new TripDetail_Direction ());

so let’s dissect this a bit.

In the first entry, we’re saying that if you Navigate to “TripList”, a new layer of type TripList() will be used for display. Similarly, the second line will all send you to a TripList layer. But note the part of that URI surrounded in {}. What this does is tells the navigation engine that whatever goes in that part of the URI needs to be added to the parameters dictionary with a key of that name, and a value of whatever was actually in the URI. For instance:
If you navigate to TripList/4, then in the Load() method where you have Dictionary parameters, you’ll have an entry in that dictionary of “ActionType”, “4” where Key = “ActionType” and Value = “4” (yes, as a string).

Knowing this, you can quickly deduce how this gets used throughout the application and can imagine how we’ll end up using it in our MonkeySpace App.

Now that we’ve wired up the SpeakerList layer, we can run our app. Let’s have a go at Webkit first:

image

So here you see a few of the names on the list of speakers, and I’ve hovered over my good buddy Ananth’s entry to show you the URL that will be navigated to if you were to click there. That URL corresponds to this area of our code:

string.Concat ("Speakers/", s.Id),

That’s where we set the navigation URL to Speakers/ID. But remember, when we navigate somewhere it has to be accounted for in our Navigation Map. So, let’s go down this path now.

First we’ll create a new layer to display speaker information

	class SpeakerDetail : Layer
	{
		// Add code to initialize your layer here.&nbsp; For more information, see http://www.factr.com/documentation
		public override void Load (Dictionary<string, string> parameters)

now we’ll populate it

			var speakerId = int.Parse (parameters["id"]);

			var model = Data.Instance.Speakers.Find (p => p.Id == speakerId);

			var firstName = model.Name.Split (' ')[0];

			var headshot = new iPanel ();
			headshot.InsertImageFloatLeft (string.Concat ("http://monkeyspace.org", model.HeadshotUrl), "110", "110");
			headshot.AppendLine ();
			headshot.AppendSubHeading (model.Name);

			var list = new iList ();
			list.Text = string.Concat ("Connect with ", firstName);
			list.Add (new SubtextItem ("Tweet".AppendPath (model.Id.ToString ()), "Tweet", string.Concat ("@", model.TwitterHandle))
			{
				Icon = new iFactr.Core.Controls.Icon ("http://aux.iconpedia.com/uploads/1061260918.png")
			});
			list.Add (new SubtextItem&nbsp;(string.Empty, "Sessions", string.Concat ("with ", firstName))
			{
				Icon = new iFactr.Core.Controls.Icon ("Assets/icon.png")
			});

			var bio = new iPanel ();
			bio.Title = string.Concat ("About ", firstName);
			bio.AppendSubHeading (model.Bio);

			this.Items.Add (headshot);
			this.Items.Add (list);
			this.Items.Add (bio);

And of course let’s walk through this.

First we pull the ‘id’ parameter out of the parameters bag passed to the Load() method. Remember, this bag is populated by parsing the Navigation Map entry and the URI that was navigated to.

Next, we find the speaker matching this ID from our model.

Then we pull out the first name.

Next we create an iPanel item that will be the head shot of the speaker. You can see that iPanel behaves a lot like an HTML block, but iFactr has customized its usage a bit and so offers some specific functionality as far as how you can insert images, float them, append text, etc.

Next we create a new list and add some text to it which will serve as a kind of “header” look when it’s rendered on various clients. We then add sub text items to this list allowing users to Tweet to the speaker, or see sessions the speaker is conducting.

Then we add another panel, give it an “About” title, and slap in the speaker’s bio information.

Finally, we add all these items to the collection of the layer so they’re ran through the rendering engine.

But wait! We also have to add the Speakers/{id} entry to our navigation map so that when the link is clicked from the speakers list, it goes to this page to show further detail!

			NavigationMap.Add ("Speakers/{id}", new SpeakerDetail ());

And we’re done. With code only added to our central Application project, nowhere else.

iPhone:

imageimage

On an iPad, we can really see the concept of “Master” and “Detail” view I spoke about in my last post:

image

Console:

imageimage

Android tablet:

imageimage

Webkit:

imageimage

If this doesn’t suffice as Ananth’s 15 minutes of fame… well… perhaps his session(s) at MonkeySpace will :)

Ah yes, I almost forgot (ok not really) …

That, my friends, is Windows Phone 7 :)

*boom goes the dynamite*