Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Mobile App Showcase: Tuenti

Tuenti
November 05, 2013

Mobile App Showcase: Tuenti

Tuenti has one of the most popular Spanish mobile apps with millions of monthly active users. Want to know how it was built?

Tuenti

November 05, 2013
Tweet

More Decks by Tuenti

Other Decks in Technology

Transcript

  1. 1. A little bit of history ! 2. Process &

    release workflow ! 3. Testing ! 4. Libraries and architecture
  2. Tuenti 1.0 • 21 January, 2010. • No API at

    the beginning. It was created along the way. • Only one dev.
  3. Tuenti 2.0 • 13 April, 2011, a year later. •

    Big feature: push notifications. • As Tuenti 1.0, almost no help from design. • Again, only one dev. Created in his free time.
  4. Tuenti 2.X • Comms & social iterations. • Too much

    work for one person… • Mobile team started to grow. • Three people in september 2011 Dani Miguel Luis
  5. Messenger • Started development in december, 2011. • Focused on

    communication with a slight social layer. • Objective: communications tool
  6. • Change of direction: giving back some importance to social.

    • Claim: The easiest way to share experiences with the people that matter right now. • October 2012: Messenger hit the app store. • Rebranding: Tuenti Social Messenger | Tuenti Classic
  7. 3.5

  8. Problems • Tuenti brand is “strong”. Difficult to be considered

    a messenger. • 1.0 quite incomplete. • Mobile Team getting bigger. • Not Mobile First first yet.
  9. New Mobile • One Core Team: support • Product teams:

    • Sharing (MAD) • Comms (MAD) • Cloud Phone (MAD) • Growth (BCN) • Tent (BCN)
  10. • Core Team • Voice • Messaging • Account •

    Social • Onboarding (BCN) • Acquisition (BCN)
  11. Plan • Support for manage SCRUM boards • Create a

    user story / bug / task • Every commit needs a ticket (hook on repository) • Estimate. Visualize remaining time • Assign directly to developer in charge of the feature
  12. Development Process Flow • Develop features • commit and push

    code to repository • create unit/acceptant test. Run them!!! • Fix things. • Meetings!! • More and more code
  13. No matter who. No matter what. No matter when. Short

    term. Long term. Any term. Writing good code is ALWAYS faster than writing bad code.! ROBERT MARTIN (@UNCLEBOB)
  14. Code Reviews • Ensure developers follow coding standards • At

    least 2 developers must “pass” the review to be able to close it. • “It’s nothing personal” • Be proactive with your comments.
  15. Continuous Integration • Jenkins / 2 mac minis • Jobs

    for server and client • Jobs create on command line • tu-jenkins create-dev (unit) • tu-jenkins create-nightly (full regresion) • tu-jenkins pre-integrate <ticket-number> • Unit test every push / instant feedback for the developer • Every night unit and acceptant test execute • Email every morning to the branch owner.
  16. Branch Integration Process • Feature is code complete • Tested

    and QA approved • All the API dependencies are working in live • Translations added and reviewed by QA/PM • Unit and acceptant test!! • Has latest integration merged (with blue ball)
  17. Distribution • Rake hotapps (everything automated) • Clean, checkstyle, archive

    & upload to Hockeyapp • Distribute betas internally in an easy way • Also used for collect crash reports, find bugs • Previously we had Crashlytics • Rake appstore
  18. Release Process Flow • We have enough to go live

    (Product Team) • We take latest stable (blue ball) changeset from integration and create a new Release Candidate branch • QA team makes a regression • 3 days, if something is discovered, release cancelled • Send to Apple!! • Merge back to master, integration and TAG
  19. –Edsger W. Dijkstra “Program testing can be a very effective

    way to show the presence of bugs, but is hopelessly inadequate for showing their absence”
  20. # coding: utf-8 # Author:: David Pastor (mailto:[email protected]) ! Feature:

    Login Screen ! Background: Given I launch the app And in the start screen I go to the login ! # Regular login ! @QAke(1982) Scenario: Verified new messenger user logs in and skips sync process Given a user: "myself" exists with verified: true When in the login screen I do login skipping synchronization with "myself" Then in the conversations screen I check that is the current screen
  21. class LoginScreen < Base ! element :incorrect_credentials_label, "view:'UILabel' marked:'Oops! Make

    sure your…’” element :login_button, "view:'UIButton' marked:'Log In'" element :forgot_password_button, "view:'UIButton' marked:'UITextFieldForgotButton'" ! def navigation_bar @navigation_bar ||= NavigationBar.new(:title => "Log In") do |bar| bar.add_element :join, "view:'UIButton' marked:'Join Tuenti'" end end ! def email_text_field TextField.new("view:'TMPaddingTextField' index:0", :placeholder => "Email") end ! def password_text_field TextField.new("view:'TMPaddingTextField' index:1", :placeholder => "Password") end
  22. Problems • Very slow… • We didn’t like cucumber. •

    We wanted something in Objective-C if possible, not ruby.
  23. KIF

  24. #import "TMKIFScreen.h" ! @interface TMLoginKIFScreen : TMKIFScreen ! // Convenience

    - (void)loginWithEmail:(NSString *)email password:(NSString *)password; ! // Login button - (void)tapOnTheLoginButton; - (void)checkLoginButtonEnabled:(BOOL)expectedEnabled; ! // Email text field - (void)typeIntoEmailTextField:(NSString *)string; - (void)clearEmailTextField; ! @end
  25. @interface TMGetEventInfoOperation : TMServiceOperation ! @end @implementation TMGetEventInfoOperation ! -

    (void)doMain { ... NSArray *unknowns = [[TMUserParser parser] parseUsers:unknownsFromJSON]; ! [[TMUserStorage sharedStorage] addAsUnknowns:unknowns]; ... } ! @end
  26. @interface TMGetEventInfoOperation : TMServiceOperation @property (nonatomic, strong) TMUserParser *userParser; @property

    (nonatomic, strong) TMUserStorage *userStorage; @end @implementation TMGetEventInfoOperation ! - (void)doMain { ... NSArray *unknowns = [self.userParser parseUsers:unknownsFromJSON]; ! [self.userStorage addAsUnknowns:unknowns]; ... } ! @end
  27. • XMPPFramework • Reachability • Mixpanel • Cordova • HockeySDK

    • TheAmazingAudioEngine • Typhoon • KIF • libextobjc
  28. @weakify(self); self.albumKVOToken = [self.album addObserverForKeyPath:@“photoMoments” block:^(id obj, NSDictionary *change) {

    @strongify(self); dispatch_async(dispatch_get_main_queue(), ^{ [self doWhatever]; }) }];
  29. self.commentKVOToken = [comment addObserverForKeyPath:@keypath(comment.state) block:^(id obj, NSDictionary *change) { //

    Comment state just changed }]; @property (nonatomic, copy) NSString *commentKVOToken;
  30. NSInteger const kWiFiMaxOperationCount = 6; NSInteger const kWWANMaxOperationCount = 2;

    NSInteger const kWiFiMaxUploadOperationCount = 2; NSInteger const kWWANMaxUploadOperationCount = 1;
  31. MVP

  32. @protocol TMAlbumStorageDelegate <NSObject> ! @optional - (void)albumStorage:(TMAlbumStorage *)albumStorage didUpdateAlbumsForUser:(TMUser *)user

    withAlbums:(NSArray *)albums; ! - (void)albumStorage:(TMAlbumStorage *)albumStorage didUpdateAlbumWithId:(NSString *)albumId; ! @end
  33. @interface TMAlbumStorage : NSObject ! - (void)addAlbum:(TMAlbum *)album; - (void)removeAlbum:(TMAlbum

    *)album; ! - (NSArray *)albumsForUser:(TMUser *)user; ! - (void)addDelegate:(id<TMAlbumStorageDelegate>)delegate delegateQueue:(dispatch_queue_t)delegateQueue; ! - (void)removeDelegate:(id<TMAlbumStorageDelegate>)delegate delegateQueue:(dispatch_queue_t)delegateQueue; ! - (void)removeDelegate:(id<TMAlbumStorageDelegate>)delegate; ! @end
  34. Dictionary with ongoing operations Operations must have a id<NSCopying> token

    Listen to isFinished keypath via KVO to remove operation from dictionary
  35. - (void)doMain { TURequestOperation *getAlbumInfoOp = [self.requestOperationProvider operationWithName:kGetAlbumInfoOperationName]; [getAlbumInfoOp addValue:self.albumId

    forArgument:kAlbumIdParameterName]; getAlbumInfoOp.successBlock = ^(TURequestOperation *operation, NSDictionary *response) { TMAlbumParser *parser = [[TMAlbumParser alloc] init]; TMAlbum *album = [parser parseAlbumForUser:self.user withDictionary:response[kAlbumResponseKey]]; [[TMAlbumStorage sharedStorage] updateAlbumsForUser:self.user withAlbums:@[album]]; self.album = [[TMAlbumStorage sharedStorage] albumForId:album.albumId user:self.user]; }; getAlbumInfoOp.failureBlock = ^(TURequestOperation *operation, NSDictionary *response, NSError *error) { }; [self startRequest:[getAlbumInfoOp asRequest]]; }
  36. - (void)presentImage { [self cancelOperation]; ! CGSize size = self.imageView.frame.size;

    ! UIImage *image = [TUNetImageProvider imageFromURL:self.imageURL size:size]; if (image) { // Image in memcache. [self updateViewWithImage:image]; } else { [self updateViewWithImage:self.defaultImage]; self.operation = [TUNetImageProvider obtainImageFromURL:self.imageURL size:size delegate:self]; } }