Introduction
Do you like watching that old TV show so much that you want to take it with yourself wherever you go? And what about silently reading the episodes, instead of listening?
In this article, I present Xamarin TV Scripts, a simple entertainment app for Xamarin Forms. If you are interested, please download the source code and enjoy the show!
Background
This is an app I've been long thinking of doing. Not from scratch, but by porting the concepts I once applied on another platform: Windows Phone.
Long time ago, in 2011, I posted an article here on Code Project, called The Big Bang Transcripts Viewer, in which I explored the wonders of my newly acquired Windows Phone 7 device.
Figure: The Big Bang Transcript Viewer - the good ol' days of Windows Phone
I thought Windows Phone had so much growth potential, and for some time it seemed so. For users, the interface was fast, sleek and unique. The experience was nice, although it always played catch-up with Android and iOS in terms of features. For developers, it offered the convenience of Visual Studio IDE, the .NET Framework and a great emulator. But unfortunately, many manufacturers remained reluctant in embracing and adopting the platform for their "flagship" device models, which were reserved for the Android platform. Also, Windows Phone lacked some of the most critically demanded apps and games, and even after years after its release, WinPhone never got much traction in the US mobile market.
Enter Xamarin Forms
For those migrating from Windows Phone development to cross-platform development, Xamarin (and more specifically Xamarin Forms) is a natural next step.
Xamarin Forms development shares some similarities with Windows Forms, and I've been able to explore those to my advantage; both can be developed with Visual Studio using C# language and the user interface can be designed with XAML files. Both platforms were created with MVVM pattern in mind, which means they both share the concepts of view models and binding. Also, the preferred event notification technique is based on pub/sub messaging.
But of course, the differences between Windows Phone and Xamarin Forms development had to be addressed accordingly. Since Xamarin Forms is a cross-platform framework, one can't simply get access to the device's file system to read or write files, or save/retrieve data to/from the local database. Instead, each platform (Android, IoS, etc.) has its own file system API, database API, and so on. That's why I had to figure out how to invoke the device-level APIs of Xamarin Android from the Xamarin Forms (Net Standard) project.
The Solution
Figure: The .NET Standard and the Xamarin Android projects
The solution is built on Visual Studio 2017 and, like most Xamarin solutions, it contains 2 projects: one for the target device's platform (Android) and the other one is the .NET Standard 2.0 project.
When you create a new Xamarin solution, you are presented with a set of options: Platform, UI Technology and Code Sharing Strategy.
Because developing for iOS requires a Mac OS machine (which I don't have) and Windows (Universal Windows Platform) would only make sense in a Windows Phone, I only wanted an Android app, so I unchecked the iOS and Windows checkboxes.
As for the UI Technology, Xamarin Forms comes with a rich set of components, and I think I would be in trouble if I tried to recreate them using the Android Native UI Technology Android.
Component Type | Components |
Xamarin Forms View | ActivityIndicator , BoxView , Button , DatePicker , Editor , Entry , Image , Label , ListView , OpenGLView , Picker , ProgressBar , SearchBar , Slider , Stepper , Switch , TableView , TimePicker , WebView |
Xamarin Forms Pages
| ContentPage, TabbedPage, MasterDetailPage, NavigationPage, Carousel, TemplatedPage |
Xamarin Forms Layout | AbsoluteLayout , ContentPresenter , ContentView , Frame , Grid , RelativeLayout , ScrollViewer , StackLayout , TemplatedLayout |
Xamarin Forms Cells | EditorCell, EntryCell, ImageCell, SwitchCell |
The .NET Standard project type replaced the old PCL (Portable Class Library) and became the new de facto standard cross-platform project type for .NET projects. That is, you can share a .NET Standard assembly with .NET Framework, .NET Core and Xamarin apps. The same "Xamarin,TVScripts.csproj" .NET Standard project contains not only the Xamarin Forms dependencies, but also all the templates needed for adding new components and assets for your Xamarin Forms project.
We can see in the image above the presence of the Xamarin.Forms.Xaml.dll assembly that comes with the Xamarin.Forms
assembly. That assembly gives us the ability to design our user interfaces using XAML (eXtensible Markup Language), which is a declarative type of XML document language developed by Microsoft for user interfaces an other applications.
But when will our Xamarin Forms application start?
Since the Xamarin.TVScripts.Android
must be the startup project for the solution, at some point, it will necessarily pass the control of the application to the Xamarin.TVScripts
Net Standard code. It does so in the MainActivity
class, which is in fact the application's entrypoint, and more specifically in the OnCreate
method.
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
...
protected override void OnCreate(Bundle bundle)
{
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
UserDialogs.Init(this);
base.OnCreate(bundle);
global::Xamarin.Forms.Forms.Init(this, bundle);
LoadApplication(new App());
}
}
Listing: The MainActivity class
The LoadApplication(new App());
code will of course load and run the application, which resides in the other (.NET Standard) application.
Now we can talk about the App
class.
The App
class is in the App.Xaml.cs file, which is the code-behind file for the App.Xaml. Despite having the xaml extension, this file does not represent a view, but rather the Application
class in XAML language.
Back to the App.Xaml.cs file: it implements the Xamarin.Forms.pplication
superclass. When the application starts, it must define the MainPage
property of the App
class. Then Xamarin Forms will initiate the navigation flow through our custom XAML pages. The code below instantiates a new NavigationPage
, starting at the HomePage
:
public partial class App : Application
{
public App ()
{
InitializeComponent();
MainPage = new NavigationPage(new HomePage());
}
}
Listing: The App class
The NavigationPage
is a Xamarin component that will handle the navigation and user-experience of a stack made of our custom pages. This "stack" holds the navigation history and, just like in a web browser, allows us to navigate back and forth across our views.
User Interface
We have a user interface composed of 3 views: the HomePage
, the SeasonPage
and the EpisodePage
.
The HomePage
just shows the TV show cover image, along with a ListView
containing the show seasons. The ListView
is the Xamarin component and works as a view for presenting lists of data, especially long lists that require scrolling.
Figure: Cover image and ListView from HomePage.xaml
The ListView
can be used as a simple list of unformatted text or, like in our case, you can customize it to display a set of styled items. You can replace the default plain text presentation of the ListView
by providing the ListView.ItemTemplate
property with the desired layout, represented by any arrange of various Xamarin components:
Figure: building the ListView's ItemTemplate
Since the season image is square, we can apply a little trick by using a image mask in order to transform it into a circular icon:
Figure: Applying a circular mask on top of an image
Similarly, the SeasonPage
shows a simple list of episodes, and it does not have much styling. Each episode name is defined by a TextBlock
contained in a round-cornered Frame
component.
Figure: The SeasonPage.xaml view featuring the list of episodes of Season 2
The EpisodePage
, on the other hand, is more carefully designed. It is by far the most important view, where users will spend most of the time in the app. This view holds similarities with the other views, because of the styled ListView
s displaying a collection of data (the TV Show's quotes), but it does so in a very customized way. Each quote can be shown in one of these three styles:
- commentary (without characters)
- character speaking at the left
- character speaking at the right
An episode commentary is displayed when no character name is found in the associated Quote
instance. Otherwise, the character's picture will be displayed at one side, and the speech bubble will be shown at the other. The character's photo will be displayed at the left or right, depending on whether the quote number is even or odd. This way we can easily alternated between those 2 modes.
The character's pictures are retrieved by binding the character's name to the corresponding image residing in the "drawable" folder of the Droid project.
The speech bubble is composed by two parts: a TextBlock
contained within a rounded-cornered Frame
, and an Image of a triangle immediately connected to it. This gives us the impression of a speech bubble.
Figure: The EpisodePage.xaml view displaying the episode's conversation
In the image below, we can identify the 3 different types of "cards". The first one is called Narration Card, and usually represents a scene description, not spoken by a character. The other two are called OddCards and Even Cards, which are scripts spoken by characters and are displayed in an alternated way.
Figure: The 3 types of speeches in EpisodePage.xaml view
Each Odd/Even Card has its layout defined by a 2-column Grid
to separate image from the speech bubble. Since the image column only takes 80 pixels, the speech bubble takes the rest of the space within the Grid. And it looks nice even when the device screen is rotated in landscape mode.
Figure: Anatomy of a speech card
The speech bubble is tricky. At first, I tried to implement some sort of vectorized drawing, but unfortunately that was not possible with Xamarin. Then I ended up combining a round-shaped frame along with an image containing a triangle, and it looked nice.
Figure: XAML code inside a speech bubble
Bindings
Xamarin Forms is all based on the MVVM (Model-View-ViewModel) pattern, so you should expect the extensive use of bindings. Since MVVM decouples the View layer from the data it shows, the bindings act like a glue that tie view and its data together dynamically, so that each change in data should be reflected on the view side.
In the fragment below, we can notice 3 bindings:
- The label's
Text
property is bound to the Name
property of the Season
object - The label's
Style
property is bound to the ListItemTextStyle
value - The label's
TextColor
property is bound to the ListFontColor
value
<Label Text="{Binding Name}"
LineBreakMode="NoWrap"
TextColor="{DynamicResource ListFontColor}"
Style="{DynamicResource ListItemTextStyle}"
FontSize="16"
VerticalOptions="Center"
VerticalTextAlignment="Center"
HorizontalOptions="StartAndExpand"/>
Listing: the Label component inside a ListView inside the HomePage.xaml file
As mentioned, the data from the Name
property comes from the Season
class:
[Table("Seasons")]
public class Season : BaseModel
{
public Season() { }
public Season(int seasonNumber, string name)
{
SeasonNumber = seasonNumber;
Name = name;
}
public int SeasonNumber { get; set; }
public string Name { get; set; }
[Ignore]
public string ImageSource => $@"_season{SeasonNumber}.jpg";
}
Listing: The Season class from our model
The TextColor
, on the other hand, does not come from the Season
class, but is rather provided by a value included in the ResourceDictionary
in the App.xaml file:
<?xml version="1.0" encoding="utf-8" ?>
<Application ...>
<Application.Resources>
<ResourceDictionary>
...
<Color x:Key="ListBackGroundColor">#FFFFFF</Color>
...
</ResourceDictionary>
</Application.Resources>
</Application>
Listing: the App.xaml file
This happens because we explicitly defined this binding as a DynamicResource
(TextColor="{DynamicResource ListFontColor}").
Files
The app data consists of 3 types of files: the seasons file, the episodes file and the quotes files (each episode has its own separated quotes file).
1 Season 1
2 Season 2
3 Season 3
4 Season 4
. .
. .
. .
Listing: The seasons.txt file
. . .
. . .
. . .
2 23 The One With The Chicken Pox
2 24 The One With Barry and Mindy's Wedding
3 1 The One With The Princess Leia Fantasy
3 2 The One Where No One's Ready
3 3 The One With All The Jam
. . .
. . .
. . .
Listing: The episodes.txt file
The seasons.txt and the episodes.txt are plain text files that merely display the names of the seasons and the episodes.
2 [Scene Chandler's Office, Chandler is on a coffee break. Shelley enters.) Shelley
3 Chandler Dehydrated Japanese noodles under fluorescent lights... does it get better than this?
4 Shelley Question. You're not dating anybody, are you,
because I met somebody who would be perfect for you.
5 Chandler Ah, y'see, perfect might be a problem. Had you said 'co-dependent',
or 'self-destructive'...
6 Shelley Do you want a date Saturday?
7 Chandler Yes please.
Listing: The 0108.txt file (Season 01, Episode 08)
The quotes files, on the other hand, contains 3 columns: the quote sequence number, the character who is speaking, and the speech (quote) itself.
Since plain text files can hold a large amount of unstructured data, they are often hard to query. And if we could transfer their data to a local database residing in our device, it would be easier to query and fetch the data, as we want.
Local SQLite Database
Among local relational databases, SQLite stands out as a stable and reliable candidate. We can generate a local SQLite database using the code-first approach, that is, by first implementing the entities (season, episode, quote) as C# classes, and then using them as the source for the SQLite database schema.
The plain text files are accessed in the Xamarin Droid project., because the implementation depends upon the platform's file system API. So we place the file access methods directly in the Main Activity class, which contains the assets
object from which the files can be accessed. Just like file access, the data base access is also implemented in the Droid project.
Following Xamarin Documentation Guide, it was quite easy to implement a class in the Xamarin.TVScripts.Android
project to retrieve the SQLite Connection:
[assembly: Xamarin.Forms.Dependency(typeof(SQLite_android))]
namespace Xamarin.TVScripts.Droid
{
class SQLite_android : ISQLite
{
private const string dbFileName = "TVScripts.db3";
public SQLiteConnection GetConnection()
{
string dbPath = Path.Combine(
System.Environment
.GetFolderPath(System.Environment.SpecialFolder.Personal),
dbFileName);
return new SQLite.SQLiteConnection(dbPath);
}
}
}
Listing: The SQLite_android class
Notice that the SQLite_android
class implements the ISQLite
interface (class SQLite_android : ISQLite), and that we use the annotation [assembly: Xamarin.Forms.Dependency(typeof(SQLite_android))] just above the namespace. What does that mean? Both the concrete class name and the interface are intended to be used for the built-in Dependency Injection Service of Xamarin Forms when the database functionalities are invoked from the other project. This is particularly useful because the Xamarin.TVScripts
project does not reference Xamarin.TVScripts.Android
project where the SQLite_android
class resides (and it would never would, since this would result in a cross-reference error).
So, from where does exactly the SQLite_android.GetConnection()
is called?
The Xamarin.TVScripts
project has a set of Data Access Object classes that implement basic CRUD (Create
, Read
, Update
, Delete
) methods for the SQLite database. These classes correspond to the model classes of our model (Season
, Episode
and Quote
) and they inherit from a common superclass called BaseDAO<T>
. Whenever the data access classes need to access the database, they first obtain the connection through the GetConnection
method in the base class:
public class BaseDAO<T> where T : BaseModel, new()
{
public BaseDAO()
{
using (SQLiteConnection connection = GetConnection())
{
connection.CreateTable<T>();
}
}
public IList<T> GetList()
{
using (SQLiteConnection connection = GetConnection())
{
return new List<T>(connection.Table<T>());
}
}
.
.
.
protected SQLite.SQLiteConnection GetConnection()
{
return DependencyService.Get<ISQLite>().GetConnection();
}
}
Listing: The BaseDAO<T> class
Notice how the BaseDAO<T>
is a generic class that imposes a constraint for the T
type. This improves code reusability, because we can put in the base class many methods that would otherwise become different methods in the derived classes, varying only in terms of the model entity being manipulated.
Now, let's take a look at the GetConnection()
method:
protected SQLite.SQLiteConnection GetConnection()
{
return DependencyService.Get<ISQLite>().GetConnection();
}
Listing: The BaseDAO<T>.GetConnection method
Here, we can see how we obtain the ISQLite
implementation using the DependencyService
class. The Get<T>()
method will make the Xamarin
framework to look for a class that implements the interface and return a new instance from it.
Text-to-Speech
The app also offers a features for reading the speech bubbles text, using the features provided by the Text-To-Speech app available at Android Playstore.
[assembly: Xamarin.Forms.Dependency(typeof(Xamarin.TVScripts.Droid.TextToSpeech))]
namespace Xamarin.TVScripts.Droid
{
public class TextToSpeech : Java.Lang.Object, ITextToSpeech,
global::Android.Speech.Tts.TextToSpeech.IOnInitListener
{
global::Android.Speech.Tts.TextToSpeech speaker;
string toSpeak;
public void Speak(string text)
{
toSpeak = text;
if (speaker == null)
{
speaker = new global::Android.Speech.Tts.TextToSpeech(Application.Context, this);
}
else
{
speaker.Speak(toSpeak, QueueMode.Add, null, null);
}
}
public void OnInit(OperationResult status)
{
if (status.Equals(OperationResult.Success))
{
speaker.Speak(toSpeak, QueueMode.Add, null, null);
}
}
}
}
Listing: The TextSpeech class
Just like the SQLite_android
class, the TextToSpeech
is registered in the Xamarin Dependency Injection Container through the use of the Xamarin.Forms.DependencyAttribute
annotation.
When a speech bubble is tapped, the ListView
will raise an OnItemSelected
event, which in turn will invoke the text-to-speech functionality to speak both the character's name and his/her speech.
async void OnItemSelected(object sender, SelectedItemChangedEventArgs args)
{
var quote = args.SelectedItem as Quote;
if (quote == null)
return;
await Speak(quote.Character);
await Speak(quote.Speech);
}
private async Task Speak(string text)
{
await Task.Run(() =>
{
DependencyService.Get<ITextToSpeech>().Speak(text);
});
}
Listing: The client code for the text-to-speech functionality in the EpisodePage.xaml.cs file
This showcases how you could leverage your app's capabilities (and also extend them) in terms of helping people with reading disabilities (both blindness and dyslexia). And you can obtain these results without having to develop a complex and secondary feature in your application. Now you can save that time to develop the critical code for your app.
Conclusion
If you have reached this line, thank you very much for your attention and patience. I really think Xamarin Forms makes Android development a lot of fun for Visual Studio/C# developers, not only for serious applications, but also for taking crazy ideas like mine and make them happen. I hope some of you get inspired to try something new.
So, do you like the article and the code? I'm looking forward to seeing your thoughts. Please give me feedback in the comments section below!
History
- 2018-03-31: First version