poniedziałek, 18 lipca 2016

Xamarin.Android kontra Java odc.1 (narzędzia, składnia, RecyclerView, SearchView, MediaStore, bonus: stylowanie themy)

Po pewnym zluzowaniu przyszła pora, by coś zacząć zrobić z dalszego roadmap przedstawionego na podsumowaniu DSP. Na obecny moment najbardziej ciągnie mnie do Xamarina. W oparciu o pewną dozę praktyki chciałbym sobie wyrobić własne zdanie o tej platformie. Jako że jestem na świeżo po pisaniu w natywnych narzędziach app-ek na Android i iOS na DSP, a jednocześnie jestem .NET-owcem od wielu lat, więc mam dobre podstawy, by zabrać się za takie porównywanie.

 

Chcę mieć jak największą elastyczność, być jak najbliżej natywnego API, nie będę wchodził na wyższy poziom abstrakcji Xamarin.Forms. W mojej głowie powstała wizja, by w Xamarin.Android i Xamarin.iOS  powstały “zielone ‘limitowane’ edycje”  moich wcześniejszych app-ek z DSP Winking smile. W związku z tym na github w moim repozytorium powstał nowy folder Xamarin.

Prace rozpocząłem tradycyjnie od Androida. Na obecny moment przeniosłem z Javy listę plików muzycznych z wyszukiwaniem, którą zasiliłem tymczasowym kodem pobierającym pliki audio z MediaStore (docelowo trafi do serwisu jak w Javie).

image

Oto pierwszy screenshot z uruchomionej na telefonie “zielonej” aplikacji sygnowanej - dla odróżnienia od oryginału w Javie - nazwą “Light Organ X” (ang.)/ ”Kolorofon X” (pl.):

Screenshot_20160718-231923

Teraz pora podzielić się uwagami i wrażeniami.

 

Narzędzia

Szablony - jak dla mnie w Visual Studio jest mało szablonów, jeśli porównać to z Android Studio. Nie ma w sumie nic poza pustym projektem czy aktywnością. Owszem dużo się pisze ręcznie, na podstawie przykładów czy dokumentacji, ale jakby np. aktywność ustawień dało się wygenerować, czy ekran z flow button na pewno byłby większy power. Nie patrzyłem tutaj na Xamarin Studio.

Komponenty - w projekcie Xamarin jest folder Components. Jak użyjemy opcję “Get more components” z menu kontekstowego, to możemy sobie dodać takie odpowiedniki pakietów nugetowych. Wcześniej jednak trzeba sobie założyć konto Xamarin i się na nie zalogować. Dotyczy to także darmowej wersji Community, z której korzystam. Może kiedyś zrobią tak, że wystarczy być zalogowanym w Visual Studio na konto MS…  Instalowałem sobie “kompat library” v4 i v7 i design library. Wcześniej zanim wszedłem w komponenty i założyłem sobie konto Xamarin, użyłem pakietów nugetowych od Xamarin. W jednym i drugim przypadku nie zauważyłem różnicy w działaniu aplikacji czy narzędzi (na git trafił zdaje sie wariant używający nugeta)

Designer - pliki .xml wzięte wprost z Android Studio nie uruchomią designu, trzeba zmienić im rozszerzenia na .axml. Duży minus za brak wsparcia dla kontrolek z dodatkowych bibliotek w obecnej wersji Xamarin 4.1.1.3 i VS 2015 Update 3.

image

W efekcie, gdy używam elementów stosowanych przeze mnie wcześniej w Android Studio, korzystam z edytora XML. Dodam jednak, że w Android Studio też nie używałem dużo designera, mimo że pokazywał więcej. Trzeba wspomnieć, że w następnej wersji Xamarin 4.1.2 (alfa) obsługa niestandardowych kontrolek jest zapowiedziana. Zainstalowałem nawet na chwilę tę wersję, ale designer nadal nie wizualizował mi tych samych kontrolek, choć było widać pewne różnice w jego zachowaniu. Ponieważ projekt na eksperymentalnej wersji Xamarin przestał mi się kompilować, wróciłem do stabilnego wydania (musiałem jeszcze po tym wyczyścić repozytorium MEF dla Visual Studio, bo designer stabilnej wersji przestał po tym działać). Z kolei Xamarin w Visual Studio 15 Preview 3 póki co nie sprawia wrażenia, by przynosił nowości widoczne gołym okiem. Na koniec dodam, że przy stylowaniu posiłkowałem się przez chwilę designerem themy w Visual Studio. To akurat okazało się dość pomocne, choć nie wykorzystałem tego wprost, ale zobaczyłem co się generuje.

image

 

Różnice w deklaracjach i składni

Nie musze wszystkiego w manifeście robić w manifeście.

Nad aktywnością (np. klasą listy plików) piszę sobie tak:

[Activity(Label = "@string/file_list_activity_name", Theme = "@style/AppTheme.NoActionBar")]

a całą aplikację (np. jej ikonę) konfiguruję poprzez atrybut assembly:

[assembly: Application(AllowBackup = true,
                        Icon = "@mipmap/ic_launcher",
                        Label = "@string/app_name",
                        SupportsRtl = true,
                        Theme = "@style/AppTheme")]

Xamarin daje wygodniejszą generyczną metodę do wyszukiwania referencji na element w widoku np:

mRecyclerView = FindViewById<RecyclerView>(Resource.Id.item_list);

Przy okazji widzimy, że nieco inaczej odwołujemy się do zasobów (generowana jest klasa Resource).

A teraz C# kontra Java.

Zamiast metod getX i setX mamy właściwości.

Jeśli chodzi o delegaty, to czasami możemy użyć zamiast nich eventu. Jednak nieraz robimy podobnie jak w Javie, implementując widoczny w C# odpowiedni interfejs. Przykładowo jeśli chcemy, by aktywność była delegatem dla SearchView, to piszemy:

   public class FileListActivity : Activity,  Android.Support.V7.Widget.SearchView.IOnQueryTextListener
   {

        …

        mSearchView.SetOnQueryTextListener(this);

        …

       public bool OnQueryTextSubmit(string query)
       {
           return false;
       }

       public bool OnQueryTextChange(string newText)
       {
           searchText = newText;

           SearchFiles();

           return true;
       }      

   }

Przy adapterze do RecyclerView możemy z kolei użyć eventu do obsługi kliknięcia w pozycję listy:

   mAdapter = new SimpleItemRecyclerViewAdapter(mModel);
   mAdapter.ItemClick += OnItemClick;
   mRecyclerView.SetAdapter(mAdapter); 

   …    

   public class MediaItemViewHolder : RecyclerView.ViewHolder
   {
       public TextView TitleView { get; private set; }
       public TextView ArtistView { get; private set; }
       public TextView DurationView { get; private set; }
       public MediaBrowserCompat.MediaItem Item { get; set; }

       public MediaItemViewHolder(View view, Action<MediaBrowserCompat.MediaItem> listener) : base(view)
       {
           TitleView = view.FindViewById<TextView>(Resource.Id.title);
           ArtistView = view.FindViewById<TextView>(Resource.Id.artist);
           DurationView = view.FindViewById<TextView>(Resource.Id.duration);

           view.Click += (sender, e) => listener(Item);
       }
   }

  public class SimpleItemRecyclerViewAdapter : RecyclerView.Adapter
   {      

       public event EventHandler<MediaBrowserCompat.MediaItem> ItemClick;

       ….

       public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType)
       {
           View view = LayoutInflater.From(parent.Context).
                   Inflate(Resource.Layout.file_list_item, parent, false);

           var vh = new MediaItemViewHolder(view, OnClick);
           return vh;
       }

       …

        void OnClick(MediaBrowserCompat.MediaItem item)
       {
           ItemClick?.Invoke(this, item);
       }
   }

Przy okazji widać tutaj opcjonalne wywołanie metody z C#6, którego nie ma w Javie. C#6, a w niedalekiej przyszłości C#7 zwiększają jeszcze przewagę składniową C# nad Javą. Ale żeby było pluralistycznie na moim blogu, pokażę że Java ma pewną rzecz, której zamodelowanie w C# jest mniej wygodne i wymaga więcej kodu. Chodzi o klasy zagnieżdżone, które w Javie mogą być statyczne (jak w C#) lub instancyjne. Te ostatnie mają niejawną referencję to instancji rodzica. W C# tego nie mamy i musimy jawnie taką referencję dostarczyć. Przykładowo w FileListSearchViewExpandListener potrzebujemy odwołać się do zawierającej jej aktywności:

     MenuItemCompat.SetOnActionExpandListener(item, new FileListSearchViewExpandListener(this));

     …    

       private class FileListSearchViewExpandListener: Java.Lang.Object, MenuItemCompat.IOnActionExpandListener
       {
           private readonly FileListActivity _activity;

           public FileListSearchViewExpandListener(FileListActivity activity)
           {
               _activity = activity;
           }

           public bool OnMenuItemActionCollapse(IMenuItem item)
           {
               _activity.searchOpen = false;

               _activity.mAdapter.SetFilter(_activity.mModel);

               return true;
           }

           public bool OnMenuItemActionExpand(IMenuItem item)
           {
               _activity.searchOpen = true;

               if (_activity.searchText != null)
               {
                   _activity.queryToSave = _activity.searchText;
               }

               return true;
           }

Inną przykładem na mniej zgrabne mapowanie z Javy na C# są stałe w pakiecie. W C# pojawiają się dodatkowe sztuczne klasy na takie rzeczy np:  InterfaceConsts dla stałej MediaStore.Audio.Media.InterfaceConsts.Title.

Sporym mankamentem Xamarina jest w zasadzie niemapowanie generyków z Javy na generyki w C#. Dlatego klasa SimpleItemRecyclerViewAdapter rozszerza RecyclerView.Adapter i w środku musimy sami rzutować obiekt holdera na odpowiedni typ.

Czasami C# odpowiednik może nazywać się inaczej niż w Javie np. przy obsłudze zasobów językowych w Xamarin używamy metody GetText w kodzie:

mSearchView.QueryHint = Resources.GetText(Resource.String.search_songs);

Trzeba też powiedzieć, że czasami styk między API w Javie a C# nie jest całkowicie transparentny. Są różne pułapki. W moim kodzie potrzebowałem na przykład dokonać specjalnego rzutowania na obiekt Javy przy obsłudze SearchView:

            var item = menu.FindItem(Resource.Id.action_search);
           var searchView = MenuItemCompat.GetActionView(item);
           mSearchView = searchView.JavaCast<Android.Support.V7.Widget.SearchView>(); 

IMHO jest to pewna niedoskonałość Xamarina. Pewne rzeczy trzeba po prostu wiedzieć.

 

Stylowanie themy

Jak uzyskałem “zieloną” edycję? 

W values/styles.xml wprowadziłem zmiany:

     <color name="theme_devicedefault_background">#ff33691e</color>
    <style name="AppTheme" parent="Theme.AppCompat">       
        <item name="android:colorBackground">@color/theme_devicedefault_background</item>
        <item name="android:windowBackground">?android:attr/colorBackground</item>      
        <item name="colorPrimary">#ff1b5e20</item>       
    </style>

oraz 

   <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Dark">
       <item name="android:colorBackground">#ff1b5e20</item>
   </style> 

 

To tyle na dziś. Aha dodam, że obecna przedstawiona tutaj początkowa wersja w Xamarin zajmuje na telefonie ponad 30MB, a pełnofunkcjonalna wersja w Javie… nieco ponad 15 MB. W Xamarin można dokonać optymalizacji przez linkowanie, ale zrobię to na koniec. Najbliższy krok to dalsze portowanie kolejnej porcji app-ki, by działał serwis przeglający i odtwarzający pliki audio.

Brak komentarzy: