Flex / Twitter App Take 2
If you think this looks like any fun and don’t know anything about WPF go check out Kurt Brockett’s blog for an interesting challenge.
Thanks to a great tip from Matt Chotin, from Adobe, in the comments of my previous post I take a massive step forward in the UI department. Again, the Open Source gods ( is this open source? ) work their magic to give me an even more useful component: SpringGraph from another Adobe Engineer ( seeing a pattern are we ) named Mark Shepard. He hasn’t blogged in some time, so I’m not sure he is still doing stuff with Adobe/Flex or blogging but, his component totally rules and fits my needs pretty closely. Here is the original SpringGraph Demo and the Roamer Demo which is like the SpringGraph just with nodes hidden or collapsed ( which may be the final component I use ). Well onto, what I’m trying to do:
Shiny New Object
First things first, time to see what makes the SpringGraph tick. Given my limited knowledge of all things Flash, I had to piece together how the main body of his MXML file even worked.
<fc:SpringGraph id="s" backgroundColor="#ffffff" lineColor="#333388" left="0" right="0" top="0" bottom="0" itemRenderer="AmazonItemView" repulsionFactor="{rep.value}" > </fc:SpringGraph>
It took me a few minutes to understand the beauty of the itemRenderer system built into Flex. If you check out the other source objects in Mark’s project you will notice AmazonItem.as and AmazonItemView.as. Think of this as a mini MVC subsystem. With a collection of AmazonItem-s being the model, the SpringGraph control being the controller and the AmazonItemView obviously being the view. This is made clearer when you add the following AS3 snippet to the mix
private var items: Graph; public function addItem(id: String, name: String, linkTo: AmazonItem): AmazonItem { var newItem: AmazonItem = new AmazonItem(id, name); items.add(newItem); if(linkTo != null) items.link(newItem, linkTo); s.dataProvider = items; return newItem; }
What is happening here? Well as you call the addItem function an instance of the model, AmazonItem is created and added to collection. This collection is then bound to the dataProvider of the SpringGraph. When SpringGraph is drawn on the stage it loops at this dataProvider and instantiates a view object named itemRenderer for each item in the dataProvider. So this allows you to completely change what the SpringGrid is rendering on your behalf — pretty cool, huh?
My “Model”
Now that I think I understand how that component works I need a place to store some data about a Twitter user. Normally, you could just create a basic class and use that but, in the case of this component it seems you need to subclass the delivered Item class. Once again, no big deal. Created a new class called TwitterPerson. That class has a few attributes:
public class TwitterPerson extends Item { [Bindable]public var screenName:String; [Bindable]public var name:String; [Bindable]public var profile_image:String;
These will store the data returned from the API, I’ve named the attributes nearly the same as the API to make it easier to follow. The attributes are bindable so that as I fill in the information the “view” will update automatically. The constructor is uber-simple:
public function TwitterPerson(xml:XML){ screenName = xml.screen_name; name = xml.name; profile_image = xml.profile_image_url; }
To make it as easy as possible to construct the TwitterPerson I just pass in an XML node and map the parameters myself. If you check out the API you will see that the two services I use “statuses/friends/” and ”users/show/” return different data but have the same information about each person with the same attribute names ( thanks for the consistency Twitter guys! ).
My “View”
The other piece I need to construct is the view that the SpringGraph will call to render the data stored in the model. For right now all I want to do is put the picture of the person being rendered up there and give a tool tip for their name. Flex makes something like this so easy it’s barely worth mentioning but for completeness:
<mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml"> <mx:Image source="{data.profile_image}" scaleX="1" scaleY="1" toolTip="{data.name}"/> </mx:VBox>
Two Heads are better then one but One’s a start
Back in the main MXML of our application things have changed a bit:
<mx:HTTPService id="myInfoService" url="http://twitter.com/users/show/dan_mcweeney.xml" resultFormat="e4x" result="onMyInfoServiceResult(event)" fault="onMyInfoServiceFailed(event)" /> <fc:SpringGraph id="springLayout" backgroundColor="#ffffff" lineColor="#333388" left="0" right="0" top="0" bottom="0" itemRenderer="TwitterPersonView" repulsionFactor=".80" canDragNodes="false" />
You can see how nice and clean the SpringGraph is to use it was basically a one for one swap of other component. Another change here is method used to fetch the data. As you will see in the next section I leave it up to the Model class itself to determine how their friends are, hopefully this will be a good decision architecturally, so the start of the application is to just call the send method of the myInfoService HTTPService. The result event is then triggered and that’s where everything gets stuck back together again.
At this point if you are following along, which I’m fairly certain no one is, you should get one head floating in the middle of a big white background, yay! Although the astute observer will notice that I snuck a new method into that last snippet of code: “person.getFriends()”.
Model^2 Math.pow(Model,2)
As I mentioned eariler I have now moved the getting of friends into the Model. I did this to hopefully make it easier for some ideas I have in the long run but also to see how to make a HTTPService entirely in AS. All this method does is ask the Twitter API to for the user’s list of friends:
1 public function getFriends():void{ 2 var friendsService:HTTPService = new HTTPService(); 3 var friends:Array = new Array(); 4 friendsService.url = 5 "http://twitter.com/statuses/friends/" + 6 screenName + ".xml"; 7 friendsService.resultFormat = "e4x"; 8 friendsService.addEventListener(FaultEvent.FAULT, 9 friendsFault); 10 friendsService.addEventListener("result", 11 friendsResult); 12 friendsService.send(); 13 }
The only remaining mystery is what happens in the friendsResult method which gets bound to the “result” event on line 10-11.
private function friendsResult(result:ResultEvent):void{ var friendsArray:Array = new Array(); for each (var userEntry:XML in result.result.user){ friendsArray.push(new TwitterPerson(userEntry)); } sevenDegress_2(Application.application). onAddFriends(this, friendsArray); }
When the friends HTTPService returns it has a list of all the user’s friends and their current status. The method just pushes these newly minted TwitterPerson(s) into an array and then calls another mysterious method from the application that contains it. I don’t know if this is the best way to do this or not — seems like a bad case of statically linking these two objects but it seemed like the easiest way at the time. This line of code gets the root Application object’s attribute called application and invokes a method on it. For this to work at compile time though you need to cast that application attribute over to to the specific type of your application hence that funky, sevenDegrees_2 thing you see there. The source code to that one extra method is also pretty straightforward thanks once again to Mark’s Graph class:
public function onAddFriends(node:TwitterPerson, friendsList:Array):void{ for each (var person:TwitterPerson in friendsList){ people.add(person); people.link(person, node); } }
When the friends HTTPService gets a result it passes it back up to the application which in turn modifies the data provider by adding the new nodes ( via the add() method ) and then linking them to each other ( via the link() method ). This should now produce the pretty little Poof ball like below:
Hatchet Attack
This control is actually a little too useful for my needs, I don’t really want anyone to be able to drag folks around once they have been placed on the screen so, I have to delve into Mark’s control and get rid of that functionality. This turned out to be a fairly straightforward:
1 private function mouseDownEvent 2 (event: MouseEvent):void { 3 var now: int = getTimer(); 4 if((now - lastMouseDownTime) < 300) { 5 // it's a double-click 6 var node: GraphNode = _dataProvider.findNode( 7 UIComponent(event.currentTarget)); 8 if(node != null) { 9 dragEnd(event); 10 if(Object(node.view). 11 hasOwnProperty("doubleClick")) 12 Object(node.view).doubleClick(event); 13 } 14 return; 15 } 16 lastMouseDownTime = now; 17 if (_canDragNodes) 18 dragBegin(event); 19 event.stopImmediatePropagation(); 20 }
Beyond the addition of line 17, the only other thing I needed to add was one parameter _canDragNodes ( and its’ getter and setter methods ). Although it is truly tons of fun to drag the seed member of the graph around and watch all their Twitter Friends fly after them — it’s not really useful.
If you want to see it running live, meaning you really think I’m a liar, here is a link to the Current Version of 7DegreesOfTwitter.
Hi Dan,
Saw you message on Twitter about tweetr and the possible message truncation bug. I just aw it to, I am putting a new build out pretty soon in 2 days to fix this and adds a few more features like deleting a post and better webcam support now I know how it works on macs etc. Cheers, John.