28 KiB
layout | title | author | date | categories | color | image | excerpt | language | verticals | geolocation | ||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
post | Building a Xamarin.Forms mobile app that lets Shelfie connect charities with donors | Gavin Bauman | 2016-12-29 |
|
blue | images/feat_ShelfieArchitecture.jpg | See how a hackfest helped Shelfie develop a cross-platform mobile app to expand its solution for publicizing and participating in fundraising opportunities. |
|
|
|
Shelfie and Microsoft teamed up in a hackfest to build a mobile social media platform to create fundraising photo and video campaigns for nonprofits. Its aim is to build communities around a cause, enhancing awareness, engagement, and retention while increasing donations and contributions to the campaigns.
To address the lack of awareness of various fundraising campaigns, a solution was developed to create a mobile application that allows users to view current and future events in their feed that the user can subscribe to and post pictures about. While a beta version of this application existed for iOS, the Shelfie team struggled with aggregating the resources required to build clients for both Android and Windows. The mobile application built at the hackfest is a Xamarin.Forms cross-platform application for Android and Windows that talks to a Ruby on Rails backend migrated from Amazon Web Services (AWS) to Microsoft Azure. The project's repository is hosted in Visual Studio Team Services to allow for future HockeyApp build integration.
Core team
- Brendan Barbato – CEO and Founder of Shelfie, Babson College
- Nathan Geyer – Shelfie Developer, Northeastern University
- Gregory Degruy – Technical Evangelist, Microsoft
- Adina Shanholtz – Technical Evangelist, Microsoft
- Gavin Bauman – Technical Evangelist, Microsoft
Customer profile
Shelfie is a startup born at Babson College in Massachusetts that wants to build a mobile social media platform to create fundraising photo and video campaigns for nonprofits, with the aim of building communities around a cause, and enhancing awareness, engagement, and retention, while increasing donations and contributions to the campaigns.
Brendan Barbato is the student founder and CEO of Shelfie. He's based out of Babson College and has successfully worked with student developers from other universities to build out the beta version of his app.
From the company:
"Shelfie, a mobile platform that facilitates engaging photo campaigns for charities. We aim to enhance awareness, retention, and donor utilization through transparency—where funds are spent and how much. Users will see a list of charities hosting a challenge, submit their best photo, have the option to donate and see how much progress the fundraising campaign has made, vote on other submissions and receive votes, and those with the most votes win cool profile achievements and prize packs from the charity."
Problem statement
Charities are losing money on fundraising events due to a lack of focus on the fundamentals. They face three problems:
- Donor utilization
- Acquiring donors
- Donor retention
In terms of donations, 50% of charities use special fundraising events as a way to raise money and, on average, spend $1.33 to raise $1 at these events (charitywatch.org). Acquisition costs for a donor can be between $1 and $100, with acquisition rates as low as 1% (Smart Annual Giving). Once the charity is able to acquire a donor, the average retention rate of a first-year donor is 27.3%, whereas the overall retention rate of multi-year donors is 58.4%. Charities are spending a lot to acquire new donors, when 72.7% of them are not donating again (Fundraising Effectiveness Project). Ultimately, charities are spending money to lose money, retention is terrible, and it is almost as if charities raise money to raise more money. Shelfie aims to change that. While they have a beta application for iOS, they'd like to target a larger audience by porting their app to Android and iOS.
After research on porting their iOS app to additional platforms, the Shelfie team had two key concerns they needed to address:
- The need for a modular codebase. Upon referencing the iOS project, we determined there was little code we could use to our advantage during the hackfest. We decided to use a Xamarin.Forms Portable Class Library Project to ensure that any services built could be used across all platforms and any future projects.
- The need to migrate to Azure. After discussing with the client, he was impressed by the sheer global presence Azure has that AWS does not. This would also pave the way for building out an authentication system using the Mobile Apps feature of Azure App Service.
Solutions, steps, and delivery
The different components involved in the development of the Shelfie client are:
- Building the Xamarin.Forms cross-platform UI
- Authenticating against the pre-existing API
- Migrating the service from AWS to Azure
Building the Xamarin.Forms cross-platform UI
Shelfie's backend was built in Ruby, with an exposed API that returned JSON. Our team built a Xamarin app on top of this, turning the JSON into a navigatable and beautifully rendered UI, featuring sections customized to the user. Users should be able to sign in to Shelfie and be able to navigate a tabbed page while viewing their custom profile, their participating challenges, and friend notifications.
During our three-day hackfest, we "old-school" architected this UI flow diagram via whiteboard.
UI diagram
![Application Flow Diagram]({{ site.baseurl }}/images/App_Design_Mock_Ups.jpg)
Next, using the Model-View-ViewModel pattern, we designed a tabbed master page that held three content pages:
To properly deserialize the JSON we were getting from the API, we created models that would store the data. The ViewModel would then call the API to store the values into the model, store the model in an Observable Collection that would notify the UI when added, and check that no exceptions were thrown. Finally, the pages would populate values based on the ViewModel bindings.
One issue we ran into was with the "challenges" model. We wanted to display lists of available challenges for users to browse. However, none of the JSON that returned with getting challenges contained thumbnail pictures. To solve this, if a challenge had gone through a "round" (opening it up for user-uploaded pictures), we showed a thumbnail from a submitted round and a generic picture otherwise. We created an identical challenge model with an extra "image" field, which we populated with this logic in the ViewModel.
var challenges = await challengeService.GetChallenges();
foreach (var item in challenges)
{
ChallengeImage challengeImg = new ChallengeImage();
//Check if a challenge round has a pic, otherwise use a default
var thumbnail = item.Rounds.First().Winners.Where(w => w.Submission.VerifiedContent.Thumb.Url != null).FirstOrDefault();
if (thumbnail != null)
{
challengeImg.Image = ImageSource.FromUri(new Uri(thumbnail.Submission.VerifiedContent.Thumb.Url));
}
else
{
challengeImg.Image = ImageSource.FromFile("Books.jpg");
}
challengeImg.Id = item.Id;
challengeImg.Name = item.Name;
challengeImg.Sponsor = item.Sponsor;
challengeImg.Trophies = item.Trophies;
challengeImg.Rounds = item.Rounds;
challengeImg.InvalidNameComplete = item.InvalidNameComplete;
Challenges.Add(challengeImg);
Building the fundraising challenges screen
One of the pages that the user initially sees on the tabbed home screen is a list of fundraising challenges. We rendered these challenges using a Xamarin Forms ListView. As mentioned above, we ran into a bit of an issue with displaying challenges with images. After solving this problem, we realized we were a little limited with the aesthetic options the ListView afforded us; unless we created a custom renderer, we could only scroll through challenges vertically, not horizontally. Still, the benefits given by using a ListView, such as built-in caching, made our application run smoothly.
<StackLayout BackgroundColor="#eee">
<ListView CachingStrategy="RecycleElement" ItemsSource="{Binding Challenges}">
<ListView.ItemTemplate>
<DataTemplate>
<ImageCell ImageSource="{Binding Image}"
Text="{Binding Name}"
Detail = "{Binding Sponsor}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Label Text="{Binding ErrMessage}" VerticalOptions="Center" HorizontalOptions="Center" />
</StackLayout>
Building the noise notifications
When imagining a list of notifications, it's intuitive to display them in a ListView. However, because we wanted to include "Accept" and "Reject" buttons, we had to look into writing our own custom cells. It was tricky figuring out how to include actions on the buttons, but we finally implemented it by writing commands bound and defined in the view model.
<StackLayout>
<Label Text="Noise Feed" HorizontalTextAlignment="Center" />
<ListView ItemsSource="{Binding Noise}" RowHeight="100">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<Image Source="{Binding Image}" Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" Aspect="Fill" />
<StackLayout Orientation="Horizontal" Grid.Row="0" Grid.Column="1">
<Label Text="{Binding Name}"/>
</StackLayout>
<StackLayout Orientation="Horizontal" Grid.Row="1" Grid.Column="1">
<Button Text="Accept" BorderColor="Green" Command="{Binding BindingContext.AcceptEvent, Source={x:Reference NoisePage}" CommandParameter="{Binding Id}"/>
<Button Text="Reject" BorderColor="Red" Command="{Binding BindingContext.RejectEvent, Source={x:Reference NoisePage}" CommandParameter="{Binding Id}"/>
</StackLayout>
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
Creating the user profile
The design of the Shelfie user profile was inspired by the user profiles of Facebook and Instagram. We wanted to make sure that we displayed the right amount of information about the user, while displaying any pictures from challenges the user may have participated in. Creating this aesthetically pleasing page took a lot of precision and thought. We did this with a creative nesting of responsive grids, combined with a ListView. The top half of the page is defined by a 3x3 grid, displaying information about the user. The bottom half is a ListView that renders the user's submitted pictures.
<Grid >
<!-- First Grid -->
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Row="0" Grid.Column="0">
<!-- Second Grid -->
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Background color -->
<Label Text="" Grid.Row="0" Grid.ColumnSpan="3" BackgroundColor="Gray" />
<!--<Label Text="" Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" HeightRequest="1" BackgroundColor="Black"/>-->
<!-- Profile Picture TODO add box shadow-->
<Image Source="{Binding UserProfile.ProfilePicture.Thumb.Url}" Grid.Column="1" Grid.Row="0" Grid.RowSpan="2"/>
<Label Text="{Binding UserProfile.Name}" Grid.Column="1" Grid.Row="1" VerticalTextAlignment="End" HorizontalTextAlignment="Center" FontSize="16" FontAttributes="Bold" />
<!-- Various Labels around picture-->
<StackLayout Grid.Row="1" Grid.Column="0">
<Label Text="5" VerticalOptions="Center" HorizontalOptions="Center" FontAttributes="Bold" FontSize="34" FontFamily="Avenir" />
<Label Text="Trophies" VerticalOptions="Center" HorizontalOptions="Center" FontAttributes="Bold"/>
</StackLayout>
<StackLayout Grid.Row="2" Grid.Column="0">
<Label Text="342" VerticalOptions="Center" HorizontalOptions="Center" FontAttributes="Bold" FontSize="34" />
<Label Text="Following" VerticalOptions="Center" HorizontalOptions="Center" FontAttributes="Bold"/>
</StackLayout>
<StackLayout Grid.Row="2" Grid.Column="1">
<Label Text="12" VerticalOptions="Center" HorizontalOptions="Center" FontAttributes="Bold" FontSize="34"/>
<Label Text="Submissions" VerticalOptions="Center" HorizontalOptions="Center" FontAttributes="Bold"/>
</StackLayout>
<StackLayout Grid.Row="1" Grid.Column="2">
<Label Text="703" VerticalOptions="Center" HorizontalOptions="Center" FontAttributes="Bold" FontSize="34"/>
<Label Text="Points" VerticalOptions="Center" HorizontalOptions="Center" FontAttributes="Bold"/>
</StackLayout>
<StackLayout Grid.Row="2" Grid.Column="2">
<Label Text="205" VerticalOptions="Center" HorizontalOptions="Center" FontAttributes="Bold" FontSize="34"/>
<Label Text="Followers" VerticalOptions="Center" HorizontalOptions="Center" FontAttributes="Bold"/>
</StackLayout>
</Grid>
<ListView ItemsSource="{Binding Submissions}" RowHeight="100" Grid.Row="1" Grid.Column="0">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Grid Padding="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Image Source="{Binding Item1.VerifiedContent.Thumb.Url}" Grid.Row="0" Grid.Column="0"/>
<Image Source="{Binding Item2.VerifiedContent.Thumb.Url}" Grid.Row="0" Grid.Column="1"/>
<Image Source="{Binding Item3.VerifiedContent.Thumb.Url}" Grid.Row="0" Grid.Column="2"/>
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
The end result is a comprehensive user profile.
Authenticating against the pre-existing API
We built the UI for the logon screen in XAML and used a LoginViewModel to actually make the call to the external service. After the user enters the proper logon credentials, it's wrapped into an HTTP Web Request and sent to the server. We're also accepting the server's response, and upon successful authentication, the user is redirected to the home page and the token is kept. A snippet for the web request follows:
private bool isLoginButtonEnabled;
public bool IsLoginButtonEnabled
{
get { return isLoginButtonEnabled; }
set
{
isLoginButtonEnabled = value;
OnPropertyChanged();
}
}
private Command _loginCommand;
public Command LoginCommand
{
get
{
return _loginCommand ?? (_loginCommand = new Command(async () => { await callSignInApi(SHELFIE_BASE_AUTH_API_URL);}, () => CanLogin));
}
}
private async Task<string> callSignInApi(string queryString)
{
JObject jObject = new JObject(
new JProperty("email", _username),
new JProperty("password", _password));
var stringContent = new StringContent(jObject.ToString(), Encoding.UTF8, "application/json");
var request = new HttpRequestMessage
{
RequestUri = new Uri(queryString),
Method = HttpMethod.Post,
Content = stringContent,
};
HttpClient httpclient = new HttpClient();
HttpResponseMessage httpResponseMessage = await httpclient.SendAsync(request);
var json = JsonConvert.SerializeObject(httpResponseMessage);
try
{
var key = httpResponseMessage.Headers.Where(s => s.Key == "Access-Token").FirstOrDefault().Value;
}
catch (Exception e)
{
Debug.WriteLine("Not returning the proper field!");
}
MoveOn();
return httpResponseMessage.ToString();
}
private bool _canLogin;
public bool CanLogin
{
get { return _canLogin; }
set
{
_canLogin = value;
OnPropertyChanged();
}
}
private async void MoveOn()
{
await PushAsync(new HomeTabbedPage());
App.Current.MainPage = new NavigationPage(new HomeTabbedPage());
}
Migrating the service from AWS to Azure
The original intent to migrate the AWS backend over to Azure was to use Web Apps. There were some clear advantages: there are plenty of educational resources on integrating Xamarin with Azure App Service and any dependencies the app would have needed to run. It also makes the provisioning process much simpler because we would not have to think about the underlying operating system and any scaling concerns. Unfortunately, we learned that while Web Apps supports plenty of common technologies such as Python, ASP.NET, and Node.js, Ruby on Rails is not inherently compatible with Web Apps, so we were forced to implement the system using a virtual machine and IaaS services instead.
The Shelfie backend server stack included Ruby on Rails, PostgreSQL, and Amazon Relational Database Service (RDS), all of which were built on top of AWS leveraging their storage and virtual machine solutions. The main components of the Shelfie backend that we focused on with the migration included initializing their Ruby database logic hosted on an Azure Linux virtual machine, and opening endpoints on this virtual machine to enable the consumption of APIs by the client.
Following are the steps we took for the migration.
Step 1. Migrate core backend assets to Azure IaaS from AWS
The first step was moving all of the Ruby server assets from the AWS virtual machine to an Azure virtual machine. This involved an overview of the code and solution hosted and managed through Bitbucket and Heroku initially.
With this context, we were then able to create a Linux virtual machine on Azure through the Azure portal.
![Azure Linux VM]({{ site.baseurl }}/images/s1.azure-linux-vm.png)
A package of all assets was to be moved onto this new Linux virtual machine. This migration was done over secure copy.
scp .\shelfiebackend.zip greg@23.101.135.131:~/
In the future, rsync copy is highly recommended for maintaining a clean state between assets moved between the Azure and AWS environments. Regardless of the command used during our proof of concept, listing contents now shows that the assets have been fully hosted on Azure.
![Azure Backend Asset]({{ site.baseurl }}/images/s1.backend-assets-in-azure.png)
Ideally you should be able to run the following Ruby command to start the new server in Azure, but some remaining configuration needs to be completed in Step 2.
$ ruby bin/rails server
Step 2. Set up environment and testing in Azure IaaS
The first step in preparing our new virtual machine to run this Ruby server involved quite a bit of package installation on the system itself. A few key ones are listed here.
$ sudo apt-get install nodejs
$ sudo apt-get install npm
$ sudo apt-get install rvm
The last of these is the Ruby version manager (RVM). RVM was integral in building the Ruby code and running this server.
$ rvm install ruby-x.x.x {MUST INCLUDE THE CURRENT VERSION IN PARITY WITH SERVER PORTED FROM AWS in place of x.x.x}
$ sudo gem update --system
$ sudo gem install rails
After the initial environment configuration setup through this package installation, we did our first server test run. After we ran the server, we got the following error. One reason for this error was a missing application.yml file that contained the keys for using third-party authentication services. This file was .gitignored and had to be handed over to us by the partner. Another reason for this error was that we needed to update the database.yml with the Azure configuration because we no longer would be using Amazon RDS.
![KeyBlocker]({{ site.baseurl }}/images/s2.key-blocker.png)
Essentially the goal here was to port from the RDS dependency on AWS to PostgreSQL installed on the system running in Azure, so we updated the database file accordingly.
AWS
# SQLite version 3.x
# gem install sqlite3
#
# Ensure the SQLite 3 gem is defined in your Gemfile
# gem 'sqlite3'
#
default: &default
adapter: postgresql
encoding: unicode
host: <%= ENV['RDS_HOSTNAME'] %>
username: <%= ENV['RDS_USERNAME'] %>
password: <%= ENV['RDS_PASSWORD'] %>
port: <%= ENV['RDS_PORT'] %>
pool: 5
timeout: 5000
development:
<<: *default
database: <%= ENV['RDS_DEVELOPMENT_DB_NAME'] %>
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
database: <%= ENV['RDS_TESTING_DB_NAME'] %>
production:
<<: *default
database: <%= ENV['RDS_DB_NAME'] %>
Azure
# SQLite version 3.x
# gem install sqlite3
#
# Ensure the SQLite 3 gem is defined in your Gemfile
# gem 'sqlite3'
#
default: &default
adapter: postgresql
encoding: unicode
host: localhost
username: postgres
password: password
port: 5432
pool: 5
timeout: 5000
development:
<<: *default
database: shelfie_dev
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
database: <%= ENV['RDS_TESTING_DB_NAME'] %>
production:
<<: *default
database: <%= ENV['RDS_DB_NAME'] %>
Now the following command should work, but port bindings are another issue we faced.
ruby bin/rails server
There's some routing configuration that needs to happen to allow traffic through ports outside of 3000. NGINX is a helpful tool to make note of in configurations like these.
Example NGINX config
server {
listen 80;
root /var/www/;
index index.php index.html index.htm;
server_name example.com;
location / {
try_files $uri $uri/ /index.php;
}
location ~ \.php$ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:8080;
}
location ~ /\.ht {
deny all;
}
}
We decided to put this NGINX implementation on backlog for the sake of the POC. Instead, we forced a binding in the following command. This -b forces the server to bind to all interfaces.
ruby bin/rails server -b 0.0.0.0
So now we can access any of the following endpoints, and even make API calls from this domain hosted all in Azure, after we do one last rake in Step 3!
http://23.101.135.131:3000/users/sign_in
http://23.101.135.131:3000/api
http://23.101.135.131:3000/admin
Step 3. Configure database management
Minor components still a part of other AWS environments will need to be pulled down to access the endpoints listed previously. Rails is pretty handy because it has a database migration command built in that handles this for us!
$ rake db:migrate
Shelfie also has a user and admin management portal. To actually use this portal, we had to register a new administrator.
$ u = User.new(email: "admin@shelfie.com", password: "password", birth_date: "1/1/1990", screen_name: "admin", first_name: "Bob", last_name: "Smith",
provider: "email", uid: "admin@shelfie.com" admin:true)
$ u.save!
From there we can sign in using this address.
http://23.101.135.131:3000/admin
Using a browser to navigate to this URL gives us this nice management portal running Azure to give Shelfie the same level of control they had in their AWS instance.
![Admin Portal]({{ site.baseurl }}/images/3.admin-portal.png)
Learnings and key takeaways
The team decided to forgo the use of a third-party MVVM framework to work on Shelfie to avoid being locked into one, but we encountered some issues performing relatively straightforward tasks such as page navigation. After some additional research, we found that Prism is a very useful and popular framework for Xamarin and Xamarin.Forms development.
As mentioned earlier, the need to use a virtual machine and Azure IaaS components arose after learning that Web Apps would not support the Ruby on Rails backend. It would have been great to use Web Apps because it would have increased developer productivity greatly, but we saw no clean way around it at this time. Documentation on support web technologies using Web Apps can be found here.