Android

하이브리드앱 개발 on Visual Studio #1

페이스북에서 아래와 같은 소식이 잠시 화제가 되었다.

image

한 장의 스크린샷과 함께, 안드로이드SDK/크롬SDK/Git/SQL를 지원할 예정이라는 내용이었다. 잘못 알려진 부분이 있어서 좀 더 자세히 다뤄보고자 한다.

일단 마이크로소프트는 “비주얼 스튜디오에서 안드로이드SDK, 크롬SDK, Git, SQL 지원 예정”이라는 발표를 한 적이 없다. 대신 최근에 Apache Cordova와 함께 하이브리드 앱 개발을 지원하겠다고 발표하였다. 현재 이 기능은 프리뷰 버전으로 제공되고 있다.

하이브리드앱은 앱 내에서 웹 컨텐츠를 처리하기 위해서 간단한 로컬 웹서버/웹뷰를 필요로 한다. 또한, 웹 컨텐츠에서 디바이스 기능을 호출할 수 있는 API와 각 플랫폼 별로 앱 패키징해주는 기능이 필요하다. 이러한 기능을 하는 것이 Apache Cordova이다. 원래는 PhoneGap이라는 이름으로 유명하지만, Adobe에서 이를 오픈소스화하여 코드 베이스를 Apache로 넘겨서 Apache Cordova가 되었다고 한다. 두 프로젝트는 현재 이름만 다른 패키지 배포판이다. (참고)

그렇다면, VS에서 하이브리드 앱을 지원하기 위한 기술 요소들을 한번 생각해보자. 먼저, Android나 iOS 개발도구가 필요하다. 각 앱을 빌드하고 에뮬레이터(Android의 경우)를 띄워서 앱을 실행할 수 있어야 하기 때문이다. Android 앱을 빌드하려면 Java도 필요하고, Webkit 기반의 웹뷰를 에뮬레이팅하기 위해 Chrome이 필요할 것으로 보인다. SQLite는 WebSQL를 대신해서 로컬 스토리지 용도로 쓰일 수 있다. 그리고, iOS 앱을 배포하기 위해서 iTunes가 필요할 것으로 보인다.

실제로, Multi-Device Hybrid Apps과 함께 제공되는 문서에 보면 이러한 내용이 명시되어 있다.

The installer will then ask permission to download certain dependencies. These are mostly open source software pre-requisites required by individual platforms or Apache Cordova to build and run your applications. Any dependencies that already exist on your system will not be re-installed (as long as the required version is present).

하이브리드 앱을 빌드하기 위해 필요한, 개별 플랫폼이나 Apache Cordova에 필요한 오픈소스 구성요소들을 설치한다고 되어있다. 좀 더 자세하고 정확한 설명은 다음과 같다.

Third Party Dependencies 

  • Joyent Node.js – Enables Visual Studio to integrate with the Apache Cordova Command Line Interface (CLI) and Apache Ripple™ Emulator
  • Git CLI – Required only if you need to manually add git URIs for plugins
  • Google Chrome – Required to run the Apache Ripple emulator for iOS and Android
  • Apache Ant 1.8.0+ – Required as a dependency for the Android build process
  • Oracle Java JDK 7 – Required as a dependency for the Android build process
  • Android SDK – Required as a dependency for the Android build process and Ripple
  • SQLLite for Windows Runtime – required to add SQL connectivity to Windows apps (for the WebSQL Polyfill plugin)
  • Apple iTunes – Required for deploying an app to an iOS device connected to your Windows PC

앞서 페이스북의 소식에 지원 예정이라고 되어 있었던, GitSQLite의 경우는 이전 Visual Studio 2012에서 부터 사용할 수 있었다.

이왕 살펴보는 겸, VS에서 하이브리드 앱을 어떻게 만드는지 좀 더 살펴보자.

image

VS 2013 Update 2부터는 TypeScript가 정식으로 지원되어, 위 화면과 같이 하이브리드 앱을 만들 때에도 JavaScript와 TypeScript 프로젝트 템플릿이 제공된다.

프로젝트 구성은 다음과 같다.(TypeScript의 경우)

image

여기서 살펴볼 만한 부분은 res, typeings, merges 정도이다.

res는 플랫폼 고유 리소스들이 들어 있는 폴더이다. 서명, 아이콘, 스플래시 스크린 등이 들어간다. 그리고, merges는 CSS 파일과 같은 플랫폼 고유의 코드를 추가하는데 사용한다.

기본 문서에는 JS로 되어 있으나, 호기심에 TypeScript(이하 TS)로 해보았다. JS 프로젝트와의 차이는, scripts 폴더에 index.js 대신 index.ts 파일이 있으며, 추가로 typings 폴더가 있다는 점이다. JS 프로젝트에는 typings 폴더가 없다. typings 폴더 밑에는 Cordova를 구성하는 각 클래스 별로 Type이 선언되어 있는 *.d.ts 파일이 있다.

image

다른 JavaScript 라이브러리의 TypeScript에서 사용하기 위해서 사용하는 파일들이다. Cordova의 API를 사용할 때 Type 체크나 IntelliSense 등에 사용된다. 자세한 내용은 이 글의 Ambient Declaration 부분이나 TypeScriptLang.org의 내용을 참고하자.

일단 비어있는 템플릿 앱을 실행해 보았다.

image

실행하는 옵션에 Android 에뮬레이터와 디바이스가 보인다. 그런데 실행하자 다음과 같은 에러가 나온다.

image

해당 에러를 검색해보면 스택오버플로우에 답변이 있다. 환경 변수와 PATH 설정이 잘 못 되어 있어서 그런 듯 하다. 가이드 대로 환경 변수와 PATH 설정을 변경하고 재시작하면, Ripple 에뮬레이터에서 실행하는데에는 더 이상 문제가 발생하지 않는다.

흥미로운 점은, Ripple이라는 하이브리드 앱의 에뮬레이터이다.

image

하이브리드 앱의 에뮬레이터가 Chrome 브라우저에서 실행되고 있다. 앞서 설치 과정에서 Chrome을 왜 설치하는지 알 수 있었다.

마지막으로 Device/Emulator에서 앱을 실행해보려고 했으나 위와 유사한 또 다른 에러가 발생했다. 내 경우에는 Android ADK 가 2곳에 설치가 되어 있었는데 예전에 설치했던 ADK의 버전이 낮아서 발생하는 문제인 것 같았다. Android SDK Manager에서 API 레벨을 19이상으로 업데이트해서 문제를 해결하고 다음과 같이 디바이스에서의 실행에 성공하였다.

_978

여기까지 Multi-Device Hybrid Apps 공식 가이드 문서를 참고로 하여 기본적인 하이브리드 앱 개발환경을 살펴보았다. 사실 개인적으로는 WinJS 라이브러리를 타 플랫폼에서 어떻게 사용할 수 있는지가 궁금한데, 이 부분은 다음 편에서 WinJS 앱 샘플을 통해 살펴보도록 하겠다.

Advertisements

Xamarin 앱 개발 테스트 #6

지난 #5에서 PCL과 Shared 프로젝트를 통해서 C# 코드를 공유하는 방법을 알아보았다.

이번에는 UI 구현을 통해서 Windows Phone과 유사하게 동작하는 Xamarin.Android 앱을 만들어 보고자 한다. Android 앱 개발을 잘 모르다 보니, Android UI 모델과 Xamarin.Android를 동시에 공부해야 해서 조금 어려운 부분은 있었지만 결과적으로 포팅은 큰 어려움 없이 성공적으로 진행되었다.

다음은 Xamarin.Android의 MainActivity.cs 파일이다.

    public class MainActivity : Activity
    {
        int count = 1;

        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);
            
            // Set our view from the "main" layout resource
            SetContentView(Resource.Layout.Main);

            string searchTerm = "flowers";

            SearchFlickr(searchTerm);
        }

        async void SearchFlickr(string searchTerm)
        {
            List<FlickrPhotoResult> results = await FlickrSearcher.SearchAsync(searchTerm);
            var gridview = FindViewById<GridView>(Resource.Id.gridview);
            gridview.Adapter = new ImageAdapter(this, results);
        }
    }

Main.axml 파일에 GridView를 하나 만들어 놓고, 여기에 Adapter를 이용해서 FlickrSearcher에서 받은 List 데이터를 넘겨주고 있다. 같은 부분의 Windows Phone 코드를 살펴보면 다음과 같다.

        async void SearchFlickr(string searchTerm)
        {
            List<FlickrPhotoResult> results = await FlickrSearcher.SearchAsync(searchTerm);

            this.DataContext = results;

            ...
        }

Windows Phone에서는 XAML 페이지의 DataContext로 지정하고, GridView의 ItemsSource에 데이터 바인딩을 통해서 XAML의 각 오브젝트에서 처리를 하게 된다.

<GridView x:Name="gridView" ItemsSource="{Binding}" Grid.Row="2" Margin="10,0,0,0">
            <GridView.ItemTemplate>
                <DataTemplate>
                    <Grid HorizontalAlignment="Left" Width="460" Height="300">
                        <Border Background="{ThemeResource ListViewItemPlaceholderBackgroundThemeBrush}">
                            <Image Source="{Binding ImageUrl}" Stretch="UniformToFill" AutomationProperties.Name="{Binding Title}"/>
                        </Border>
                        <StackPanel VerticalAlignment="Bottom" Background="{ThemeResource ListViewItemOverlayBackgroundThemeBrush}">
                            <TextBlock Text="{Binding Title}" Foreground="{ThemeResource ListViewItemOverlayForegroundThemeBrush}" Style="{StaticResource TitleTextBlockStyle}" Height="60" Margin="15,0,15,0"/>
                        </StackPanel>
                    </Grid>
                </DataTemplate>
            </GridView.ItemTemplate>
        </GridView>

처음에는 이 개념이 이해하기 어려운데, 막상 데이터 바인딩 없이 Android UI 개발을 하려고 하니 머리가 더 복잡해지는 느낌이다.

어쨌든 Android에서는 데이터 바인딩 대신 Adapter를 만들어서 UI에 데이터를 뿌려 주어야 한다. Adapter는 BaseAdapter를 상속하여 클래스로 직접 구현해야 하는데, 아래와 같이 구현해 보았다.

using Android.App;
using Android.Content;
using Android.Graphics;
using Android.Views;
using Android.Widget;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;

namespace FlickrSearch.Android
{
    class ImageAdapter : BaseAdapter
    {
        Context context;
        List<FlickrPhotoResult> list;
        public ImageAdapter(Context c, List<FlickrPhotoResult> data)
        {
            context = c;
            list = data;
        }

        public override int Count
        {
            get { return list.Count; }
        }

        public override Java.Lang.Object GetItem(int position)
        {
            return null;
        }

        public override long GetItemId(int position)
        {
            return position;
        }

        public override global::Android.Views.View GetView(int position, global::Android.Views.View convertView, global::Android.Views.ViewGroup parent)
        {
            /*
            // 직접 ImageView를 생성해서 전달하는 경우 
            ImageView imageView;
            var item = list[position];

            if (convertView == null)
            {
                imageView = new ImageView(context);
                imageView.LayoutParameters = new AbsListView.LayoutParams(1200,900);
                imageView.SetScaleType(ImageView.ScaleType.CenterCrop);
                //imageView.SetPadding(8,8,8,8);
            }
            else
            {
                imageView = (ImageView)convertView;
            }
            Bitmap bitmap = GetImageBitmapFromUrl(item.ImageUrl);
            imageView.SetImageBitmap(bitmap);
            return imageView;
             */

            // GridViewItem.axml 파일을 이용해서 값만 전달하는 경우
            View row = convertView;
            RecordHolder holder = null;

            if (row == null)
            {
                LayoutInflater inflater = ((Activity)context).LayoutInflater;
                row = inflater.Inflate(Resource.Layout.GridViewItem, parent, false);

                holder = new RecordHolder();
                holder.txtTitle = row.FindViewById<TextView>(Resource.Id.item_text);
                holder.imageItem = row.FindViewById<ImageView>(Resource.Id.item_image);
                row.Tag = holder;
            }
            else
            {
                holder = row.Tag as RecordHolder;
            }
            holder.txtTitle.Text = list[position].Title;
            Bitmap bitmap = GetImageBitmapFromUrl(list[position].ImageUrl);
            holder.imageItem.SetImageBitmap(bitmap);
            return row;
        }

        private Bitmap GetImageBitmapFromUrl(string p)
        {
            Bitmap imageBitmap = null;
            using (var wc = new WebClient())
            {
                var imageBytes = wc.DownloadData(p);
                if (imageBytes != null && imageBytes.Length > 0)
                {
                    imageBitmap = BitmapFactory.DecodeByteArray(imageBytes, 0, imageBytes.Length);
                }
            }
            return imageBitmap;
        }
    }

    public class RecordHolder : Java.Lang.Object
    {
        public TextView txtTitle;
        public ImageView imageItem;
    }
}

추상 클래스인 BaseAdapter를 상속하여 구현하는데, VS에서 자동으로 생성해주는 코드에 Count, GetItem, GetItemId, GetView 등 일부 메소드의 구현부만 추가하면 된다. GetView에서 2가지 방식으로 구현을 해보았는데, 주석처리된 위 쪽은 직접 ImageView를 생성하여 이미지를 뿌려주는 것이고, 아래 쪽은 별도의 axml 파일을 생성해서 이를 템플릿처럼 사용하는 것이다. 해당 코드들은 Android와 Xamarin의 문서 및 포럼 글들을 참고로 하였다. 별도로 생성하는 GridViewItem.axml의 내용은 다음과 같다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">
  <ImageView 
    android:id="@+id/item_image" 
    android:layout_width="480dp" 
    android:layout_height="360dp" 
    android:layout_marginRight="0dp" > 
  </ImageView> 
  <TextView 
    android:id="@+id/item_text" 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" 
    android:layout_marginTop="0dp" 
    android:textSize="30sp" > 
  </TextView>
</LinearLayout>

Android UI 레퍼런스와 샘플 코드를 찾아보느라 조금 시간이 걸리기는 했지만, 어렵지 않게 구성할 수 있었고 전체적으로 생각보다 매끄럽게 포팅이 진행되었다. 그러나, 에뮬레이터 자체가 느리기도 하고, 코드를 최적화하지 않아서 동작이 버벅거렸다. 특히, 이미지 스크롤 할 때 끊김이 심했는데, 데이터 로딩 코드를 최적화 해야 할 듯 싶다.(Windows Phone에서는 URL만 바인딩 시키면 지가 알아서 퍼포먼스 지장 없도록 로드해와서 뿌려 주는데… 내가 Android를 잘 몰라서 그렇겠지…)

xamarin

Windows Phone(좌), Xamarin.Android(우)

UI가 아주 예쁘게 나오진 않았지만 결과적으로 같은 기능을 가진 앱을 포팅할 수 있었다.

이번 Xamarin 앱 개발 테스트를 통해서 Xamarin으로 크로스플랫폼 앱 개발이 충분히 가능한 시나리오이며, 실제 구현하기 위해서 알아야 하는 기본적인 개념과 테크닉들을 알아볼 수 있었다. 아직 국내에 Xamarin이 잘 알려지지 않았고, 실제로 이 도구를 이용해서 앱을 개발한 사례도 극히 드문 것으로 알고 있다. 국내에서 최근에 유행하고 있는 Unity도 같은 오픈소스 프레임워크를 사용하고 있는데, 이 글을 통해서 더 많은 개발자들이 C#의 다양한 활용 가능성에 대해서 알릴 수 있으면 좋겠다.

계속해서 기회가 된다면 디바이스에 올려서 퍼포먼스 테스트를 해보거나, Xamarin.iOS 쪽도 살펴볼까 싶다.

사용한 소스 코드는 여기서 다운로드할 수 있다.