Windows Azure Mobile Services "Rent a Home" Sample, Part 2: UI and Data

March 31, 2013

In the previous installment, we saw the general UI of the application. We’ll now turn to see how that UI was implemented on all four platforms. If you’re looking for a quick start or documentation on Mobile Services, you should take a look at the Windows Azure Mobile Developer Center.

Android
The model class for apartment listings on Android is the following:

public class Apartment implements Serializable {
  private int id; 
  private String address;
  private boolean published;
  private int bedrooms;
  private double latitude;
  private double longitude;
  private String username;
  //Getters and setters omitted for brevity
}

Note that the class doesn’t have to be Java-serializable, but its field types are restricted to what Mobile Services currently supports. The Android SDK relies on the gson library (Google’s fast and extensible JSON serializer) to serialize objects on the wire. The reason my class implements the market Serializable interface is so that it can be passed across activities.

The main activity on Android loads a simple layout that consists of a ListView, bound to a custom adapter (derived from ArrayAdapter<Apartment>). It displays three TextViews for each apartment listing: the apartment’s address, the number of bedrooms, and the user who submitted that apartment listing.

public class ApartmentAdapter extends ArrayAdapter<Apartment> {
  public ApartmentAdapter(Context context,
                          List<Apartment> apartments) {
    super(context, R.layout.apartment_row, apartments);
  }
  
  @Override
  public View getView(int position, View row, ViewGroup parent) {
    Apartment apartment = getItem(position);
    if (row == null) {
      LayoutInflater inflater = LayoutInflater.from(getContext());
      row = inflater.inflate(R.layout.apartment_row, null);
    }
    TextView address = (TextView)row.findViewById(R.id.txtAddress);
    TextView username = (TextView)row.findViewById(R.id.txtSecondary);
    TextView bedrooms = (TextView)row.findViewById(R.id.txtBedrooms);
    address.setText(apartment.getAddress());
    username.setText("added by " + apartment.getUserName()));
    bedrooms.setText(Integer.toString(apartment.getBedrooms()));
    return row;
  } 
}

When the activity initializes, it retrieves a list of apartment listings from the Mobile Services backend, and binds it to the UI using the custom adapter:

mobileService = new MobileServiceClient(
            MOBILESERVICE_URL, MOBILESERVICE_APIKEY, this);
apartmentTable = mobileService.getTable("apartment", Apartment.class);
apartmentTable
  .where()
  .field("published").eq(true).and()
  .field("bedrooms").gt(1)
  .orderBy("bedrooms", QueryOrder.Descending)
  .execute(new TableQueryCallback<Apartment>() {
    public void onCompleted(List<Apartment> items, int count,
                            Exception exception,
                            ServiceFilterResponse response) {
      if (exception != null) {
        displayError(exception);
      } else {
        ApartmentAdapter aa = new ApartmentAdapter(this, items);
        listApartments.setAdapter(aa);
      }
    }
  });

To add a new apartment listing, the user taps the menu/action bar “Add” item, and is presented with a dialog that collects the listing’s information and submits it to Mobile Services:

//Dialog view setup omitted for brevity
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Add New Apartment");
builder.setView(innerLayout);
builder.setPositiveButton("Submit", new OnClickListener() {
  public void onClick(DialogInterface dialog, int which) {
    Apartment apartment = new Apartment();
    apartment.setAddress(editAddress.getText().toString());
    apartment.setBedrooms((Integer)spinBedrooms.getSelectedItem());
    apartment.setPublished(true);
    apartmentTable.insert(apartment,
      new TableOperationCallback() /* omitted for brevity */);
  }
});
builder.setNegativeButton("Cancel", null);
builder.create().show();

Finally, when the map activity is invoked, it displays apartment listings using a simple map overlay on top of Google’s MapView (using Google Maps in your application requires an API key, which you obtain online). The overlay relies on the coordinates provided by the server when a new apartment listings is inserted (we’ll see how that happens later). When an overlay item is tapped, the overlay displays a simple dialog with the listing’s details.

public class ApartmentOverlay extends ItemizedOverlay<OverlayItem> {
  private List<OverlayItem> items = new ArrayList<OverlayItem>();
  private List<Apartment> apartments = new ArrayList<Apartment>();
  private Context context;

  public void addApartment(Apartment apartment) {
    Location location = apartment.getLocation();
    items.add(new OverlayItem(new GeoPoint(
      (int) (location.getLatitude() * 1E6),
      (int) (location.getLongitude() * 1E6)),
      "Apartment",
      apartment.getAddress()));
    apartments.add(apartment);
    populate();
  }

  public ApartmentOverlay(Context ctx, Drawable defaultMarker) {
    super(boundCenterBottom(defaultMarker));
    context = ctx;
    populate();
  }

  @Override
  protected OverlayItem createItem(int i) {
    return items.get(i);
  }

  @Override
  public int size() {
    return items.size();
  }

  @Override
  protected boolean onTap(int index) {
    Apartment apartment = apartments.get(index);
    AlertDialog.Builder builder = new AlertDialog.Builder(context);
    builder.setTitle("Apartment");
    builder.setMessage("Address: " + apartment.getAddress() + "\n" +
                       apartment.getBedrooms() + " bedrooms");
    builder.create().show();
    return super.onTap(index);
  }
}

iOS
The Mobile Services framework on iOS does not rely on static types to convey information across the wire. Instead, it uses the NSDictionary class, which contains a collection of key-value pairs. Although my implementation could provide a static Apartment class, which would be “serialized” to and from the NSDictionary representation, I opted to use NSDictionary throughout. If the model were more complex, I might have considered the static type approach.

The main view controller on iOS contains a UITableView that uses the subtitle table view cell style. When the view controller is initialized, it retrieves a list of apartment listings from the Mobile Services backend, and provides it to the UITableView in the UITableViewDelegate‘s numberOfSectionsInTableView:, tableView:numberOfRowsInSection:, and tableView:cellForRowAtIndexPath: methods.

- (void)viewDidLoad {
  [super viewDidLoad];
  self.client = [MSClient clientWithApplicationURLString:kMobileAppURL
                                      withApplicationKey:kMobileAppKey];
  self.table = [self.client getTable:@"apartment"];
  NSPredicate *predicate = [NSPredicate
                            predicateWithFormat:@"published == YES"];
  [self.table readWhere:predicate
             completion:^(NSArray *results, NSInteger totalCount, NSError *error) {
    self.items = [results mutableCopy];
  }];
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView
 numberOfRowsInSection:(NSInteger)section {
    return [self.items count];
}

- (UITableViewCell *)tableView:(UITableView *)tv
         cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  static NSString *CellIdentifier = @"Cell";
  UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier:CellIdentifier
                              forIndexPath:indexPath];
  NSDictionary *apt = [self.items objectAtIndex:indexPath.row];
  cell.textLabel.text = apt[@"address"];
  cell.detailTextLabel.text = [NSString stringWithFormat:@"%d bedrooms",
                               [apt[@"bedrooms"] integerValue]];
  return cell;
}

To add an apartment listing, the user navigates to a secondary view controller that uses a UITableView with static cells to collect the apartment’s address and number of bedrooms. When the user taps “Save”, the secondary view controller creates a new NSDictionary with the apartment’s details, and provides it to its delegate (which is the home view controller), that in turns inserts the apartment listing to the Mobile Services backend:

//In the secondary view controller:
- (IBAction)saveTapped:(id)sender {
  NSDictionary *apartment = @{
    @"address" : self.itemText.text,
    @"bedrooms" : @(self.bedrooms.selectedSegmentIndex+1),
    @"published" : @(YES)
  };
  if ([self.delegate respondsToSelector:@selector(saveApartment:)]) {
    [self.delegate performSelector:@selector(saveApartment:)
                        withObject:apartment];
  }
}

//In the primary view controller:
- (void)saveApartment:(NSDictionary *)apartment {
  [self.navigationController popViewControllerAnimated:YES];

  __weak HomeController *s = self;
  [self.table insert:item completion:^(NSDictionary *result, NSError *error) {
    //Error handling omitted for brevity
    NSUInteger index = [self.items count];
    [s.items addObject:result];
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index
                                                inSection:0];
    [s.tableView insertRowsAtIndexPaths:@[ indexPath ]
                       withRowAnimation:UITableViewRowAnimationTop];
  }];
}

Finally, if the user navigates to the map view controller, it displays the apartment listings using an ApartmentAnnotation class that implements the MKAnnotation protocol — this tells the MKMapView where to display the apartments on the map.

//The MapViewController's viewDidLoad method:
- (void)viewDidLoad {
  for (NSDictionary *apartment in self.apartments) {
    MKAnnotation *annotation = [[ApartmentAnnotation alloc]
                                initWithApartment:apartment];
    [self.mapView addAnnotation:annotation];
  }
}

//The ApartmentAnnotation class:
@interface ApartmentAnnotation : NSObject <MKAnnotation>

- (id)initWithApartment:(NSDictionary *)apartment;

@end

@implementation ApartmentAnnotation

- (id)initWithApartment:(NSDictionary *)apartment {
    if (self = [super init]) {
        self.apartment = apartment;
    }
    return self;
}

- (NSString *)title {
    return self.apartment[@"address"];
}

- (NSString *)subtitle {
    return [NSString stringWithFormat:@"%d bedrooms",
            [self.apartment[@"bedrooms"] integerValue]];
}

- (CLLocationCoordinate2D)coordinate {
    return CLLocationCoordinate2DMake(
      [self.apartment[@"latitude"] doubleValue],
      [self.apartment[@"longitude"] doubleValue]
    );
}

@end

Windows Phone 8
The Windows Phone Azure Mobile Services SDK supports typed data, much like the Android version. The Apartment class is very similar to the Android version, and the [DataTable]/[DataMember] attributes help with customizing the serialized JSON output to fit the backend model.

[DataTable(Name = "apartment")]
public class Apartment
{
  public int Id { get; set; }

  [DataMember(Name = "address")]
  public string Address { get; set; }

  [DataMember(Name = "published")]
  public bool Published { get; set; }

  [DataMember(Name = "bedrooms")]
  public int Bedrooms { get; set; }

  [DataMember(Name = "latitude")]
  public double Latitude { get; set; }

  [DataMember(Name = "longitude")]
  public double Longitude { get; set; }

  [DataMember(Name = "username")]
  public string UserName { get; set; }
}

The Windows Phone UI uses the Pivot control, which enables swipe navigation from the apartments list to the map that displays them, and to an additional page that is used to add new apartment listings. The apartment list is a simple ListBox control that has a data template with a few TextBlocks. The Pivot control is set up as follows:

<phone:Pivot Title="RENT A HOME">
  <phone:PivotItem Header="apartments">
    <StackPanel>
      <ListBox x:Name="listApartments">
        <ListBox.ItemTemplate>
          <DataTemplate>
            <StackPanel Orientation="Vertical">
              ... three TextBox controls omitted for brevity ...
            </StackPanel>
          </DataTemplate>
        </ListBox.ItemTemplate>
      </ListBox>
    </StackPanel>
  </phone:PivotItem>
  <phone:PivotItem Header="map">
    <maps:Map x:Name="mapApartments" CartographicMode="Hybrid"
                                     LandmarksEnabled="True" />
  </phone:PivotItem>
  <phone:PivotItem Header="new">
    <StackPanel>
      ... standard UI for adding listings omitted for brevity ...
    </StackPanel>
  </phone:PivotItem>
</phone:Pivot>

When the page loads, the application fetches apartment listings from the mobile service backend and binds the resulting list to the ListBox. The LINQ-like syntax is very convenient for expressing queries, such as retrieving only published apartment listings, and the C# support for async methods makes it very easy to perform this operation asynchronously using the await operator:

var items = await MobileService.GetTable<Apartment>()
                               .Where(a => a.Published == true)
                               .ToListAsync();
listApartments.ItemsSource = items;

The Windows Phone application uses the Nokia Maps control, which is the recommended maps framework for Windows Phone 8 (Microsoft.Phone.Maps namespace). Apartment listings are displayed on top of the map as simple overlays, that, when tapped, display a message with the apartment’s details and zooms in to the listing’s location on the map:

mapApartments.Layers.Clear();
MapLayer layer = new MapLayer();
foreach (Apartment apartment in apartments)
{
  MapOverlay overlay = new MapOverlay();
  overlay.GeoCoordinate = new GeoCoordinate(
                  apartment.Latitude, apartment.Longitude);
  overlay.PositionOrigin = new Point(0, 0);
  Grid grid = new Grid
  {
    Height = 40,
    Width = 25,
    Background = new SolidColorBrush(Colors.Red)
  };
  TextBlock text = new TextBlock
  {
    Text = apartment.Bedrooms.ToString(),
    VerticalAlignment = VerticalAlignment.Center,
    HorizontalAlignment = HorizontalAlignment.Center
  };
  grid.Children.Add(text);
  overlay.Content = grid;
  grid.Tap += (s, e) =>
  {
    MessageBox.Show(
      "Address: " + apartment.Address + Environment.NewLine +
      apartment.Bedrooms + " bedrooms",
      "Apartment", MessageBoxButton.OK);
    mapApartments.SetView(overlay.GeoCoordinate, 15,
                          MapAnimationKind.Parabolic);
  };
  layer.Add(overlay);
}
mapApartments.Layers.Add(layer);

Windows 8
The Windows 8 implementation is strikingly similar to the Windows Phone one. In fact, the latest release of Windows Azure Mobile Services consolidates most of the .NET frameworks into a single portable class library, with only minor parts provided as separate auxiliary assemblies (this was enabled by introducing much-awaited portable class library support for the HttpClient class). This means that our application’s model could be placed in a portable class library as well, and reused from all supporting .NET platforms. (This is not currently the case.)

Because of the larger screen estate, the Windows 8 application doesn’t have multiple pages — the entire UI can fit on the screen. The apartment listings are bound to a ListView control, and the map on the right displays them alongside.

The code responsible for manipulating the model is not very interesting, but the maps framework is worth mentioning. The Windows 8 application uses the Bing Maps control, which requires an API key that you obtain online. In the Bing Maps parlance, overlays are called pushpins, and here’s how you place them on the map:

mapApartments.Children.Clear();
foreach (Apartment apartment in apartments)
{
  Pushpin pushpin = new Pushpin
  {
    Text = apartment.Bedrooms.ToString()
  };
  mapApartments.Children.Add(pushpin);
  Location location = new Location(
                    apartment.Latitude, apartment.Longitude);
  MapLayer.SetPosition(pushpin, location);
  pushpin.Tapped += (s, e) =>
  {
    mapApartments.SetView(location, 15);
  };
}

Server Scripts
On the backend side, we need a server script to enrich our apartment listing with geographical coordinates. The user provides an address, such as “One Microsoft Way, Redmond WA”, which we have to convert to a latitude-longitude pair. (This process is called geocoding.)

Even though each mobile platform supports some geocoding service (for example, on Android it’s the Google geocoding service, exposed through the Geocoder class), it would be a fairly bad idea to perform geocoding on the client. One reason is that the geocoding process is slow and expensive. Another reason is that on each platform, geocoding the same address might lead to a different result, which is wildly unintuitive. This is why this work is best offloaded to the service’s backend.

Specifically, here’s the relevant part from the insert script on the apartment table, which performs geocoding with Google’s free geocoding API using the request Node.js module provided by Windows Azure Mobile Services:

function insert(item, user, request) {
  var reqModule = require('request');
  var base = 'http://maps.googleapis.com/maps/api/geocode/json?sensor=false'; 
  var what = escape(item.address);
  reqModule(base + '&address=' + what,
    function(error, response, body) {
      if (!error) {
        var geoResult = JSON.parse(body);
        var location = geoResult.results[0].geometry.location;
        item.latitude = location.lat;
        item.longitude = location.lng;
      }
      //Continue processing the request, omitted for brevity
    }
  );
}

Summary
This concludes our whirlwind tour of the Rent a Home application, more specifically its UI-related parts and the maps frameworks used. In the next installment, we’ll look at how authentication (with Twitter) was integrated into the app on all four platforms, and how it was then used to associate apartment listings with the name of the user who added them.


I am posting short links and updates on Twitter as well as on this blog. You can follow me: @goldshtn

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>