This section describes the implementation of our data model.
Let's start with the twitter.js
file:
As always, we define our namespace; in this case, TWITTER
:
We define two variables global to the TWITTER
namespace: _baseURL
and _searchBase
. These two URLs point at Twitter's JSON API; the first is for API requests such as user lookups, user streams, and such, while the latter is only for searching. We define them here for two reasons: to make the URLs a little less nasty in the following code, and if Twitter should ever decide to have a different version of the API (and you want to change it), you can do so here.
Next, we define our first object, TwitterUser
using the following code snippet:
We've defined our two properties here, _screenName
and _userData
. We're using underscores at the front to indicate that these are internal (private) variables that no outside object should access. Instead, an outside object should use the get
/set
methods we define next:
This one's simple enough, it just returns the private member when asked. But the next one's more complicated:
Like a normal set method, we've assigned theScreenName
to _screenName
. But when this happens, we want to load in the user information from Twitter. This is why it is important to have get
/set
methods in front of private methods; you might just need them to do something important when the value changes or is read.
Here we've defined our URL that we're going to use to ask Twitter to look up the user in question. For more information about how this particular URL works, see the Twitter documentation at https://dev.twitter.com/docs/api/1/get/users/lookup. You can see a full example of what is returned at the bottom of the page.
We use the encodeURIComponent()
method to ensure that the text is properly encoded (so that it can handle international characters).
Now that we have our URL, we're going to use another utility function defined for us in PKUTIL
(www/framework/utility.js
), called loadJSON()
. It uses AJAX to send a request to the earlier URL, and Twitter then sends a response back, in the form of JSON. When it is finished, the function will call the completion
function we're passing as the second parameter after getUserURL
. This method can check if the request succeeded or not, and set any private members that are necessary. We'll also call the completion
function passed to the setScreenName()
method.
If success is true, then the JSON has been properly returned and parsed into the data parameter. We just assign it to the private _userData
member.
But, if the return value of success
is false
, then something's gone wrong. Anything could have happened. Twitter might be down (not unheard of), the network connection might have failed, or Twitter might have rate limited us. (For Twitter's error codes, see https://dev.twitter.com/docs/error-codes-responses.) For now, we're just going to assume the latter, but you could certainly build more complicated error-detection schemes to figure out the type of error.
Finally, regardless of success or failure, we call the completion
function passed to us. This completion
function is important so that we know when we can safely access the _userData
member (via getUserData
a little lower).
The method getProfileImageURL()
is a convenience function that returns the user's profile image URL. This is a link to the avatar being used for Twitter. First we check to see if _userData[0]
exists, and if it does, return profile_image_url
, a value defined by the Twitter API. If it doesn't, we'll just return an empty string.
Next, the getUserData()
method is used to return the _userData
member. If it has been properly loaded, it will have a lot of values in it, all determined by Twitter. If it has failed to load, it'll have an error property in it, and if it hasn't been loaded at all, it'll be empty.
The getTimeline()
method is also a convenience function used to get the timeline for the Twitter user. theMaxCount
is the maximum number of tweets to return (up to 200), and completion
is a function to call when it's all done. We do this by creating a new TwitterStream
object (defined later) with the Twitter screen name prepended by an @
character.
If theMaxCount
isn't specified, we use a little ||
trick to indicate the default value of 25
tweets.
The last thing we do is actually call the setScreenName()
method with the screen name and completion
function passed in to the constructor. If you remember your JavaScript, this whole function, while we can think of it as defining an object, is also the constructor of that object. In this case, as soon as you create the TwitterUser
object, we'll fire off a request to Twitter to load in the user's data and set it to _userData
.
Our next object is the TwitterStream
object:
Here we've defined three properties, _searchPhrase
, _stream
, and _theMaxCount
. The _searchPhrase
property can either be the screen name of a user or a literal search term, such as a hashtag. The _stream
property is the actual collection of tweets obtained from Twitter, and the _theMaxCount
property is the maximum number of tweets to ask for. (Keep in mind that Twitter is free to return less than this amount.)
You may ask why we're storing either a search phrase or a screen name. The reason is that we're attempting to promote some code re-use. It's logical to assume that a Twitter stream is a Twitter stream, regardless of how it was found, either by asking for a particular user's stream or by searching for a word. Fair assumption, right?
Yeah, but totally wrong, too. The streams are close enough so that we can work around the differences, but still, not the same. So, even though we're treating them here as one-and-the-same, they really aren't – at least until Twitter decides to change their Search API to better match their non-Search API.
Here we have the get
/set
methods for the _theMaxCount
property. All we do is set and retrieve the value. One thing to note is that this should be called before we actually load a stream. This value is part of the ultimate URL we sent to Twitter.
Notice that we have two set
methods that act on the _searchPhrase
property while we only have one get
method. What we're doing here is permitting someone to call the setScreenName()
method without the @
character. The _searchPhrase
property will then be set with the @
character prepended to the screen name. The next set
method (setSearchPhrase()
) is intended to be used when setting real search terms (such as a hashtag).
Internally, we'll use the @
character at the front to mean something special, but you'll see that in a second.
The getStream()
method just returns the _stream
property, which if we haven't loaded, will be blank. So let's look at the loadStream()
method:
The loadStream()
method takes a completion
function. We'll call this at the end of the operation no matter what; it lets the rest of our code know when it is safe to access the _stream
member via the getStream()
method.
The other component is the forScreenName
variable; if true
, we'll be asking Twitter for the stream that belongs to the screen name stored in the _searchPhrase
property. Otherwise, we'll ask Twitter to do an actual search for the _searchPhrase
property:
All we've done so far is defined the theStreamURL
property to point either at the Search API (for a search term) or the non-Search API (for a screen name's stream). Next we'll load it with the loadJSON()
method using the following code snippet:
Here's another reason why we need to know if we're processing for a screen name or for a search: the JSON we get back is slightly different. When searching, Twitter helpfully includes other information (such as the time it took to execute the search). In our case, we're not interested in anything but the results, hence the two separate code paths.
Again, if we have a failure, we're assuming that we are rate-limited.
When done, we call the completion
method, helpfully passing along the data stream.
Just like at the end of the previous object, we call some methods at the end of this object too. First we set the incoming search phrase, then we set the maximum number of tweets to return (or 25, if it isn't given to us), and then we call the loadStream()
method with the completion
function. This means that the moment we create a new TwitterStream
object, it's already working on loading all the tweets we'll be wanting to have access to.
We've taken care of almost all our data model requirements, but we've got just a little bit left to do in the twitterUsers.js
file; use the following instruction:
First, we create a users()
array in the Twitter namespace. We're going to use this to store our predefined Twitter users, which will be loaded with the following loadTwitterUsers()
method:
What we've done here is essentially just chained together five requests for five different Twitter accounts. You can store these in an array and ask for them all at once. But for this our app needs to know when they've all been loaded. You could also do this by using recursion through an array of users, but we'll leave it as an example to you, the reader.
We have implemented our data model and predefined the five Twitter accounts we want to use. We also went over the loadJSON()
method in PKUTIL
, which helps with the entire process. We've also been introduced to the Twitter API.
Before we go on, let's take a look at the loadJSON()
method you've been introduced to. It's been added to this project's www/framework/utility.js
file, as shown in the following code block:
First off, this is a pretty simple function to begin with. What we're really doing is utilizing the PKUTIL.load()
method (explained later) to do the hard work of calling out to the URL and passing us the response, but when the response is received, it's going to be coming back to us in the data variable.
The theParsedData
variable will store the actual JSON data, fully parsed.
If the URL returns something successfully, we try to parse the data. Assuming it is a valid JSON string, it'll be put into theParsedData
. If it isn't, the JSON.parse()
method will throw an exception as follows:
Any exceptions will be logged to the console, and we'll end up telling our completion function that the request failed:
At the end, we call the completion
function and tell it if the request failed or succeeded, and what the JSON data was (if successfully parsed).
The PKUTIL.load()
method is another interesting beast (for full implementation details, visit https://github.com/photokandyStudios/YASMF/blob/master/framework/utility.js#L126). It's defined as follows:
First, we'll check to see if the browser understands XMLHttpRequest
. If it doesn't, we'll have to call the completion
function with a failure notice and a message describing how we couldn't load anything. This is shown in the following code block:
Next we set up the XMLHttpRequest()
method, and assign the onreadystatechange
function as shown in the following code snippet:
This function can be called many times during the loading process, so we check for a specific value. In this case, 4
in the following code snippet means that the content has been loaded:
Of course, just because we got data doesn't mean that it is useable data; we need to verify the status of the load, and here we get into a little bit of murky territory. iOS defines success with a zero value, while Android defines it with 200
, as shown in the following code snippet:
If we've successfully loaded the data, we'll call the completion
function with a success notification and the data:
But, if we've failed to load the data, we call the completion
function with a failure notification and the status value of the load:
Keep in mind that we're still just setting up the XMLHttpRequest
object; we've not actually triggered the load yet.
The next step is to specify the path to the file, and here we run into a problem on WP7 versus Android and iOS. On both Android and iOS we can load files relative to the index.html
file, but on WP7, we have to load them relative to the /app/www
directory. Subtle to track down, but critically important. Even though we aren't supporting WP7 in this book, the framework does, and so it needs to handle cases such as the following:
Now that we've set the filename, we fire off the load:
Tip
Should you ever decide to support WP7, it is critical that even though the framework supports passing false
for aSync
, which should result in a synchronous load, you shouldn't actually ever do so. WP7's browser does very funny things when it can't load data asynchronously. For one thing, it loads it asynchronously anyway (not your intended behavior), and it also has a tendency to think the file simply doesn't exist. So, instead of loading scripts, you'll get errors in the console indicating that a 404 error has occurred. And you'll scratch your head (I did!) wondering how in the world that could be when the file is right there. Then you'll remember this tip, change the value back to true
, and things will suddenly start working. (You seriously do not want to know the hours it took me to debug on WP7 to finally figure this out. I want those hours back!)