ASP.NET MVC 3

Desenvolva aplicativos Web híbridos, nativos e móveis

Shane Church

Baixar o código de exemplo

Você deseja criar um aplicativo móvel, mas está perplexo com a variedade de dispositivos disponíveis e APIs a aprender. Que plataforma móvel deveria escolher? O iOS da Apple (iPhone e iPad) usa Objective C, o Android da Google usa Java e o Windows Phone usa Silverlight, e cada uma dessas opções tem uma API diferente e um mercado diferente. A decisão de se concentrar em uma determinada família de tecnologia pode fazer com que 50% do mercado, ou mais, seja incapaz de usar seu aplicativo. Se você se decidir por tentar suportar todas essas plataformas, terá, pelo menos, três bases de código diferentes para manter, aumentando significativamente seus custos de desenvolvimento e manutenção.

Existe outra opção: você pode criar um aplicativo Web móvel, porque ele poderá ser visto em qualquer um desses dispositivos. Mas essa abordagem apresenta alguns obstáculos. O maior obstáculo para o desenvolvimento de um aplicativo de negócios completo usando HTML e JavaScript é a falta de acesso a muitos recursos de hardware em dispositivos nativos como câmera, GPS e acelerômetro.

Claramente, o mercado de dispositivos móveis só tende a crescer, então, como suportar todas essas opções de dispositivos e, ao mesmo tempo, oferecer a melhor experiência de usuário possível? Neste artigo, mostrarei como criar um aplicativo móvel que tira proveito do melhor dos dois mundos, envolvendo um aplicativo Web móvel em um shell de aplicativo nativo.

O conceito de aplicativo híbrido

O conceito básico de aplicativo híbrido é envolver um aplicativo Web otimizado para dispositivos móveis em um shell de aplicativo nativo específico de dispositivo. O shell do aplicativo nativo hospeda um controle de navegador da Web que é configurado para iniciar a URL do aplicativo móvel específico quando o aplicativo do shell é iniciado. Outros elementos da interface do usuário podem ser fornecidos no shell do aplicativo nativo, mas somente o controle do navegador da Web é necessário. O controle do navegador da Web nativo monitora então as URLs que são solicitadas à medida que o usuário navega pelo site. Quando o usuário solicita uma URL específica que requer funcionalidade nativa, o controle do navegador da Web interrompe o evento de navegação e, em seu lugar, invoca a funcionalidade nativa. Assim que o usuário completa o processo nativo, o aplicativo navega o controle do navegador da Web de volta ao fluxo do site no local apropriado.

Para ilustrar como isso é feito, vou examinar minha experiência ao criar um aplicativo para um cliente, em conjunto com meus colegas da EffectiveUI. Criada para um funcionário de serviços externos que processa uma série de ordens de serviço de manutenção de bens públicos, como cartazes, bancos e hidrantes, o aplicativo tira proveito de recursos suportados pelo navegador para conseguir a localização exata do usuário e de acesso a hardware nativo para tirar fotografias e carregá-las no servidor. A Figura 1 mostra o menu principal do aplicativo completo.

The Completed Application Main Menu
Figura 1 O menu principal do aplicativo completo

Criando o aplicativo Web

Ao criar esse aplicativo móvel, eu segui uma série de sugestões do artigo de Steve Sanderson, “Crie uma experiência de navegação móvel melhor" (msdn.microsoft.com/magazine/hh288079) na edição de julho de 2011 da MSDN Magazine. Além das recomendações desse artigo, aprendi algumas coisas durante o processo:

  • Otimize elementos de interface do usuário, pois usuários que empregam muito o toque estão usando interação baseada em toque. A interação baseada em toque é inerentemente menos precisa que a interação baseada em mouse dos desktops. Todos os elementos interativos como botões e itens de menu precisam ser proporcionalmente maiores na interface móvel do que o são na experiência do desktop.
  • Otimize suas exibições para dispositivos móveis, pois dispositivos móveis que consomem muita largura de banda são limitados em termos de recursos, especialmente quando se considera a largura de banda. Não obrigue seu usuário a baixar uma grande quantidade de imagens para poder usar seu site. Usuários de dispositivos móveis esperam interfaces sensíveis e rapidamente abandonarão seu site ou aplicativo se elas não desempenharem de acordo com suas expectativas.
  • Use HTML5 e CSS3 porque navegadores da Web não têm a imensa herança dos navegadores de desktop, eles são muito mais rápidos na adoção de novos padrões de HTML5 e CSS3 do que seus equivalentes em desktops. Em muitos casos, navegadores para dispositivos móveis estão muito à frente dos navegadores para desktops em termos de implementação desses recursos. Tire proveito disso em suas exibições para dispositivos móveis para aliviar a carga que o navegador para dispositivos móveis precisa baixar e deixe que o navegador realize a maior parte do processamento técnico.

Uma das exigências técnicas de meu cliente ao criar esse aplicativo era demonstrar o compartilhamento da lógica do controlador entre as exibições do site para desktop e para dispositivos móveis. Essa exigência é comum a vários clientes e deve ser favorecida também pelos desenvolvedores, uma vez que ela simplifica muito o processo de criação de um aplicativo que suporte tanto usuários de desktops como de dispositivos móveis. O ASP.NET MVC 3 fornece a capacidade de se alternar exibições com base nos elementos solicitantes, como o navegador solicitante, ao mesmo tempo em que também compartilha controladores e modelos entre várias exibições. Ele permite ao desenvolvedor controlar de maneira detalhada a experiência no site para cada uma das diferentes plataformas, o que significa que o desenvolvedor só precisa criar a lógica de negócios e, em seguida, adaptar a apresentação a cada uma das plataformas. A Figura 2 mostra uma função de utilitário para decisão sobre a exibição a ser apresentada.

Figura 2 Utilitário para decisão sobre a exibição a ser apresentada

private ActionResult SelectView(string viewName, object model,
  string outputType = "html")
{
  if (outputType.ToLower() == "json")
  {
    return Json(model, JsonRequestBehavior.AllowGet);
  }
  else
  {
    #if MOBILE
      return View(viewName + "Mobile", model);
    #else
      if (Request.Browser.IsMobileDevice)
      {
        return View(viewName + "Mobile", model);
      }
      else
      {
        return View(viewName, model);
      }
    #endif
  }
}

A função de utilitário me permitiu atender à solicitação de compartilhamento do mesmo código para tomada de decisão sobre a exibição a ser apresentada ao usuário, com base na solicitação de entrada. Se a solicitação de entrada for um script solicitando JSON em vez de HTML, o controlador pode também responder adequadamente usando a mesma lógica de negócios e classes de modelo, simplesmente definindo corretamente o parâmetro outputType. Eu uso também uma instrução de precompilador procurando pelo símbolo de compilação condicional MOBILE para permitir a depuração de exibições em dispositivos móveis usando meus navegadores para desktop. Isso foi habilitado usando um destino de compilação adicional, o "Mobile", no projeto ASP.NET MVC 3, que me permitiu ignorar a verificação do Request.Browser.IsMobileDevice na configuração de depuração do desktop, aumentando bastante minha eficiência na criação e depuração da versão para dispositivos móveis do aplicativo.

Durante a criação do aplicativo, eu também usei páginas mestras diferentes para as versões do site para dispositivos móveis e para desktops. As versões das páginas mestras para desktops e para dispositivos móveis são significativamente diferentes, para abordar as disparidades nas apresentações entre as plataformas. A página mestra para dispositivos móveis inclui arquivos CSS específicos para dispositivos móveis e uma estrutura de layout simplificada para facilitar o desenvolvimento de exibições individuais usando marcação e sintaxe do jQuery Mobile Framework.

Todas as modernas plataformas para dispositivos móveis permitem acesso ao rádio de GPS de um dispositivo para determinar a localização atual do usuário através de APIs de localização geográfica do HTML5 do World Wide Web Consortium (W3C). O uso de APIs de localização geográfica foi discutido em detalhes no artigo de Brandon Satrom, “Integrando localização geográfica a aplicativos da Web” (msdn.microsoft.com/magazine/hh580735) na edição de dezembro de 2011. Embora esse artigo discuta o uso de um suporte retroativo de JavaScript em HTML5 para suportar a localização em navegadores que não suportam de forma nativa as APIs de localização geográfica em HTML5, a maioria dos navegadores para dispositivos móveis suporta de forma nativa as APIs de localização geográfica, de modo que a técnica de suporte retroativo não é necessária. Você deve considerar os dispositivos e navegadores direcionados ao avaliar a necessidade de usar técnica de suporte retroativo. Algo a se notar especificamente para o Android é que você deverá certificar-se de que o parâmetro enableHighAccuracy na chamada de localização geográfica seja definido como "verdadeiro" para que a funcionalidade de GPS seja acessada com sucesso no emulador do Android, como mostra a Figura 3.

Figura 3 Localização geográfica usando HTML5

if(navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function (position) {
$("#map_canvas").GoogleMap("addMarker", {
id: "device_location",
latitude: position.coords.latitude,
longitude: position.coords.longitude,
description: "Current Location",
iconImageUrl: '@Url.Content("~/Content/images/my-location-dot.png")',
callback: function () {
$("#map_canvas").GoogleMap("panToLocation", {
latitude: position.coords.latitude,
longitude: position.coords.longitude
});
}
});
}, function (error) {
}, {
enableHighAccuracy: true
});
}

Usando o jQuery Mobile

O jQuery Mobile Framework é “um sistema de interface do usuário unificado e baseado em HTML5 para todas as plataformas de dispositivos móveis", de acordo com o site de projeto (jquerymobile.com). Ele contém uma série de widgets otimizados para toque e facilita imensamente a tarefa de criar aplicativos da Web para dispositivos móveis que tenham a aparência de aplicativos nativos para dispositivos móveis. O jQuery Mobile pode ser adicionado ao seu projeto ASP.NET MVC 3 através do NuGet, usando a interface NuGet Package Manager ou a partir do Package Manager Console executando o comando “Install-Package jquery.mobile.” Isso acrescenta os arquivos jQuery Mobile JavaScript e CSS ao seu projeto. Você vai precisar, ainda, acrescentar referências aos arquivos jQuery Mobile JavaScript and CSS à sua página mestra para dispositivos móveis, como mostra a Figura 4.

Figura 4 A página mestra para dispositivos móveis

    <!DOCTYPE html>
    <html>
    <head>
      <title>@ViewBag.Title</title>
      <meta name="viewport" content="width=device-width,
        initial-scale=1.0, user-scalable=no, height=device-height" />
      <meta http-equiv="Content-type" content="text/html; charset=utf-8">
      <link href="@Url.Content("~/Content/eui_assets/css/reset.css")"
        rel="stylesheet" type="text/css" />
      <link href="@Url.Content("~/Content/jquery.mobile-1.0.min.css")"
        rel="stylesheet" type="text/css" />
      <link href="@Url.Content("~/Content/mobile.css")"
        rel="stylesheet" type="text/css" />
      <script src="@Url.Content("~/Scripts/jquery-1.7.1.min.js")"
        type="text/javascript"></script>
      @RenderSection("PreJQueryMobileInit", false)
      <script src="@Url.Content("~/Scripts/jquery.mobile-1.0.min.js")" 
        type="text/javascript"></script>
      <script type="text/javascript">
        $('a[data-ajax="false"]').live('click', function (event) {
          if (!$(this).hasClass("camera-link")) {
            $.mobile.showPageLoadingMsg();
          }
        });
      </script>
      @RenderSection("Head", false)
    </head>
    <body class="eui_body" id="@ViewBag.BodyID">
      @RenderBody()
    </body>
    </html>

O jQuery Mobile introduz modificações significativas nos padrões com os quais qualquer desenvolvedor em jQuery esteja familiarizado. Citando a documentação do jQuery Mobile:

A primeira coisa que se aprende com o jQuery é que se deve chamar o código na função $(document).ready() de modo que tudo seja executado assim que o DOM for carregado. Entretanto, no jQuery Mobile, [AJAX] é usado para carregar o conteúdo de cada página no DOM durante a navegação, e o manipulador de prontidão do DOM só é executado para a primeira página. Para executar código sempre que uma nova página for carregada, você pode vincular ao evento pageinit.

Eu usei o evento pageinit em todas as páginas do aplicativo que continham controle de Google Maps, de modo a inicializar o mapa quando a página for transferida para exibição via AJAX.

Um recurso adicional da página mestra para dispositivos móveis é a linha @RenderSection(“PreJQueryMobileInit”, false) line, mostrada na Figura 4, que permite executar scripts antes que o jQuery Mobile seja inicializado na página. No exemplo de aplicativo, eu usei esse recurso para vincular o evento mobileinit de modo a poder configurar um retorno de chamada personalizado quando o comportamento do filtro listview do jQuery Mobile for concluído. Também acrescentei duas linhas de código à biblioteca do jQuery Mobile para adicionar um método filterCompleteCallback ao protótipo listview para poder receber uma notificação quando a filtragem de lista interna for concluída. Isso me permitiu atualizar os itens correspondentes no mapa, de modo a corresponder com a lista filtrada. A função de retorno de chamada precisou ser adicionada ao listview do jQuery Mobile antes que o jQuery Mobile fosse aplicado a qualquer marcação; esse código é executado no manipulador de eventos mobileinit mostrado na Figura 5.

Figura 5 Vinculando o evento mobileinit Event

if(navigator.geolocation) {   
  navigator.geolocation.getCurrentPosition(function (position) {
    $("#map_canvas").GoogleMap("addMarker", {
      id: "device_location",
      latitude: position.coords.latitude,
      longitude: position.coords.longitude,
      description: "Current Location",
      iconImageUrl: '@Url.Content("~/Content/images/my-location-dot.png")',
      callback: function () {
        $("#map_canvas").GoogleMap("panToLocation", {
          latitude: position.coords.latitude,
          longitude: position.coords.longitude
        });
      }
    });
  }, function (error) {
  }, {
    enableHighAccuracy: true
  });
}
@section PreJQueryMobileInit {
  <script type="text/javascript">
    $(document).bind("mobileinit", function () {
      $.mobile.listview.prototype.options.filterCompleteCallback = function () {
        // Note that filtercompletecallback is a custom
        // addition to jQuery Mobile and would need to be updated
        // in future revisions.
        // See comments in jquery.mobile-1.0.js with SSC 09/12/2011
        var ids = [];
        var $visibleItems = $("#js-work-orders-list").find(
          "li:not(.ui-screen-hidden");
        for (var i = 0; i < $visibleItems.length; i++) {
          var item = $($visibleItems[i]).find("p");
          ids.push(item.text().substr(item.text().indexOf('#') + 1));
        }
        ids.push("device_location");
        $("#map_canvas").GoogleMap("hideAllMarkersExceptList", ids);
      }
    });
  </script>
}

O jQuery Mobile tira proveito significativo de novos recurso do HTML5 como as marcas de cabeçalho e rodapé e os atributos data-*. Os atributos data-role determinam o comportamento que deve ser anexado a um determinado elemento. Por exemplo, na exibição MapMobile.cshtml da Figura 6, eu tenho duas divs definidas com o atributo data-role=“page”.

Figura 6 Marcação MapMobile.cshtml

    <div data-role="page" id="map_page" data-fullscreen="true"
      data-url="map_page" data-theme="a">
      <header data-role="header" data-position="fixed">
        <a href="@Url.Action("Index", "Home")" data-icon="home"
          data-direction="reverse">Home</a>
        <h1>Map Demo</h1>
        <a href="#" data-icon="back" id="js-exit-street-view"
          class="ui-btn-hidden">Exit Street View</a>
      </header>
      <div data-role="content" class="main-content">
        <div id="map_canvas" style="width:100%;height:100%"></div>
      </div>
      <footer data-role="footer" data-position="fixed"
        data-id="fixed-nav" data-theme="a">
        <nav data-role="navbar">
          <ul>
            <li><a href="#map_page" class="ui-btn-active
              ui-state-persist">Map</a></li>
            <li><a href="#items_page">Work Orders</a></li>
          </ul>
        </nav>
      </footer>
    </div>
    <div data-role="page" id="items_page" data-url="items_page" data-theme="a">
      <header data-role="header" data-position="fixed">
        <a href="@Url.Action("Index", "Home")" data-icon="home"
          data-direction="reverse">Home</a>
        <h1>Map Demo</h1>
      </header>
      <div data-role="content" class="main-content">
        <div class="list-container">
          <ul data-role="listview" id="js-work-orders-list" data-filter="true">
          @foreach (MapItem item in Model.Items)
      {
          <li class="work-order-id-@item.ID">
            <a href="@Url.Action("Details", "Home", new { id = item.ID })"
              data-ajax="false">
              <h3>@item.Issue</h3>
              <p>Work Order #@item.ID</p>
            </a>
          </li>
        }
          </ul>
        </div>
      </div>
      <footer data-role="footer" data-position="fixed"
        data-id="fixed-nav" data-theme="a">
        <nav data-role="navbar">
          <ul>
            <li><a href="#map_page" data-direction="reverse">Map</a></li>
            <li><a href="#items_page" class="ui-btn-active
              ui-state-persist">Work Orders</a></li>
          </ul>
        </nav>
      </footer>
    </div>

Esse atributo informa ao jQuery Mobile que cada um desses divs deve ser tratado como uma página separada no dispositivo móvel e que a transição entre elas deve ser feita usando AJAX, sem que a navegação na página ocorra no navegador. Isso produz o efeito mostrado nas capturas de tela mostradas na Figura 7. O site do jQuery Mobile fornece recomendações e mais detalhes sobre como usar cada um dos atributos data-* no contexto do jQuery Mobile.

Transitioning Between Pages Via AJAX
Figura 7 Transição entre páginas via AJAX

Criando shells para aplicativos nativos e para dispositivos móveis

O padrão básico para desenvolvimento de cada um dos shells para aplicativos nativos é a criação de um aplicativo que simplesmente contenha um controle de navegador da Web para tela inteira. Nesse controle, eu capturo o evento que é disparado quando o usuário solicita uma nova página e compara a URL com uma lista de URLs conhecidas que deveriam invocar funcionalidade nativa. É aqui que o milagre de um aplicativo baseado na Web em um shell de aplicativo nativo acontece. Para as finalidades desse aplicativo, a URL que eu correspondi no site é "Home/Image", para invocar a funcionalidade de câmera nativa. Se o usuário estiver na página com detalhes da ordem de serviço, ele verá um ícone de câmera no canto superior direito da tela como mostra a Figure 8. O clique nesse ícone invoca a câmera nativa.

Invoking Native Camera Functionality
Figura 8 Invocando a funcionalidade de câmera nativa

Windows Phone

O Windows Phone usa o Silverlight para todas as funcionalidades nativas. De certo modo, isso torna o Windows Phone a plataforma mais fácil de ser suportada para o desenvolvedor da Web para dispositivos móveis que seja familiarizado com o ASP.NET. O layout XAML básico para o shell de aplicativo nativo é simples, como mostrado abaixo:

<Canvas x:Name="LayoutRoot" Background="Black" Margin="0">
  <phone:WebBrowser HorizontalAlignment="Left" Name="webBrowser1" 
    Navigating="webBrowser1_Navigating" IsScriptEnabled="True"
    IsGeolocationEnabled="True"
    Background="Black" Height="720" Width="480" />
</Canvas>

Os principais itens a serem observados aqui são que IsScriptEnabled seja definido como verdadeiro — porque, por padrão, o controle de navegador da Web no Windows Phone não habilita script — e que eu esteja manipulando o evento Navigating.

Em MainPage.xaml.cs, na Figura 9, eu manipulo o evento webBrowser1_Navigating event. Se a URL de navegação corresponder à URL que estou procurando, eu escolho a ID da ordem de serviço com a qual irei trabalhar e invoco CameraCaptureTask nativa, enquanto cancelo a navegação no navegador da Web. Depois que o usuário tirar a fotografia com a câmera, o método photoCaptureOr­SelectionCompleted será invocado. Aqui, eu carrego a imagem no servidor da Web usando a mesma ação POST de formulário HTTP que o site estaria usando se submetesse um formulário que contivesse um botão de entrada para carregamento de arquivo. Quando o carregamento da foto for concluído, upload_FormUploadCompleted será invocado, retornando o usuário ao fluxo do aplicativo Web.

Figura 9 MainPage.xaml.cs do Windows Phone

public partial class MainPage : PhoneApplicationPage
{
  CameraCaptureTask cameraCaptureTask;
  BitmapImage bmp;
  string id = "";
  string baseURL = "http://...";
  // Constructor
  public MainPage()
  {
    InitializeComponent();
    cameraCaptureTask = new CameraCaptureTask();
    cameraCaptureTask.Completed +=
      new EventHandler<PhotoResult>(photoCaptureOrSelectionCompleted);
  }
  private void webBrowser1_Navigating(object sender, NavigatingEventArgs e)
  {
    // Catch Navigation and launch local camera
    if (e.Uri.AbsoluteUri.ToLower().Contains("home/image"))
    {
      id = e.Uri.AbsoluteUri.Substring(e.Uri.AbsoluteUri.LastIndexOf("/") + 1);
      cameraCaptureTask.Show();
      e.Cancel = true;
    }
  }
  void photoCaptureOrSelectionCompleted(object sender, PhotoResult e)
  {
    if (e.TaskResult == TaskResult.OK)
    {
      byte[] data = new byte[e.ChosenPhoto.Length];
      e.ChosenPhoto.Read(data, 0, data.Length);
      e.ChosenPhoto.Close();
      Guid fileId = Guid.NewGuid();
      Dictionary<string, object> postParameters = new Dictionary<string, object>();
      postParameters.Add("photo", new FormUpload.FileParameter(
        data, fileId.ToString() +
        ".jpg", "image/jpeg"));
      FormUpload upload =
        new FormUpload(baseURL + "Home/UploadPicture/" + id, postParameters);
      upload.FormUploadCompleted +=
        new FormUpload.FormUploadCompletedHandler(upload_FormUploadCompleted);
      upload.BeginMultipartFormDataPost();
    }
  }
  void upload_FormUploadCompleted(object source)
  {
    webBrowser1.Navigate(webBrowser1.Source);
  }
  private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)
  {
    webBrowser1.Navigate(new Uri(baseURL));
  }
}

O Windows Phone apresenta alguns comportamentos diferentes quando interage com a versão baseada na Web dos controles do Google Maps ou do Bing Maps, quando comparado ao Android ou ao iOS. Devido à maneira como o navegador para dispositivos móveis Internet Explorer 9 captura gestos de tocar, arrastar e apertar sem passá-los pelo mecanismo JavaScript, mapas baseados na Web não podem ser ampliados ou exibidos no modo panorâmico com gestos e precisam usar os controles de ampliação ou de panorama fornecidos pelo mapa. Dada essa limitação, uma melhoria futura a esse projeto seria invocar o controle do Bing Maps nativo no Windows Phone quando a funcionalidade de interatividade com o mapa for necessária e, em seguida, retornar ao aplicativo da Web em telas que não necessitassem da funcionalidade de interatividade com o mapa.

Android

O código Java para Android foi escrito por meu colega, Sean Christmann, e é semelhante ao código para o Windows Phone. O seguinte layout em XML define um layout de tela inteira para o controle WebView do Android:

<?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"
    >
    <WebView android:id="@+id/webView"
      android:layout_width="match_parent" 
      android:layout_height="match_parent"></WebView>
</LinearLayout>

Em EffectiveUIActivity.java, mostrado na Figura 10, a substituição de onCreate configura o WebViewClient para substituir os métodos onLoadResource e shouldOverrideUrlLoading do controle WebView para buscar a mesma cadeia de caracteres correspondente à usada no Windows Phone e, caso ela seja encontrada, criar uma atividade de câmera e cancelar a navegação. O código também substitui onGeolocationPermissionsShowPrompt para suprimir o prompt ao usuário que ocorreria cada vez que o aplicativo fosse executado para permitir ao controle WebView acesso à localização GPS. Depois que atividade de câmera é executada, a função onActivityResult faz postagem da fotografia no servidor da Web usando o mesmo método do exemplo anterior do Windows Phone e, em seguida, retorna o usuário ao fluxo do aplicativo da Web.

Figura 10 EffectiveUIActivity.java do Android

    public class EffectiveUIActivity extends Activity {
      /** Called when the activity is first created. */
      WebView webView;
      String cameraId;
      static String baseURL = "http://...";
      @Override
      public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        webView = (WebView)findViewById(R.id.webView);
        webView.getSettings().setJavaScriptEnabled(true);
        webView.getSettings().setGeolocationEnabled(true);
        webView.setVerticalScrollbarOverlay(true);
        webView.loadUrl(baseURL);
        final EffectiveUIActivity activity = this;
        webView.setWebViewClient(new WebViewClient(){
          @Override
          public void onLoadResource(WebView view, String url) {
            super.onLoadResource(view, url);
            if(url.contains("Home/Image")){
              activity.createCamera();
            }
          }
          @Override
          public boolean shouldOverrideUrlLoading(WebView view, String url){
            String match = "Home/Image/";
            int i = url.indexOf(match);
            if(i>0){
              cameraId = url.substring(i+match.length());
              activity.createCamera();
              return true;
            }
            return false;
          }
        });
        webView.setWebChromeClient(new WebChromeClient(){
          @Override
          public void onGeolocationPermissionsShowPrompt(
            String origin, GeolocationPermissions.Callback callback) {
            super.onGeolocationPermissionsShowPrompt(origin, callback);
            callback.invoke(origin, true, false);
          }
        });       
      }
      public void createCamera(){
        Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
        startActivityForResult(intent, 2000);
      }
      @Override
        public void onActivityResult(int requestCode, int resultCode, Intent data) {
          if (resultCode == Activity.RESULT_OK && requestCode == 2000) {
            Bitmap thumbnail = (Bitmap) data.getExtras().get("data");
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            thumbnail.compress(CompressFormat.JPEG, 75, bos);
            byte[] imagebytes = bos.toByteArray();
            ByteArrayBody bab = new ByteArrayBody(imagebytes, "image/jpeg",
              UUID.nameUUIDFromBytes(imagebytes).toString()+".jpg");
            HttpClient client = new DefaultHttpClient();
            HttpPost post = new HttpPost(baseURL+"Home/UploadPicture");
            MultipartEntity reqEntity =
              new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE);
            reqEntity.addPart("photo", bab);
            try {
              reqEntity.addPart("ID", new StringBody(cameraId, "text/plain",
                Charset.forName( "UTF-8" )));
            } catch (UnsupportedEncodingException e1) {
                // TODO Auto-generated catch block
                e1.printStackTrace();
              }
              post.setEntity(reqEntity);
              try {
                HttpResponse response = client.execute(post);
                BufferedReader reader = new BufferedReader(
                  new InputStreamReader(
                  response.getEntity().getContent(), "UTF-8"));
                String sResponse;
                StringBuilder s = new StringBuilder();
                while ((sResponse = reader.readLine()) != null) {
                  s = s.append(sResponse);
                }
              } catch (ClientProtocolException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
              } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
                }
                webView.loadUrl(webView.getUrl());
              }
            }
    }

iOS

O código Objective-C para iOS foi também escrito por meu colega, Sean Christmann, e é também semelhante àquele usado para o Windows Phone e para o Android. Em WebCameraViewController.m mostrado na Figura 11, o controle UIWebView executa o método shouldStartLoadWithRequest para realizar a correspondência de padrão na URL solicitada. Se a cadeia de caracteres da URL corresponde, o código retorna “NO” para cancelar a navegação e invoca o UIImagePickerController nativo. Isso permite ao usuário escolher uma imagem da biblioteca de fotografias ou tirar uma nova fotografia com a câmera integrada. Após selecionar a fotografia, o código faz então a postagem da fotografia de volta ao servidor da Web usando a biblioteca ASIFormDataRequest (allseeing-i.com/ASIHTTPRequest) antes de retornar UIWebView ao fluxo normal do aplicativo.

Figura 11 Código iOS

- (void) choosefromCamera {
    UIImagePickerController *picker = [[UIImagePickerController alloc] init];
    picker.delegate = self;
    picker.mediaTypes = [NSArray arrayWithObjects:(NSString*)kUTTypeImage, nil];
    if ([UIImagePickerController
      isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
      picker.sourceType = UIImagePickerControllerSourceTypeCamera;
    }else{
      picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
    }
    [self presentModalViewController:picker animated:YES];
}
- (void)imagePickerController:(UIImagePickerController *)picker   
    didFinishPickingMediaWithInfo:(NSDictionary *)info {
    UIImage *image = [info objectForKey:UIImagePickerControllerOriginalImage];
    NSData *jpg = UIImageJPEGRepresentation(image, 0.3);
    [picker dismissModalViewControllerAnimated:YES];
    [picker release];
    NSString *url =
      [NSString stringWithFormat:@"%@:7511/WorkOrders/UploadPicture", baseURL];
    ASIFormDataRequest *request =
      [ASIFormDataRequest requestWithURL:[NSURL URLWithString:url]];
    [request addData:jpg withFileName:[
      NSString stringWithFormat:@"%@.jpg", [self GetUUID]]
      andContentType:@"image/jpeg" forKey:@"photo"];
    [request addPostValue:cameraId forKey:@"ID"];
    [request setDelegate:self];
    [request setDidFinishSelector:@selector(imageUploaded:)];
    [request setDidFailSelector:@selector(imageUploaded:)];
    [request startSynchronous];
    [webView reload];
}
-(void) imageUploaded:(ASIFormDataRequest *)request {
    NSString *response = [request responseString];
    NSLog(@"%@",response);
}
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(
  NSURLRequest *)request
    navigationType:(UIWebViewNavigationType)navigationType {
    NSURL *url = [request URL];
    NSString *str = [url absoluteString];
    NSRange range = [str rangeOfString:@"WorkOrders/Image/"];
    if (range.location != NSNotFound) {
      cameraId = [str substringFromIndex:range.location+17];
      [cameraId retain];
      NSLog(@"%@", cameraId);
      [self choosefromCamera];       return NO;
    }else{
      return YES;
    }
}

Redução gradual da experiência móvel

Então, o que acontece se o usuário do site em dispositivo móvel não estiver usando o shell do aplicativo nativo para acessar a câmera? Nesse cenário, é importante que haja uma redução gradual da experiência do usuário. Redução gradual é o conceito de criar seu aplicativo de modo que ele continue a funcionar corretamente mesmo quando visualizado com um software aquém do ideal. Isso não significa que cada recurso funcione exatamente da mesma maneira, ou que tenha uma aparência semelhante à experiência pretendida, mas que ele vise a garantir que todos os objetivos básicos do usuário possam ainda ser atingidos, mesmo se o usuário não estiver tendo a melhor das experiências.

Para habilitar redução gradual nesse aplicativo, eu criei um controlador e exibição em ASP.NET MVC 3 para a URL de captura de imagem, “Home/Image”, que está sendo capturada pelos shells do aplicativo nativo para fornecer um formulário de carregamento de arquivo simples, como mostrado na Figura 12. Esse formulário permite que usuários que não estejam usando os shells avançados para dispositivos móveis realizem a mesma tarefa de acrescentar uma fotografia a uma ordem de serviço, embora eles não estejam tendo a experiência integrada. O formulário faz a postagem à mesma ação controladora usada pelos shells de aplicativo nativo, encorajando a reutilização do código por toda a gama de diferentes plataformas e exibições.

A Simple File Upload Form for Graceful Degradation
Figura 12 Um formulário simples para carregamento de arquivos, para redução gradual

Vantagens significativas em custos

A abordagem de aplicativo híbrido pode oferecer significativas vantagens em custos quando comparada com aplicativos nativos exclusivos, tanto no curto como no longo prazo. Ferramentas como o jQuery Mobile diminuem as diferenças em usabilidade, o que pode levar a significativas vantagens de negócios quando o acesso a dispositivos nativos não for necessário.

Com a intensa proliferação de dispositivos móveis, você terá poucas escolhas a fazer quando estiver procurando por um aplicativo para dispositivos móveis:

  • Criar um aplicativo nativo para cada plataforma que deseja suportar Isso tem a vantagem distinta de oferecer a melhor experiência de usuário e desempenho para cada plataforma, ao mesmo tempo em que acessa todos os recursos nativos do dispositivo e a força de marketing das lojas online de aplicativos. No entanto, a desvantagem é que isso pode ser significativamente mais caro de criar e manter, porque irá necessitar uma base de código diferente para cada plataforma que você deseje suportar. À cada adição, cada nova versão do aplicativo requer que ele seja submetido novamente às lojas online de aplicativos.
  • Criar um aplicativo da Web Isso tem a vantagem de ser a opção mais simples e barata de se desenvolver, lançar e atualizar para todas as plataformas, mas a experiência do usuário pode ser comprometida pela falta de acesso a recurso de hardware nativos. A falta de acesso às lojas online de aplicativos pode também comprometer a adoção de seu aplicativo, fazendo com que todo o esforço de marketing recaia sobre você.
  • Criar um aplicativo híbrido nativo e para dispositivos móveis Essa é a abordagem que eu apresentei, que oferece uma ótima opção entre os altos custos inerentes ao desenvolvimento de um aplicativo exclusivo para dispositivos móveis para cada plataforma e a falta de acesso ao hardware nativo de um aplicativo da Web para dispositivos móveis. Essa opção fornece também acesso às lojas online de aplicativos, aumentando o alcance de seu aplicativo.

É importante notar que nenhuma dessas abordagens é inerentemente melhor que as outras — cada uma delas têm seus próprios pontos fortes e fracos. Uma análise abrangente de custo/benefício de cada uma das opções ajudará na determinação do caminho correto para seus usuários e seus negócios. Ao tomar essa decisão, é importante considerar a experiência do usuário, os custos iniciais de desenvolvimento e os custos de manutenção permanente, assim como fatores mais sutis como marketing e adoção pelo usuário.

Para vários cenários de negócios, eu defendo a inclusão de um aplicativo Web móvel ou híbrido, uma vez que o esforço adicional para se criar aplicativos para cada plataforma para dispositivos móveis poderia vir a ser maior que os benefícios aos negócios. Os cenários de negócios precisam ser cuidadosamente avaliados dentro dos limites da implantação de um aplicativo Web móvel ou híbrido.

Aplicativos móveis chegaram para ficar e crescem de importância à medida que a computação se afasta da tradicional experiência em desktops para um conjunto de experiências em dispositivos móveis. Ao considerar a criação de aplicativos para o universo da mobilidade, lembre-se que fazer concessões nem sempre é um pecado e que isso pode resultar no melhor dos mundos.

Shane Church é chefe técnico na EffectiveUI em Denver, Colorado. Ele vem desenvolvendo com o Microsoft .NET Framework com foco no ASP.NET e em tecnologias para dispositivos móveis da Microsoft desde 2002. Seu blog se encontra em s-church.net. Obtenha mais informações sobre a EffectiveUI em effectiveui.com.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Dr. James McCaffrey