środa, 27 lipca 2016

Xamarin.Android kontra Java odc.2 (narzędzia, składnia, async, synchronizacja, Java i C#, audio w tle, grafika 2D, własny widok, Visualizer, Broadcast Receiver, LocalBroadcastManager, stylowanie CardView)

Hurra!  “Kolekcjonerska” edycja mojej app-ki w Xamarin.Android uzyskała właśnie taką funkcjonalność jak oryginał w Javie po odcinku 9 z DSP. Sprawdzajcie na githubie. To większość kodu, do przeportowania pozostał jedynie uzupełniający materiał z odcinka 10.

Screenshot_20160727-195213

Ponieważ portuję swój własny kod sprzed paru miesięcy, nie chciałbym się powtarzać, jeśli chodzi o sprawy związane ściśle z samym Androidem, które już opisywałem podczas DSP. Zajmiemy się więc bardziej wysublimowanymi sprawami związanymi z samym Xamarinem lub stykiem Android-Xamarin.

#1  Target API Level

Rzecz niby banalna, a trochę dała mi w kość w Visual Studio 2015. Przy ustawieniu docelowego targetu na API 23 z Android 6 dostawałem najpierw komunikat z ostrzeżeniami o wersji Javy. Postąpiłem zgodnie ze stackoverflow i doinstalowałem Javę 8.0 SDK (x86). Potem w ustawieniach ogólnych Xamarin (Tools –> Options –> Xamarin) w Visual Studio zmieniłem ścieżkę na tę wersję. Wtedy dostałem ostrzeżenie, że wersja z manifestu Android jest starsza niż TargetFramework!!! i że do kompilacji i tak zostanie użyta najnowsza wersja API, czyli 24 z nadchodzącego Android 7 (może dlatego wcześniej krzyczało o nowszą Javę niż 7 instalowana domyślnie z VS 2015 U3).  A wszystko miałem ustawione jak należy. W końcu wpadłem na pomysł, by ręcznie przedytować plik .csproj i parametr AndroidUseLatestPlatformSdk ustawiłem na False (pozostawał ciągle na True w XML mimo wybrania z comba w VS konkretnej wersji SDK). Pomogło!

#2 Klasy zagnieżdżone z akcjami

Obserwując różne sample Xamarin wyczaiłem lepszy niż ostatnio pattern do portowania klas callbacków z Javy (czy broadcast receiverów). Otóż aby nie przekazywać jawnie referencji do obiektu klasy rodzica wzbogacamy callback o akcje wywoływane przez jego metody. Natomiast kod akcji ustawiamy w klasie rodzica, przeważnie w jej konstruktorze lub metodzie tworzącej np. OnCreate.

    public abstract class BaseActivity: AppCompatActivity, …
    {          

        class Callback : MediaControllerCompat.Callback
        {
            public Action<PlaybackStateCompat> OnPlaybackStateChangedImpl { get; set; }
            public Action<MediaMetadataCompat> OnMetadataChangedImpl { get; set; }

            public override void OnPlaybackStateChanged(PlaybackStateCompat state)
            {
                OnPlaybackStateChangedImpl(state);
            }

            public override void OnMetadataChanged(MediaMetadataCompat metadata)
            {
                OnMetadataChangedImpl(metadata);
            }
        }

        readonly Callback mediaControllerCallback = new Callback();

        …


        protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);

            …

            mediaControllerCallback.OnPlaybackStateChangedImpl = (state) =>
            {
                if (ShouldShowControls)
                {
                    ShowPlaybackControls();
                }
                else
                {
                    LogHelper.Debug(Tag, "mediaControllerCallback.onPlaybackStateChanged: " +
                            "hiding controls because state is ", state.State);
                    HidePlaybackControls();
                }
            };

            mediaControllerCallback.OnMetadataChangedImpl = (metadata) =>
            {
                if (ShouldShowControls)
                {
                    ShowPlaybackControls();
                }
                else
                {
                    LogHelper.Debug(Tag, "mediaControllerCallback.onMetadataChanged: " +
                        "hiding controls because metadata is null");
                    HidePlaybackControls();
                }
            };

        }                                
               
    }

#3 Asynchroniczne wywołania

Marzy mi się API Android z async-ami we wszystkich trwających dłużej metodach, tak jak mamy w WinRT API w Windows 8.x/10.  Póki co na pewno async możemy stosować przy kliencie Http z .NET. Przykładem niech będzie metoda z klasy BitmapHelper z przykładu MediaBrowserService (będącego portem z oryginału w Javie z Android SDK):

        public async static Task<Bitmap> FetchAndRescaleBitmap(string uri, int width, int height)
        {
            try
            {
                using (var client = new HttpClient())
                {
                    Stream stream = await client.GetStreamAsync(uri);
                    int scaleFactor = FindScaleFactor(width, height, stream);
                    LogHelper.Debug(Tag, string.Format("Scaling bitmap {0} by factor {1} to support {3}x{4} requested dimension.",
                        uri, scaleFactor, width, height));
                    return Scale(scaleFactor, stream);
                }
            }
            catch (IOException e)
            {
                throw e;
            }
        }

Później jednak autor płynnie przechodzi z kodu z async na kod bez async:

public void Fetch(string artUrl, FetchListener listener)
        {
           …

            Task.Run(async () => {
                Bitmap[] bitmaps;
                try
                {
                    Bitmap bitmap = await BitmapHelper.FetchAndRescaleBitmap(artUrl, MaxArtWidth, MaxArtHeight);
                    Bitmap icon = BitmapHelper.Scale(bitmap, MaxArtWidthIcon, MaxArtHeightIcon);
                    bitmaps = new[] { bitmap, icon };
                    cache.Put(artUrl, bitmaps);
                }
                catch (IOException)
                {
                    return null;
                }
                LogHelper.Debug(Tag, "doInBackground: putting bitmap in cache. cache size=" + cache.Size());
                return bitmaps;
            }).ContinueWith((antecedent) => {
                var bitmaps = antecedent.Result;
                if (bitmaps == null)
                    listener.OnError(artUrl, new ArgumentException("got null bitmaps"));
                else
                    listener.OnFetched(artUrl, bitmaps[BigBitmapIndex], bitmaps[IconBitmapIndex]);
            }, TaskContinuationOptions.OnlyOnRanToCompletion);
        }

Ja z kolei odczyt utworów z Music Library w klasie MusicProvider umieściłem w innym wątku:

public void RetrieveMediaAsync(Context context, Action<bool> callback)
        {
            LogHelper.Debug(Tag, "retrieveMediaAsync called");
            if (currentState == State.Initialized)
            {
                callback?.Invoke(true);
                return;
            }

            Task.Run(() =>
            {
                try
                {
                    if (currentState == State.NonInitialized)
                    {
                        currentState = State.Initializing;

                        var projection = new string[]
                        {
                            MediaStore.Audio.Media.InterfaceConsts.Id,
                            MediaStore.Audio.Media.InterfaceConsts.Artist,
                            MediaStore.Audio.Media.InterfaceConsts.Title,
                            MediaStore.Audio.Media.InterfaceConsts.Duration,
                            MediaStore.Audio.Media.InterfaceConsts.Data,
                            MediaStore.Audio.Media.InterfaceConsts.MimeType
                        };

                        var selection = MediaStore.Audio.Media.InterfaceConsts.IsMusic + "!= 0";
                        var sortOrder = MediaStore.Audio.Media.InterfaceConsts.DateAdded + " DESC";

                        var cursor = context.ContentResolver.Query(MediaStore.Audio.Media.ExternalContentUri, projection, selection, null, sortOrder);

                        if (cursor != null && cursor.MoveToFirst())
                        {
                            do
                            {
                                int idColumn = cursor.GetColumnIndex(MediaStore.Audio.Media.InterfaceConsts.Id);
                                int artistColumn = cursor.GetColumnIndex(MediaStore.Audio.Media.InterfaceConsts.Artist);
                                int titleColumn = cursor.GetColumnIndex(MediaStore.Audio.Media.InterfaceConsts.Title);
                                int durationColumn = cursor.GetColumnIndex(MediaStore.Audio.Media.InterfaceConsts.Duration);
                                int filePathIndex = cursor.GetColumnIndexOrThrow(MediaStore.Audio.Media.InterfaceConsts.Data);

                                var id = cursor.GetLong(idColumn).ToString();

                                var item = new MediaMetadataCompat.Builder()
                                        .PutString(MediaMetadataCompat.MetadataKeyMediaId, id)
                                        .PutString(CustomMetadataTrackSource, cursor.GetString(filePathIndex))
                                        .PutString(MediaMetadataCompat.MetadataKeyArtist, cursor.GetString(artistColumn))
                                        .PutString(MediaMetadataCompat.MetadataKeyTitle, cursor.GetString(titleColumn))
                                        .PutLong(MediaMetadataCompat.MetadataKeyDuration, cursor.GetInt(durationColumn))
                                        .Build();

                                musicListById.Add(id, new MutableMediaMetadata(id, item));

                            } while (cursor.MoveToNext());
                        }

                        currentState = State.Initialized;
                    }
                }
                finally
                {
                    if (currentState != State.Initialized)
                    {
                         currentState = State.NonInitialized;
                    }
                }
            }).ContinueWith((antecedent) => {
                callback?.Invoke(currentState == State.Initialized);
            }, TaskContinuationOptions.OnlyOnRanToCompletion);           
        } 

Brakuje według mnie dobrego wykładu, kiedy stosować async w Xamarin, kiedy wybrać Task.Run, a kiedy coś innego… Chyba że jeszcze nie dotarłem do takich materiałów, w każdym razie w podstawowej dokumentacji głównie jest tłumaczone czym w ogóle async w C# jest. Pamiętam, że gdzieś ktoś opisał, jak async został zaimplementowany w Xamarin… Przydałoby się to odszukać i przeprowadzić małe dochodzenie…    

#4 Synchronizowane kolekcje

Synchronizowane kolekcje, kolejny stosunkowo mało nagłaśniany temat. W Javie ktoś napisze  Collections.synchronizedList(…) i jak to przetłumaczyć na C#?  Co prawda mamy w .NET trochę synchronizowanych kolekcji, zwłaszcza od .NET 4.0 pojawiły się dość nowoczesne bardziej wydajne byty, nie bazujące tylko na lock-ach. Ale jeśli potrzebujemy indeksowanego dostępu jak w liście, to akurat tego … nie mamy. Tłumacząć klasę QueueManager użyłem w końcu kodu z klasy SynchronizedList ze źródeł Xamarin.Forms…  Ale to jest klasa internal, w dodatku jej implementacja bazuje na lock-ach. Czemu w .NET nie ma takiej gotowej klasy, albo w Xamarin?

#5 Elementy Javy i  Java-C#

Brakuje mi jakiejś klasy czy konstrukcji z Javy. Co robię? Tworzę odpowiedniki w C# lub … wywołuję elementy Javy z dodatkowych przestrzeni nazw….  Tłumacząc LightOrganProcessor w pewnym momencie potrzebowałem wywołać System.currentTimeMillis(). Mógłbym to zrobić pisząc Java.Lang.JavaSystem.CurrentTimeMillis(), ale zauważyłem że ktoś w jednym z przykładów dla Xamarin napisał ręcznie w czystym C# odpowiednik:

var beginningOfTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

long systemTimeStartSec = (long)(DateTime.UtcNow - beginningOfTime).TotalMilliseconds;

Nie zauważyłem różnic pomiędzy jednym a drugim sposobem, ale może w C# powinniśmy używać jak najwięcej C# by unikać narzutu komunikacji między dwiema maszynami wirtualnymi…

A teraz rzecz, na której straciłem kilka godzin, zanim odkryłem co nie działało. Serwis Androidowy nie dawał mi znaku życia i żadnych informacji o błędach czemu nie wstawał. Okazało się, że nie tworzyłem w BaseActivity obiektu klasy ComponentName w należyty sposób. Co prawda podawałem nazwę ala Java ale w stringu, może nie był do końca dobry, albo ktoś tego inaczej nie przewidział. W każdym razie wzorowany na jednym samplu poprawny sposób na ComponentName wymagający obiektu class Javy:

mediaBrowser = new MediaBrowserCompat(this,
                new ComponentName(this, Java.Lang.Class.FromType(typeof(MusicService))), connectionCallback, null);

#6 Fragmenty i własne widoki

O ile w Javie jak jest pakiet, to wszystko w nim jest, o tyle w projekcie w Xamarin jest pewna dowolność. Nazwa pakietu nie musi zgadzać się z .NET-ową przestrzenią nazw w klasach aplikacji (w generowanym przez Xamarin manifeście - jak podejrzałem - są wstawiane dość długie unikalne ciągi znaków przed nazwami klas). W moim przypadku przy wstawianiu fragmentu do XML musiałem zmienić jego nazwę, by pasowała do faktycznego namespace w C# (miałem wyjątek przy otwieraniu widoku aplikacji):

<fragment
            android:name="LightOrganApp.Droid.UI.PlaybackControlsFragment"
            android:id="@+id/fragment_playback_controls"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            tools:layout="@layout/fragment_playback_controls" />

Podobnie było z własnym widokiem:

<LightOrganApp.Droid.UI.CircleView
       android:id="@+id/bass_light"
       android:layout_width="match_parent"
       android:layout_height="0dp"
       android:layout_weight="1"
       app:circleColor="#d50000"/>

#7 Wyjątki

Zauważyłem po samplach, że można łapać wyjątki systemowe z Javy, ale tam, gdzie da się łapać wyjątek w .NET to taki jest wtedy łapany.

#8 Stylowanie CardView

Potrzebowałem zrobić sobie zielony dolny pasek. U mnie jest to fragment hostowany w CardView. Tło było widoczne z CardView, to więc ostylowałem. Jak? Dodałem definicję stylu w values/styles.xml:

<style name="AppTheme.CardView" parent="Theme.AppCompat">   
       <item name="cardBackgroundColor">#ff1b5e20</item>
   </style>

Po czym zaaplikowałem ten styl w miejscach użycia CardView:

<android.support.v7.widget.CardView
           android:id="@+id/controls_container"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:layout_gravity="bottom"
           app:cardElevation="8dp"
           style="@style/AppTheme.CardView">
           <fragment
               android:name="LightOrganApp.Droid.UI.PlaybackControlsFragment"
               android:id="@+id/fragment_playback_controls"
               android:layout_width="match_parent"
               android:layout_height="wrap_content"
               android:layout_alignParentBottom="true"
               tools:layout="@layout/fragment_playback_controls" />
       </android.support.v7.widget.CardView>

Uwaga jeszcze do designera plików .axml w Visual Studio 2015.  Czemu po każdym załadowaniu widoku do designera plik XML ulega edycji, mimo że nic w nim jeszcze nie zmieniłem?

#9 Zagadka z wizualizacji

Największa zagadka wszech czasów przede mną do rozwiązania. Czemu ten sam z pozoru kod w Javie powoduje ładne błyski świateł w rytm muzy, natomiast w Xamarinie… błyska trochę co najwyżej pierwsze światło, a reszta w zasadzie nie mruga? Porównywałem już parę razy kod oryginału z portowanym i póki co nie wiem. Powstaje pytanie czy to nie jest objaw mniejszej wydajności, chyba że wrapper Xamarina do Visualizer’a jest w jakiś sposób niedopracowany. Trzeba zbadać, czy w obu przypadkach z API dostaję przy tej samej piosence takie same tablice z bajtami dla szybkiej transformaty Fouriera…  Ustaliłem że dwa pozostałe światła dostają ciągle współczynnik 1, więc nic dziwnego, że nie mrugają.

#10 Rozmiar i linkowanie

Oryginalna aplikacja w Javie zajmuje na telefonie 15,42 MB. Jej nie w pełni funkcjonalny odpowiednik w Xamarin… 30,97 MB, czyli póki co dwa razy tyle. Linkowanie w Xamarin ma za zadanie wyrzucać nieużywany kod. Domyślna opcja to brak, druga to linkowanie tylko dla SDK, ostatnia - najmniej bezpieczna - linkowanie dla SDK i aplikacji. Po zastosowaniu stosunkowo bezpiecznej opcji drugiej aplikacja na telefonie zajęła… 30,82 MB. Czyli praktycznie niewiele to wniosło. Opcji trzeciej póki co nie próbowałem, bo wymaga pewnych zabezpieczeń w kodzie aplikacji, by nie zostało z niego wycięte za dużo… Przydałby się jakiś dobry przewodnik do tego, informacje w podstawowej dokumentacji wskazują raczej na żmudną pracę z możliwością wpadki.

To tyle na dziś. Punkt #9 wymaga rozwiązania. Jak ktoś to czyta i ma jakiś pomysł względem tej zagadki, zapraszam do dyskusji. Poza tym w najbliższym kroku będę portował resztę kodu z Javy, by dorównać funkcjonalnością oryginałowi.

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.