Follow me
RSS feed
My sources
My Viadeo

Développer conjointement sur iPhone et Android

Greg | 04 Jan 2011

Projets Depuis quelques semaines, je travaille sur le développement d'une application iPhone1, et j'ai donc profité de la trêve de Noël pour agrémenter mes connaissances dans le domaine du développement mobile en général. Comme tout utilisateur de Mac et possesseur d'iPhone, je me suis naturellement penché vers ce dernier en oubliant vaniteusement les autres. Dommage2 ! Oui, dommage parce qu'aujourd'hui, oublier une plateforme serait faire preuve de caprice irrationnel tant les parts de marchés des uns et des autres sont importantes3. Heureusement ma conviction qu'il est aujourd'hui important de développer des applications mobiles pour le plus grand nombre avait déjà été perçue par de plus sages que moi. Et c'est donc vers eux que je me suis tourné !

Dans ce post, je vais me concentrer sur une solution de développement conjointe entre iPhone et Android. Cependant, comme je l'ai indiqué, il serait dommage de se limiter à ces deux seules plateformes. En effet, il ne faut pas oublier BlackBerry, Symbian ou Palm.

Les SDKs

Lors de mes premiers essais, ne pensant qu'à l'iPhone, j'ai bien entendu exploré le SDK d'IOS. Comme vous pouvez vous en douter, c'est de l'Objective-C. Et je dois reconnaitre que pour avoir, depuis, regardé ce qui se fait à côté, Apple a fait un formidable travail. Certains le reprocherons certainement, mais le "tout en un" d'Apple à parfois du bon. Cela devient désagréable à partir du moment ou vous ne souhaitez pas passer par Xcode, car dans ce cas, vous allez devoir mettre les mains dans le cambouis au risque de vous engluer. Il subsiste deux autres problèmes. Le premier, malheureusement difficilement surmontable, est qu'à moins d'utiliser un SDK alternatif, tout doit se faire sur Mac. Le second, qui fera hurler4 les plus libristes, est que si vous n'ouvrez pas votre bourse, vos développements resterons à l'état de projet.

Concernant Android, le SDK et en Java. Je ne suis pas un adepte de ce langage, mais je dois reconnaitre que le travail de Google est vraiment agréable. J'ai été un peu dérouté au départ par la méthode à utiliser pour la mise en place des vues. En effet, le fait de devoir gérer à la main les layouts dans des fichiers XML m'a quelque peu déconcerté. Puis je me suis souvenu que c'est, finalement, la même chose que les .xib d'Apple... Oui, sauf qu'avec iOS, je peux créer des vues par programme. Chose que je n'ai pas réussi à faire sous Android5.

Pour BlackBerry, le SDK est disponible, après enregistrement, sur le site développeurs. Là encore il s'agit de Java. Contrairement à Android où vous n'avez aucune obligation concernant les outils à utiliser, il semble qu'ici on préfère vous voir travailler avec Eclipse. Moi qui reproche déjà sa lourdeur à Java, je suis ici servi avec un environnement de développement qui m'oblige à avoir une machine inutilement empâtée ! Autre détail énervant, le site développeurs est, à mon sens, très mal organisé au point que l'on perd beaucoup de temps au départ à trouver son chemin. Pour clore ce sujet, je trouve le SDK très mal foutu et sa documentation trop javadoc-esque.

Symbian m'a donné un peu la même impression que BlackBerry. Le SDK est disponible via le forum Nokia. Pourquoi pas ? Mais, là encore, c'est un peu le foutoir. Et on hésite à tout télécharger ou laisser tomber ! En effet, on vous parle de Qt, WRT et Java. Et débrouillez-vous avec ça ! La première question devant un tel éventail de choix est de comprendre la finalité de chacun pour s'assurer d'arriver à un résultat conforme à la demande.

Le WebOS de HP est certainement le SDK le plus simple à appréhender. En effet, il n'y a aucune tromperie sur le nom, et qui dit WebOS dit développement en HTML et JavaScript ! Et pour ceux qui voudraient mettre les mains dans du plus bas niveau, il existe le PDK (pour Plug-in Development Kit) qui vous permettra (entre autres) d'ajouter les fonctionnalités qui vous manquent dans le SDK. Mon plus gros regret ici a été de devoir installer Virtual Box, utilisé par HP pour son émulateur.

Et Windows Mobile ? Vous me pardonnerez, mais je n'ai même pas osé y jeter un oeil ! N'y voyez rien de philosophique, mais n'ayant pas de machine Windows, je n'ai pas la possibilité de tester ;)

Les alternatives

Après avoir téléchargé tous les environnements cités ci-dessus, vous pouvez développer pour chaque plateforme. Le problème est que vous devez écrire du code pour chaque plateforme cible. Heureusement des solutions existent !

Rhodes est un framework Ruby permettant de produire des applications pour iPhone, Windows Mobile, BlackBerry et Android. Il fonctionne sur un modèle type MVC. L'approche est donc plutôt séduisante. Pour ce qui est du reste, le niveau d'abstraction recherché pour permettre un support multi-plateforme est à mon sens un peu déroutant. En effet, pour ceux qui auraient l'habitude de développer sur une plateforme spécifique, il va falloir tout réapprendre. Donc si l'effet escompté, à savoir, un seul code, plusieurs plateformes est clairement une réussite, dans le cas où ce n'est pas l'objectif principale, et que l'on souhaite créer une application spécifique à une plateforme, il semble préférable de revenir au SDK ou de trouver une meilleure alternative.

Rhodes est disponible sous licence MIT mais qu'il est également possible d'acquérir une licence Entreprise ou Commerciale.

PhoneGap est lui aussi un framework pour la création d'applications mobiles cross-plateforme. Il se distingue de Rhodes par le fait qu'il a choisi le trio HTML/CSS/JavaScript pour le développement des applications. Ce choix me semble assez judicieux. En effet, avec l'émergence des solutions dans les nuages, il n'est pas difficile de trouver du monde pour développer avec ce type de technologie. Malheureusement il ne faut pas se tromper. PhoneGap propose bien du développement cross-plateforme et non pas multi-plateforme. Ainsi donc, si vous pouvez facilement transposer votre code (HTML/CSS/JS) d'une application iPhone vers Android, il n'en reste pas moins qu'il faudra générer ces applications avec leurs outils spécifiques. De plus, en restant à un niveau fonctionnel très bas, PhoneGap ne propose aucun support des vues spécifiques à chaque plateforme. Ainsi vous trouverez tout ce qu'il faut pour gérer l'accéléromètre, la caméra ou le reste, il faudra vous débrouiller pour la partie IHM.

PhoneGap est diffusé sous licence MIT.

Autre alternative, oubliée dans la version initiale de ce post : Titanium Mobile. Ce projet est très certainement ce qui se rapproche le plus de mon besoin. En effet, non seulement il permet de générer avec un même code, une application pour plusieurs plateformes, mais en plus il supporte les vues natives... Mais (il y a toujours un mais) il souffre du même problème que Rhodes. A savoir, il utilise une organisation propre des sources du projet sans possibilité de retrouver la structure spécifique à chaque plateforme. Vous allez dire que j'insiste un peu lourdement sur ce point particulier, mais il me semble important, surtout si vous souhaitez pouvoir customiser certaines choses pour une plateforme en particulier. Autre détail, si le support du BlackBerry est dans les plans, il se limite aujourd'hui à iOS et Android, en distinguant clairement, pour le premier, iPhone/iTouch et iPad. Quoi qu'il en soit, je dois être franc, c'est certainement la solution que je préconiserais avant les précédentes.

Titanium existe sous trois versions : Community qui est gratuite, Professional à 199 USD/mois/développeur et Enterprise dont le prix n'est pas communiqué.

Bien qu'il ne supporte qu'iOS, je voudrais dire un mot de Nimble Kit. Ce projet est intéressant par le fait qu'il propose de développer des applications iPhone/iPad en se basant, lui aussi, sur le trio HTML/CSS/JS. Cependant, il ajoute la dimension qui manque à PhoneGap, à savoir, le support des vues natives.

Les devices

Après cette longue introduction, et avant d'entrer dans le vif du sujet, je voudrais faire un petit point sur les devices. En effet, développer une application et la tester sur un émulateur est une chose, la tester sur un vrai device en est une autre. S'agissant d'applications mobiles, on aura le réflexe de penser qu'il va falloir s'acquitter d'un appareil pour chaque plateforme. Couteux ! Malheureusement il n'y a pas de solution miracle. En effet, si l'iPod Touch permet de se faire une idée du fonctionnement de son application in situ, je n'ai rien trouvé pour les autres (Android, RIM, Symbian ou webOS). Une lueur d'espoir existe pour Android...

So ?

Au vu de ce que j'ai exposé ci-dessus, vous l'aurez compris, si les différentes solutions disponibles semblent intéressantes, aucune ne satisfait pleinement mon besoin. En effet, je recherche une solution permettant de créer simplement une application en évitant de réécrire du code spécifique, en conservant la structure des applications de chaque plateforme et me permettant d'utiliser au maximum les composants et vues natifs de chaque OS.

Seule solution : développer mon propre framework !

Pour faire cela, j'ai pris les positions suivantes :

Pour répondre à ces besoins, et trouver les meilleurs compromis, j'ai d'abord dû me familiariser avec les SDKs que je n'avais jamais eu l'occasion de pratiquer. C'est le cas pour BlackBerry, Symbian et webOS. J'ai également passé beaucoup de temps à lire le code de Rhodes et PhoneGap qui m'ont souvent mis sur de très bonnes pistes pour résoudre certains problèmes. Enfin, j'ai pas mal abusé de mon entourage afin de trouver ceux et celles possédant les devices qui me permettraient de tester mes idées.

Pour ce qui concerne le support d'un maximum de plateformes, la solution est vite vue. Il faut mettre en place un framework suffisamment générique, mais puisant toute la puissance du SDK sous-jacent. Cela passe donc, pour chaque fonctionnalité, à retrouver les similarités ou à développer celles qui n'existeraient pas pour une plateforme donnée. J'illustrerai cela avec l'exemple Toast dans la suite de cet article.

Un framework facile à utiliser implique une solution suffisamment (re)connue pour que chacun puisse y mettre les mains sans devoir apprendre un nouveau langage. Java aurait pu être une bonne piste. Cependant, outre la désaffection que je lui porte, il n'est pas supporté par iOS. J'ai donc opté pour le trio HTML/CSS/JavaScript. Ce choix a également été motivé par la possibilité d'utiliser le framework pour créer des WebApps !

En ce qui concerne les deux dernières positions à suivre, j'ai opté pour la création d'outils avec Ruby. Pour simplifier les choses, j'ai créé un unique outil modulaire permettant de générer une nouvelle application, d'ajouter des composants dans le framework ou de manière spécifique à une application.

Je suis encore très loin du résultat souhaité, mais cela devient prometteur, et j'ai bon espoir de mettre en ligne sur l'App Store et l'Android Market une application dans les semaines à venir.

Android

Commençons par voir comment créer une application Android.

La création d'un projet Android se fait en utilisant la commande suivante :

android create project --package net.algorithmique.hello --activity Hello --target 7 --path HelloAndroid

Le paramètre --package, obligatoire, permet d'indiquer le nom du package pour l'application. Il doit forcement respecter la convention de nommage des packages Java. --activity, lui aussi obligatoire, permet de donner le nom de l'activité par défaut de l'application. le paramètre --target (obligatoire) nous permet de préciser la plateforme cible. Pour obtenir la liste de ces cibles, vous pouvez exécuter la commande android list target. Dans mon cas, 7 représente la cible suivante :

id: 7 or "android-8"
     Name: Android 2.2
     Type: Platform
     API level: 8
     Revision: 2
     Skins: HVGA (default), QVGA, WQVGA400, WQVGA432, WVGA800, WVGA854

Une fois la commande exécutée, nous obtenons un répertoire HelloAndroid, contenant la structure de notre application. Avant de modifier quoi que ce soit, nous pouvons déjà tester ce qui a été généré dans l'émulateur. Il faut donc préparer l'environnement. Pour cela, exécutez la commande android. Dans la fenêtre qui s'ouvre, sélectionnez Virtual device, sur la gauche. Dans notre exemple, nous avons besoin d'un device virtuel correspondant à notre cible. Ajoutez-le en cliquant sur New.... Dans la fenêtre qui s'ouvre, donnez un nom à votre device et sélectionnez sur la ligne Target la cible Android 2.2 - API level 8. Terminez en cliquant sur Create AVD. Vous devez maintenant voir, dans la liste des AVD, votre nouveau device. Cliquez sur Start...

Nous allons maintenant compiler et installer notre application. Pour la compilation, un ant debug6 exécuté à la racine de notre projet devrait générer un package Hello-debug.apk dans le sous-répertoire bin de notre application. Pour installer cet apk, nous allons utiliser la commande adb install bin/Hello-debug.apk7.

Sur l'émulateur, vous devez maintenant trouver une application Hello que vous pouvez exécuter en cliquant dessus.

Dans le cadre du développement du framework, voulant développer les applications en HTML/CSS/JS, nous avons besoin de modifier l'application pour permettre l'affichage de pages HTML. Pour cela nous devons modifier la vue principale pour y placer une WebView. Pour cela nous devons modifier le layout. Dans les sources du projet, ouvrez le fichier res/layout/main.xml. Modifiez ce fichier de façon à ce que son contenu devienne le suivant :

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<TextView  
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text="Hello World, Hello"
    />
</LinearLayout>

Nous devons maintenant modifier le code de façon à afficher la WebView. Editez donc le fichier src/net/algorithmique/hello/Hello.java. Dans ce fichier nous nous intéressons à la méthode onCreate. En effet, c'est à ce niveau que nous mettons en place l'interface. Nous allons donc modifier ce fichier de la façon suivante :

 1 package net.algorithmique.hello;
 2 
 3 import android.app.Activity;
 4 import android.os.Bundle;
 5 
 6 import android.webkit.WebSettings;
 7 import android.webkit.WebView;
 8 import android.webkit.WebViewClient;
 9 import android.webkit.WebSettings.LayoutAlgorithm;
10 import android.widget.LinearLayout;
11 import android.view.ViewGroup;
12 import android.graphics.Color;
13 
14 import java.net.URI;
15 import java.net.URISyntaxException;
16 
17 public class Hello extends Activity {
18   private LinearLayout root;
19   protected WebView appView;
20   
21   /** Called when the activity is first created. */
22   @Override
23   public void onCreate(Bundle savedInstanceState) {
24     super.onCreate(savedInstanceState);
25     
26     // Mise en place du layout principale
27     this.root = new LinearLayout(this);
28     this.root.setOrientation(LinearLayout.VERTICAL);
29     this.root.setBackgroundColor(Color.BLACK);
30     this.root.setLayoutParams(
31       new LinearLayout.LayoutParams(
32         ViewGroup.LayoutParams.FILL_PARENT, 
33         ViewGroup.LayoutParams.FILL_PARENT, 
34         0.0F
35     ));
36     
37     // Mise en place de la WebView
38     this.appView = new WebView(Hello.this);
39     this.appView.setLayoutParams(
40       new LinearLayout.LayoutParams(
41         ViewGroup.LayoutParams.FILL_PARENT, 
42         ViewGroup.LayoutParams.FILL_PARENT, 
43         1.0F
44     ));
45     this.appView.setInitialScale(100);
46     this.appView.setVerticalScrollBarEnabled(false);
47     this.appView.requestFocusFromTouch();
48     
49     // Activer JavaScript
50     WebSettings settings = this.appView.getSettings();
51     settings.setJavaScriptEnabled(true);
52     settings.setJavaScriptCanOpenWindowsAutomatically(true);
53     
54     // Rattachement de la WebView au layout principale
55     this.root.addView(this.appView);
56     setContentView(this.root);
57   
58     // Chargement de la page de contenu
59     this.appView.loadUrl(this.pathForResource("index.html"));
60   }
61   
62   /**
63    * Cette méthode permet de récupérer l'URI d'un fichier placé 
64    * dans les ressources (répertoire /assets/app) de l'application 
65    */
66   public String pathForResource(String ressource) {
67     URI loadUri = null;
68     StringBuffer finalUri = new StringBuffer();
69   
70     try {
71       loadUri = new URI(ressource.toString());
72     } catch (URISyntaxException e) {
73       e.printStackTrace();
74     }
75   
76     if( loadUri.getScheme() == null ) {
77       finalUri.append("file:///android_asset/app/");
78       finalUri.append(ressource);
79     } else {
80       finalUri.append(ressource);
81     }
82   
83     return finalUri.toString();
84   }
85 }

La partie intéressante du code se situe entre les lignes 26 et 59.

De la ligne 26 à 35, nous mettons en place un layout linéaire permettant d'arranger ses vues enfants sur une unique ligne et colonne. On le définit comme étant vertical (ligne 28) avec un fond noir (ligne 29). On demande, via la méthode LinearLayout.LayoutParams (int width, int height, float weight) à ce que ce layout ait la même largeur et hauteur que son parent avec un poids de 0 (lignes 31-35).

De la ligne 37 à 47 nous mettons en place la WebView. Elle aussi doit remplir toute la place prise par son parent (lignes 39 à 44) avec une échelle de 100% (ligne 45) et une scollbar verticale sans style overlay8 (ligne 46). On termine ligne 47 en donnant le focus à la vue.

Le framework que nous souhaitons développer devant utiliser JavaScript, nous activons ce dernier (ligne 49 à 52) pour la WebView.

Nous rattachons enfin la WebView au layout principal (lignes 54-56) et nous terminons en chargeant la page index.html.

Le chargement de la page HTML se fait en utilisant la méthode String pathForResource(String ressource) que nous déclarons lignes 62 à 84. Cette méthode se charge de récupérer le chemin d'accès complet à un fichier de ressource de l'application. Ces ressources seront placées dans le répertoire /assets/app de notre application. Dans la méthode pathForResource nous regardons le schéma de la ressource passée en paramètre. S'il n'y en à pas, alors nous préfixons la ressource avec file:///android_asset/app/, sinon nous le laissons inchangé. Ceci nous permettra de charger des pages externes types http ou autre.

Maintenant que nous avons la base de notre application, vous pouvez vous amuser à modifier la page index.html, en y ajoutant du JavaScript, voire même en ajoutant de nouvelles pages dans les ressources en les liant entre elles. Tout cela reste très basique et nous devons donc ajouter l'accès à des APIs natives du SDK Android via JavaScript.

iPhone

Avant d'avancer sur la partie JavaScript, voyons comment mettre en place une application, similaire à celle développée sur Android, sur iPhone.

Pour créer l'application, ouvrez Xcode et créer une nouvelle application nommée HelloiPhone de type View-based Applications pour iOS.

Dans les sources de l'application, vous devez trouver, dans le groupe Classes, les fichiers HelloiPhoneAppDelegate.h, HelloiPhoneAppDelegate.m, HelloiPhoneViewController.h et HelloiPhoneViewController.m.

La première chose a faire consiste à ajouter un UIWebView à notre application. Pour cela nous devons commencer par modifier le contrôleur de la vue principale. Editez donc le fichier HelloiPhoneViewController.h et modifiez-le de la façon suivante :

1 #import <UIKit/UIKit.h>
2 
3 @interface HelloiPhoneViewController : UIViewController {
4   UIWebView *webView;
5 }
6 
7 @property (nonatomic, retain)  UIWebView* webView;
8 
9 @end

Comme vous pouvez le voir, nous ajoutons un UIWebView dans l'interface en la déclarant en qualité de propriété. Pour que cela soit complet, il faut également éditer le fichier HelloiPhoneViewController.m et ajouter un @synthesize webView; juste après le début de l'implémentation :

 1 #import "HelloiPhoneViewController.h"
 2 
 3 @implementation HelloiPhoneViewController
 4 
 5 @synthesize webView;
 6 
 7 - (void)didReceiveMemoryWarning {
 8   // Releases the view if it doesn't have a superview.
 9   [super didReceiveMemoryWarning];
10 }
11 
12 - (void)viewDidUnload {
13   // Release any retained subviews of the main view.
14   // e.g. self.myOutlet = nil;
15 }
16 
17 - (void)dealloc {
18   [super dealloc];
19 }
20 
21 @end

Ceci fait, nous allons maintenant implémenter la mise en place de la vue Web et y charger une page d'index. Pour cela il faut modifier le fichier HelloiPhoneAppDelegate.h et ajouter dans la déclaration de l'interface un UIWebView :

 1 #import <UIKit/UIKit.h>
 2 
 3 @class HelloiPhoneViewController;
 4 
 5 @interface HelloiPhoneAppDelegate : NSObject<UIApplicationDelegate,UIWebViewDelegate> {
 6   UIWindow *window;
 7   UIWebView *webView;
 8 
 9   HelloiPhoneViewController *viewController;
10 }
11 
12 @property (nonatomic, retain) IBOutlet UIWindow *window;
13 @property (nonatomic, retain) IBOutlet HelloiPhoneViewController *viewController;
14 
15 @end

Outre l'ajout de l'UIWebView (ligne 7), l'élément notable ici et l'ajout du protocole UIWebViewDelegate à la classe HelloiPhoneAppDelegate (ligne 5).

Pour initialiser la vue Web, nous devons maintenant modifier la méthode application:didFinishLaunchingWithOptions: de HelloiPhoneAppDelegate.m :

  1 #import "HelloiPhoneAppDelegate.h"
  2 #import "HelloiPhoneViewController.h"
  3 
  4 @implementation HelloiPhoneAppDelegate
  5 
  6 @synthesize window;
  7 @synthesize viewController;
  8 
  9 + (NSString*) pathForResource:(NSString*)resourcepath {
 10   NSBundle *mainBundle = [NSBundle mainBundle];
 11   NSMutableArray *directoryParts = [NSMutableArray arrayWithArray:[resourcepath componentsSeparatedByString:@"/"]];
 12   NSString *filename = [directoryParts lastObject];
 13   [directoryParts removeLastObject];
 14   
 15   NSString *directoryStr = [NSString stringWithFormat:@"%@", [directoryParts componentsJoinedByString:@"/"]];
 16   return [mainBundle pathForResource:filename
 17                               ofType:@""
 18                          inDirectory:directoryStr];
 19 }
 20 
 21 #pragma mark -
 22 #pragma mark Application lifecycle
 23 
 24 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    
 25   // Récupération de la taille de la vue principale
 26   CGRect screenBounds = [[UIScreen mainScreen] bounds];
 27   self.window = [[[UIWindow alloc] initWithFrame:screenBounds] autorelease];
 28   
 29   // Creation de la WebView
 30   webView = [[UIWebView alloc] initWithFrame:screenBounds];
 31   [webView setAutoresizingMask: (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight) ];
 32   viewController.webView = webView;
 33   [viewController.view addSubview:webView];
 34   webView.delegate = self;
 35   
 36   // Ajout de la vue du view controller à la fenêtre principale.
 37   [self.window addSubview:viewController.view];
 38   
 39   // Chargement de la page d'index
 40   NSString* startPage = @"index.html";
 41   NSURL *appURL = [NSURL URLWithString:startPage];
 42   if(![appURL scheme]) {
 43     appURL = [NSURL fileURLWithPath:[[self class] pathForResource:startPage]];
 44   }
 45   NSURLRequest *appReq = [NSURLRequest requestWithURL:appURL cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:20.0];
 46   [webView loadRequest:appReq];
 47   
 48   // Affichage de la fenêtre
 49   [self.window makeKeyAndVisible];
 50   
 51   return YES;
 52 }
 53 
 54 - (void)applicationWillResignActive:(UIApplication *)application {
 55   /*
 56    Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
 57    Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
 58    */
 59 }
 60 
 61 - (void)applicationDidEnterBackground:(UIApplication *)application {
 62   /*
 63    Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 
 64    If your application supports background execution, called instead of applicationWillTerminate: when the user quits.
 65    */
 66 }
 67 
 68 - (void)applicationWillEnterForeground:(UIApplication *)application {
 69   /*
 70    Called as part of  transition from the background to the inactive state: here you can undo many of the changes made on entering the background.
 71    */
 72 }
 73 
 74 - (void)applicationDidBecomeActive:(UIApplication *)application {
 75   /*
 76    Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
 77    */
 78 }
 79 
 80 - (void)applicationWillTerminate:(UIApplication *)application {
 81   /*
 82    Called when the application is about to terminate.
 83    See also applicationDidEnterBackground:.
 84    */
 85 }
 86 
 87 #pragma mark -
 88 #pragma mark Memory management
 89 
 90 - (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
 91   /*
 92    Free up as much memory as possible by purging cached data objects that can be recreated (or reloaded from disk) later.
 93    */
 94 }
 95 
 96 - (void)dealloc {
 97   [viewController release];
 98   [window release];
 99   [super dealloc];
100 }
101 
102 @end

Dans le code précédent, nous commençons par récupérer les dimensions de l'écran (ligne 26) et nous initialisons la fenêtre principale de l'application (ligne 27). Nous initialisons ensuite l'UIWebView (ligne 30), nous la rendons flexible en hauteur et largeur de façon à ce qu'elle occupe tout l'espace (ligne 31), et nous la déclarons auprès du contrôleur de vues (ligne 32) puis nous l'ajoutons comme vue enfant de la vue principale du contrôleur (ligne 33). Nous terminons cette mise en place en lui déclarant la classe courante comme classe de délégation.

Nous ajoutons ensuite la vue principale du contrôleur de vue, comme enfant de la fenêtre principale de l'application (ligne 37).

Pour charger la page d'index (lignes 40 à 46) nous utilisons la méthode pathForResource: que nous avons déclaré dans la classe courante. Cette méthode devra avoir un comportement similaire à la méthode pathForResource que nous avons créé dans l'application Android.

Nous terminons en rendant visible la fenêtre principale de l'application.

Avant de compiler et tester, il faut ajouter le fichier index.html. Comme vous pouvez le voir dans le code, ce fichier est recherché dans les ressources. Vous pouvez donc le placer dans le groupe Resources du projet. Personnellement, afin de calquer la méthode utilisée sous Android, j'ai créé un groupe Assets avec un sous groupe app, et c'est à ce niveau que je place les fichiers HTML, CSS et JavaScript.

Nous avons là encore une base pour notre version iPhone. Comme pour la version Android, amusez vous à modifier le fichier d'index, lui ajouter du JavaScript et à lier des pages.

JavaScript, aller et retour

Sur Android

Maintenant que nous savons comment sont architecturées les applications Android et iPhone, voyons comment mettre en place notre framework. Nous avons donc besoin d'être capables de faire des appels JavaScript qui vont eux-mêmes servir de pont vers des appels au SDK natif de la plateforme.

Sur Android, ceci se fait relativement simplement. En effet, le SDK met à notre disposition la méthode addJavascriptInterface. Cette méthode prend en paramètre un objet définissant une nouvelle interface JavaScript et un nom pour cette interface. L'utilisation est très simple, et la classe servant à définir la nouvelle interface doit simplement recevoir le contexte applicatif. Nous aurons donc des définitions de classes qui ressemblent à quelque chose comme cela :

 1 package net.algorithmique.hello;
 2 
 3 import android.util.Log;
 4 import net.algorithmique.hello.Hello;
 5 
 6 // Specifique
 7 // import ...
 8 
 9 public class MyJavaScriptInterface {
10   public static final String NAMESPACE = "MyInterface";
11   private static final String TAG = "Hello";
12 
13   Hello mContext;
14   
15   public MyJavaScriptInterface(Hello c) {
16     mContext = c;
17   }
18   
19   /**
20    * Display a (debug) log message
21    *
22    * @param {String} data massage to be displayed
23    *
24    * @example
25    * MyInterface.log("This is a debug message!")
26    */
27   public void log(final String data) {
28     Log.d(TAG, data);
29   }
30 }

Dans cet exemple nous avons défini une interface dont l'unique méthode (log) permet d'afficher un message de debug sur la console. Pour faire prendre en compte cette nouvelle interface dans notre application, il suffit d'ajouter l'appel this.appView.addJavascriptInterface(new MyJavaScriptInterface(this), MyJavaScriptInterface.NAMESPACE); entre les lignes 52 et 54 du fichier Hello.java. Il faudra, bien entendu, ne pas oublier l'import en début de fichier.

Pour tester, modifiez le fichier index.html en lui ajoutant une ligne genre <input type="button" value="Log!" onclick="MyInterface.log('Ce message apparaitra dans les logs!');" />. Recompilez, réinstallez l'application. Affichez ensuite les logs en exécutant la commande adb logcat puis démarrer l'application. Si vous cliquez sur le bouton que nous venons d'ajouter, vous devriez voir apparaitre le message :

D/Hello   (  267): Ce message apparaitra dans les logs!

Cet exemple est relativement trivial. En effet, la méthode que nous avons ajoutée ne renvoie rien. Nous pourrions très facilement renvoyer quelque chose, il suffirait de déclarer un retour à la méthode de notre interface. Malheureusement, comme nous le verrons plus tard, cela ne sera pas possible sur iPhone. Or si nous voulons avoir un framework commun, il faut travailler avec la contrainte la plus forte. Heureusement nous pouvons utiliser des callbacks.

Une fonction de callback qui serait passée en paramètre à une méthode d'interface serait reçue comme une String. Ce qui pourrait passer comme un inconvénient au départ est en fait une chance, car cela nous permettra d'accepter les fonctions anonymes. Pour illustrer cela, imaginons que nous souhaitons ajouter dans notre interface une méthode networkInformation, nous permettant de connaitre le type (s'il existe) de connexion réseau activé. Nous définirons cette méthode de la façon suivante :

public static final int NO_NETWORK = 0;
public static final int CELLULAR_NETWORK = 1;
public static final int WIFI_NETWORK = 2;

// ...

public void networkInformation(final String callback) {
  String callbackName = mContext.findCallbackName(callback);
  NetworkInfo info = connManager.getActiveNetworkInfo();
  int available = MyJavaScriptInterface.NO_NETWORK;
  if (info != null) {
    available = info.getType();
  }
  this.mContext.callJavaScriptFunction(callbackName, available);
}

Comme vous pouvez le voir, nous utilisons la méthode findCallbackName définie dans la classe Hello. Elle permet d'isoler le nom de la méthode callback à appeler, ou à récupérer le code dans le cas d'une méthode anonyme. Son code est le suivant :

public String findCallbackName(String callbackId) {
  String callbackName = null;
  int nbCallback = 0;
  
  Pattern p = Pattern.compile("^\\s*function\\s+([^\\s\\(]+)");
  Matcher m = p.matcher(callbackId);

  while (m.find()) {
    callbackName = m.group(1);
    nbCallback++;
  }

  if(nbCallback > 1) {
    callbackName = null;
  }

  if(nbCallback == 0) {
    p = Pattern.compile("^\\s*function\\s*\\(([^\\)]*)\\)");
    m = p.matcher(callbackId);
    String callbackArgs = "";
    nbCallback = 0;
    while(m.find()) {
      callbackArgs = m.group(1);
      nbCallback++;
    }

    if(nbCallback == 0) {
      callbackName = callbackId;
    } else {
      callbackName = "("+callbackId+")("+callbackArgs+")";
    }
  }

  return callbackName;
}

La seconde méthode utilisée est callJavaScriptFunction (également définie dans la classe Hello). Elle réalise l'appel de la méthode. Son code est le suivant :

public void callJavaScriptFunction(final String callbackId, final int data) {
  this.appView.loadUrl("javascript:(function() { "+callbackId+"("+data+"); })()");
}

Ayant cela, vous pouvez maintenant récupérer le type de connexion en utilisant l'exemple suivant :

<html>
  <head>
    <meta name="viewport" content="width=320; user-scalable=no" />
    <meta http-equiv="Content-type" content="text/html; charset=utf-8">

    <script type="text/javascript">
      function netInfo(type) {
        element = document.getElementById('networkProperties');
        var networkInfo = "Network not available!"
        switch(type) {
          case MyInterface.CELLULAR_NETWORK:
            networkInfo = "Network reachable via Cellular Network!"
          break;
          case MyInterface.WIFI_NETWORK:
            networkInfo = "Network reachable via WIFI Network!"
          break;
          
          default:
            networkInfo = "Network not reachable!"
        }
         element.innerHTML = networkInfo
      }
    </script>
  </head>
  
  <body>
    <div id="networkProperties"></div>
    <input type="button" value="Network Info" onclick="MyInterface.networkInformation(netInfo);">
  </body>
</html>

Sur iPhone

Portons maintenant le travail réalisé sur Android sur iPhone.

Sur iPhone, les choses sont un peu plus compliquées. En effet, il n'existe pas d'équivalent à la méthode addJavascriptInterface du SDK Android. Pour contourner ce problème, nous allons utiliser la méthode webView:shouldStartLoadWithRequest:navigationType:. Cette méthode nous permet d'intercepter l'ensemble des requêtes effectuées depuis la vue Web. Nous pourrons récupérer, dans cette méthode, l'URL de la requête et faire un traitement en conséquence. Pour identifier les appels spécifiques à notre framework, nous partirons du principe que les URLs d'appels aux méthodes de notre framework devront avoir un schéma spécifique.

Si nous reprenons les méthodes log et networkInformation définies sous Android, nous intercepterons les appels correspondant pour les URLs suivantes :

Ces URLs sont donc formatées de la façon suivante : framework://<Namespace>.<method>/argument1/argument2/.... Bien entendu nous sommes assez loin des interfaces définies sur Android. Mais une petite encapsulation dans du JavaScript devrait résoudre cela facilement :

MyInterface = {
  log: function(message) {
    url = "framework://MyInterface.log/"+encodeURIComponent(message);
    document.location = url;
  },
  
  networkInformation: function(callback) {
    url = "framework://MyInterface.networkInformation/"+getFunctionName(callback);
    document.location = url;
  }
};

var _anomFunkMap = {};
var _anomFunkMapNextId = 0; 

function anomToNameFunk(fun) {
  var funkId = "f" + _anomFunkMapNextId++;
  var funk = function() {
    fun.apply(this,arguments);
    _anomFunkMap[funkId] = null;
    delete _anomFunkMap[funkId];  
  }
  _anomFunkMap[funkId] = funk;
  
  return "_anomFunkMap." + funkId;
}

function GetFunctionName(fn) {
  if(fn) {
    var m = fn.toString().match(/^\s*function\s+([^\s\(]+)/);
    return m ? m[1] : anomToNameFunk(fn);
  } else {
    return null;
  }
}

Ayant cela, il ne nous reste plus qu'à récupérer les appels utilisant le schéma framework dans webView:shouldStartLoadWithRequest:navigationType: (et laisser passer les autres) et rediriger les demandes vers les méthodes correspondantes. Pour ce qui concerne les appels de callback, nous utiliserons la méthode stringByEvaluatingJavaScriptFromString: d'UIWebView en construisant l'appel dynamiquement.

Je vous laisse le plaisir d'implémenter cela par vous même ;)

Utiliser les vues natives

Pour utiliser les vues natives de chaque plateforme, nous sommes confrontés à deux cas de figure :

Le premier cas s'illustre facilement. Par exemple, j'ai considéré qu'un UITabBar sur iPhone est équivalent à une TabHost sur Android. Dans le cas présent, seule la position change, mais fonctionnellement le résultat est similaire.

Pour illustrer le second cas de figure, je prendrais l'exemple du widget Toast d'Android qui n'existe pas sur iPhone.

Le widget Toast d'Android permet d'afficher un message à l'écran, un peu à la manière d'une alerte JavaScript. La différence est que cette notification est non seulement plus jolie, mais affichée pendant une durée déterminée et disparait donc sans intervention de l'utilisateur. Voici un petit exemple de ce que nous souhaitons obtenir :

Pour mettre cela en place sous Android, il suffit d'utiliser la classe android.widget.Toast. Dans une interface JavaScript, nous définirons la méthode de la façon suivante :

public void showToast(String toast) {
    Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show();
}

Malheureusement, il n'existe rien de similaire sur iOS. J'ai donc créé une nouvelle vue ToastView9, héritant de UIView, et permettant de simuler le comportant vu sur Android :

@implementation Toast

- (void) showToast:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options {
  NSLog(@"MESSAGE = %@", [arguments objectAtIndex:0]);
  
  [self displayWithMessage:[arguments objectAtIndex:0]];
}

- (void)displayWithMessage:(NSString *)message {
  CGRect bounds = [self.webView bounds];
  
  UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(
    10,
    10,
    bounds.size.width/2,
    100
  )];
  label.text = message;
  label.backgroundColor = kDefaultTextBGColor;
  label.textColor = [UIColor whiteColor];
  label.textAlignment = UITextAlignmentCenter;
  label.lineBreakMode = UILineBreakModeClip;
  label.numberOfLines = 3;
  label.adjustsFontSizeToFitWidth = true;

  [label sizeToFit];

  float width = label.bounds.size.width + 20;
  float height = label.bounds.size.height + 20;
  
  toast = [[ToastView alloc] initWithFrame:CGRectMake(
    bounds.origin.x + (bounds.size.width - width) / 2, 
    bounds.origin.y + bounds.size.height - height - 100, 
    width, 
    height
  )];
    
  
  [toast addSubview:label];
  [label release];
  
  [self.webView addSubview:toast];
  [toast release];
  
  [NSTimer scheduledTimerWithTimeInterval:3.0 
                                   target:self 
                                 selector:@selector(hideToast:) 
                                 userInfo:nil 
                                  repeats:NO];
}

-(void)hideToast:(NSTimer*)timer {
  [toast removeFromSuperview];
}

@end

@implementation ToastView

@synthesize strokeColor;
@synthesize rectColor;
@synthesize strokeWidth;
@synthesize cornerRadius;

- (id)initWithFrame:(CGRect)frame {
  self = [super initWithFrame:frame];
  if (self) {
    // Initialization code
    self.opaque = NO;
    self.strokeColor = kDefaultStrokeColor;
    self.backgroundColor = [UIColor clearColor];
    self.rectColor = kDefaultRectColor;
    self.strokeWidth = kDefaultStrokeWidth;
    self.cornerRadius = kDefaultCornerRadius;
  }
  return self;
}

- (id)initWithCoder:(NSCoder *)coder {
  self = [super initWithCoder:coder];
  if (self) {
    // Initialization code
    self.opaque = NO;
    self.strokeColor = kDefaultStrokeColor;
    self.backgroundColor = [UIColor clearColor];
    self.rectColor = kDefaultRectColor;
    self.strokeWidth = kDefaultStrokeWidth;
    self.cornerRadius = kDefaultCornerRadius;
  }
  return self;
}

- (void)drawRect:(CGRect)rect {
  CGContextRef context = UIGraphicsGetCurrentContext();
  CGContextSetLineWidth(context, strokeWidth);
  CGContextSetStrokeColorWithColor(context, self.strokeColor.CGColor);
  CGContextSetFillColorWithColor(context, self.rectColor.CGColor);
  
  CGRect rrect = self.bounds;
  
  CGFloat radius = cornerRadius;
  CGFloat width = CGRectGetWidth(rrect);
  CGFloat height = CGRectGetHeight(rrect);
  
  if (radius > width/2.0)
    radius = width/2.0;
  if (radius > height/2.0)
    radius = height/2.0;    
  
  CGFloat minx = CGRectGetMinX(rrect);
  CGFloat midx = CGRectGetMidX(rrect);
  CGFloat maxx = CGRectGetMaxX(rrect);
  CGFloat miny = CGRectGetMinY(rrect);
  CGFloat midy = CGRectGetMidY(rrect);
  CGFloat maxy = CGRectGetMaxY(rrect);
  CGContextMoveToPoint(context, minx, midy);
  CGContextAddArcToPoint(context, minx, miny, midx, miny, radius);
  CGContextAddArcToPoint(context, maxx, miny, maxx, midy, radius);
  CGContextAddArcToPoint(context, maxx, maxy, midx, maxy, radius);
  CGContextAddArcToPoint(context, minx, maxy, minx, midy, radius);
  CGContextClosePath(context);
  CGContextDrawPath(context, kCGPathFillStroke);
}

- (void)dealloc {
  [strokeColor release];
  [rectColor release];
  [super dealloc];
}

@end

La suite...

Comme je vous l'ai dit plus haut, mon framework est encore dans une phase très active de développement, au point que je n'ai encore rien rendu publique. Mais je dois dire que cela faisait longtemps que je ne m'étais pas autant amusé avec un développement personnel, et pour les plus curieux, sachez que je publierai régulièrement l'avancée de mon travail sur ce site.


1 Nous aurons certainement l'occasion d'en reparler ;)
2 C'est une erreur que je corrige !
3 http://fr.wikipedia.org/wiki/Smartphone.
4 Bien que véritable Apple addict, je dois avouer que cette prise en otage des développeurs par Apple m'a toujours énervé au plus haut point... Je hurlerais donc avec vous !
5 Si vous avez la solution, je prends avec plaisir !
6 Et pourquoi pas release ? Pour éviter pour le moment de perdre du temps avec les questions de signature d'applications...
7 Si vous rencontrez un problème lors de cette installation, relancer le serveur adb via un adb kill-server; adb start-server.
8 Donc invisible !
9 Pour cela je me suis très largement inspiré de cet article. Je vous recommande également de regarder les possibilités offertes par l'excellent Opacity pour la création de vues personnalisées.

Copyright © 2009 - 2011 Grégoire Lejeune.
All documents licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 2.5 License, except ones with specified licence.
Powered by Jekyll.