wtorek, 2 sierpnia 2016

Xamarin.Android kontra Java odc.3 (ustawienia i ich stylowanie, sockety)

W nawiązaniu do odcinka 10 app-ki w Javie na Android z czasów legendarnego już DSP na github w “limitowanej zielonej” gałęzi Xamarin przybył - odpowiadający mu w miarę możliwości - kod w C#. Oto screenshoty prosto z mojego telefonu:

Screenshot_20160802-200250  Screenshot_20160802-200318

Poruszę dziś trzy techniczne zagadnienia. Nie wszystko wyszło jak planowałem. Nastawcie się na zapiskę bez retuszu, mówiącą po prostu “jak jest” –Winking smile

 

1# Ustawienia - własna kontrolka

Napisałem w C# podobnie jak Javie własną kontrolkę NumberPickerPreference do wprowadzania ustawień liczbowych. Posiłkowałem się samplami Xamarina na githubie, powinno być OK. Uwagę zwraca zobrazowanie konstrukcji CREATOR w C# (potrzebny specjalny atrybut ExportField + referencja do assembly Mono.Android.Export), a także miejscami jawna potrzeba użycia obiektów Javy. Klasa BaseSavedState w C# nie jest dostępna w postaci generycznej. W porównaniu z oryginałem w Javie wygląda to trochę mniej zgrabnie, choć oczywiście w powstałym kodzie klasy znajdziemy i bardziej zgrabne drobniejsze elementy C# w porównaniu do Javy.

   [Register("com.apps.kruszyn.lightorganapp.droid.NumberPickerPreference")]
   public class NumberPickerPreference: DialogPreference
   {
       private const int DefaultValue = 0;

       private NumberPicker numberPicker;
       private int? currentValue;

       public NumberPickerPreference(Context context, IAttributeSet attrs): base(context, attrs)
       {
           DialogLayoutResource = Resource.Layout.numberpicker_dialog;
           SetPositiveButtonText(Android.Resource.String.Ok);
           SetNegativeButtonText(Android.Resource.String.Cancel);

           DialogIcon = null;
       }

       …       
       protected override void OnSetInitialValue(bool restorePersistedValue, Java.Lang.Object defaultValue)
       {
           if (restorePersistedValue)
           {
               currentValue = GetPersistedInt(DefaultValue);
           }
           else
           {
               currentValue = (int)defaultValue;
               PersistInt(currentValue.Value);
           }
       }

       protected override Java.Lang.Object OnGetDefaultValue(TypedArray a, int index)
       {
           return a.GetInteger(index, DefaultValue);
       }

       protected override IParcelable OnSaveInstanceState()
       {
           var superState = base.OnSaveInstanceState();

           if (Persistent)
           {
               return superState;
           }

           var myState = new SavedState(superState);
           myState.Value = numberPicker.Value;
           return myState;
       }

       protected override void OnRestoreInstanceState(IParcelable state)
       {
           if (state == null || state.GetType() != typeof(SavedState)) {
               base.OnRestoreInstanceState(state);
               return;
           }

           var myState = (SavedState)state;
           base.OnRestoreInstanceState(myState.SuperState);

           numberPicker.Value = myState.Value;
       }

       public class SavedState: BaseSavedState
       {
           public int Value { get; set; }

           public SavedState(IParcelable superState): base(superState)
           {               
           }

           public SavedState(Parcel source): base(source)
           {               
               Value = source.ReadInt();
           }

           public override void WriteToParcel(Parcel dest, ParcelableWriteFlags flags)
           {
               base.WriteToParcel(dest, flags);
               dest.WriteInt(Value);
           }

           [ExportField("CREATOR")]
           static SavedStateCreator InitializeCreator()
           {
               return new SavedStateCreator();
           }

           class SavedStateCreator : Java.Lang.Object, IParcelableCreator
           {
               public Java.Lang.Object CreateFromParcel(Parcel source)
               {
                   return new SavedState(source);
               }

               public Java.Lang.Object[] NewArray(int size)
               {
                   return new SavedState[size];
               }
           }
       }         
   }

Niestety aplikacja w Xamarin uparcie wyrzuca w runtime wyjątek:

System.NotSupportedException: Unable to activate instance of type LightOrganApp.Droid.UI.NumberPickerPreference from native handle 0x7fe6652874 (key_handle 0x63ae23e).

przy próbie jakiejkolwiek czynności wymagającej załadowania XML dla fragmentu ustawień:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">

    …
    <com.apps.kruszyn.lightorganapp.droid.NumberPickerPreference
        android:key="pref_remote_device_port"
        android:dependency="pref_use_remote_device"
        android:defaultValue="8181"
        android:title="@string/pref_title_remote_device_port" />   

</PreferenceScreen>

Przy okazji mogę pokazać jak można w Xamarin powiedzieć, że jakaś klasa .NET jest z jakiegoś pakietu ala Java. Służy do tego atrybut Register nad pisaną przez nas klasą. Niestety to nie pomogło rozwiązać problemu. Wcześniej miałem wersję podobną do kodu z poprzedniego postu (osadzałem tam kilka swoich elementów w XML), czyli bez atrybutu Register, a w XML po prostu LightOrganApp.Droid.UI.NumberPickerPreference. W każdym przypadku dostaję ten sam wyjątek dotyczący LightOrganApp.Droid.UI.NumberPickerPreference, co pokazuje równoważność obu podejść –Winking smileNiestety na forum Xamarin też nie znalazłem rozwiązania podobnych problemów:

Coś jest na rzeczy, bo nawet w samplu Xamarina o nazwie ApiDemo plik ustawień xml/advanced_preferences.xml z własnym widokiem nie wydaje się używany. Może nie jest to zbyt często używany ficzer i jest jakiś bug albo jednak coś robię podobnie jak parę innych osób inaczej niż ktoś zamierzył…

Aby zielona Xamarinowa edycja mojej app-ki działała, póki co zamiast niestandardowej kontrolki zastosowałem mniej efektowny standardowy EditTextPreference z android:inputType="number":

<EditTextPreference
        android:key="pref_remote_device_port"
        android:dependency="pref_use_remote_device"
        android:defaultValue="8181"
        android:inputType="number"
        android:maxLines="1"
        android:singleLine="true"
        android:title="@string/pref_title_remote_device_port" />

Nie zauważyłem, by przy takim inpucie można wprowadzić jakieś głupoty do tego pola (poza pustym stringiem), zresztą mój kod jest na nie odporny. Odwołuję się do pola tekstowego, którego wartość konwertuję w razie potrzeby (i możliwości) na liczbę.

 

#2 Ustawienia - stylowanie okien dialogowych

Zielone tło całego fragmentu z ustawieniami dostałem na dzień dobry w wyniku mojego wcześniejszego stylowania. Okna dialogowe np. do EditTextPreference nadal pozostawały niezielone…  Wyszła tutaj spora trudność w ostylowaniu tego elementu w Androidzie. Posiłkując się stackoverflow ustaliłem, że trzeba zastosować sztuczkę i ostylować  Dialog.Alert w używanej themie. W swojej aplikacji używam Theme.AppCompat, by wspierać Android starszy niż 5.0. Dostawałem wtedy co prawda ładne zielone tło, ale okno dialogowe … się nieładnie skurczyło w stosunku do wersji nieostylowanej. Długo zajęło mi zanim dotarłem do posta https://www.reddit.com/r/androiddev/comments/2qt9kd/overriding_alertdialogtheme_and_material_themes/, który wyjaśnił mi wszystko. Ponieważ jest to limitowana edycja mojej app-ki postanowiłem, że odpuszczę systemy starsze niż 5.0 i po zastosowaniu zdefiniowaniu stylu dziedziczącego z systemowego stylu android:Theme.Material.Dialog.Alert otrzymałem zielone okienko dialogowe w postaci nieskurczonej:

     <style name="AppTheme" parent="Theme.AppCompat">        
        …       
        <item name="android:alertDialogTheme">@style/AlertDialogStyle</item>       
    </style> 

    <style name="AlertDialogStyle" parent="android:Theme.Material.Dialog.Alert">     
      <item name="android:background">#ff1b5e20</item>       
    </style>

Nielicho się narobiłem…. wymyślając te kilka linijek w XML-u. Na obecny moment nie walczyłem, by działało to na Android 4.x…

 

#3 Sockety

Tak jak w przypadku oryginału w Javie postanowiłem użyć socketów, by rozmawiać z diodami LED na Raspberry Pi 2.  Generalnie mogę powiedzieć, że komunikacja mi działa. Mam jednak kilka uwag.

Po pierwsze najpierw myślałem, że sobie przekleję kod z testowej klienckiej app-ki UWP z czasów DSP. No tak, ale na UWP dla Windows 10 mamy platformę WinRT i .NET Core, a Xamarin używa Mono. Skutkuje to potrzebą pisania innego kodu przy użyciu klas TcpClient i NetworkStream zamiast nowocześniejszych StreamSocket i DataWriter. Dobrze chociaż, że w klasach Mono są metody zwracające obiekty klasy Task, na które można poczekać await-em. No tak, postulat Scotta Hanselmana z BUILD 2016, by zrobić porządek z .NET (pełny .NET, .NET Core, Mono) i stworzyć wspólną podstawę pokazuje swoją zasadność. Tym bardziej, że Microsoft póki co nie oferuje dll-ki portable dla socketów. Są oczywiście komponenty trzecie np. Sockets Plugin (dostępny także w postaci pakietu nuget), ale lepiej byłoby by .NET sam sobie z tym radził.

Przejrzałem sobie kod ostatnio wspomnianego tu komponentu, okazał się dość prosty, więc na razie postanowiłem skorzystać bezpośrednio z klas Mono. Nie jest możliwe stworzenie portable library tylko dla Android i iOS (zaznaczają się zawsze checki z .NET, Windows 8.x, UWP), więc stworzyłem sobie projekt LightOrganApp.Shared - jak nazwa wskazuje - typu Shared. Posłuży mi teraz do zielonego klona w Xamarin.Android, a później także do Xamarinowego klona na iOS. W stosunku do oryginału w Javie przy pomocy async powstał bardziej zgrabny kod w C#, którego esencję zamknąłem w klasie LightsRemoteController:

    public class LightsRemoteController
    {
        TcpClient tcpClient;
        NetworkStream writeStream;       

        public async Task ConnectAsync(string host, int port)
        {
            try
            {
                tcpClient = new TcpClient();

                await tcpClient.ConnectAsync(host, port);
                writeStream = tcpClient.GetStream();
            }
            catch(Exception ex)
            {
                Debug.Write(ex.StackTrace);
            }          
        }

        public async Task SendCommandAsync(byte[] bytes)
        {
            try
            {
                if (writeStream != null)
                {
                    writeStream.Write(bytes, 0, bytes.Length);
                    await writeStream.FlushAsync();
                }
            }
            catch(Exception ex)
            {
                Debug.Write(ex.StackTrace);
            }
        }       

        public async Task CloseAsync()
        {
            try
            {
                var bytes = new byte[3] { 13, 13, 13 };
                await SendCommandAsync(bytes);

                if (writeStream != null)
                    writeStream.Close();

                if (tcpClient != null)
                    tcpClient.Close();
            }
            catch(Exception ex)
            {
                Debug.Write(ex.StackTrace);
            }

            writeStream = null;
            tcpClient = null;
        }
    }

Korzysta z niej MusicService. Aplikacja zachowuje się w każdym przypadku jak powinna, ale łapanie wyjątków się w niej przydaje. Przy podawaniu błędnego hosta lub portu albo przy włączaniu przesyłu przy błędnej konfiguracji czy też przy powrocie do prawidłowych ustawień sypie się więcej wyjątków niż sypało się w takich sytuacjach w Javie… Jakby czasami pewien kod po zaistnieniu błędu wywoływał się kilka razy.  To jest wstępna implementacja, być może mogę jakoś ją udoskonalić. Z drugiej strony niewykluczone, że sockety na Mono w Xamarin w połączeniu z async same w sobie wymagają pewnych poprawek. Całkiem niedawno były poprawiane do nich bugi (np. Bug 41616 ), pewne  zgłoszenia są otwarte (np. Bug 42617).

 

W tym wszystkim nie chodzi tu o czarny pijar, nadal uważam, że Xamarin jest całkiem dobrą platformą, a C# z Visual Studio w pewnych - zwłaszcza biznesowych - aplikacjach może lepiej się sprawdzić niż tworzenie kodu w zupełnie innym języku i narzędziach. Obrałem sobie jednak dziś bardziej wysublimowane zagadnienia i pewne rzeczy w platformie mogą wymagać udoskonaleń (punkty 1# i 3#, dzisiejszy punkt 2# dotyczy Androida w ogóle). Dzisiejsze problemy nie są jednak bardzo dokuczliwe. Implementację kolorofonu na Xamarin.Android mogę uznać za zakończoną i w miarę udaną, jeśli uda mi się wyjaśnić sprawę z punktu #9 Zagadka z wizualizacji z poprzedniego posta. Zapowiada się ciekawe śledztwo.

ś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.

sobota, 25 czerwca 2016

BUILD 2016 - odc.11 (EF Core, Windows Insider, Open Source, HoloLens, Microsoft Band)

Jest weekend, co prawda już po północy i po… Brexit, ale trzeba żyć dalej. Co obejrzałem z BUILD wczoraj i dziś?

  • Entity Framework Core - konkretna prezentacja, kto używa EF lub zamierza powinien obejrzeć
  • Windows Insider Program - będąc Windows Insiderem nie mogłem sobie odmówić obejrzenia, spodziewałem się przyznam więcej, dostajemy w każdym razie rzetelne podsumowanie
  • Open Source - pierwsze kroki w open source i społeczności, warto obejrzeć, teraz chyba już wiem skąd Maciek na DSP mówił o 1%  dev-ów dających się poznawać i tworzących coś poza
  • HoloLens - zdaje się jedyne nagranie wideo o HoloLens z BUILD 2016 w necie, w tym roku w ogóle przy całkiem pokaźnej liczbie sesji nie wrzucono na Channel 9 nagrań wideo, a w przypadku HoloLens jakby się ktoś wręcz umówił (mieliśmy już z jednym kolegą teorię spiskową, że poza granicami USA z uwagi na możliwe w zastosowanie w wojsku technologia nie jest mocno promowana). Prezentacja może nie robi szału, ale dostarcza trochę informacji
  • Microsoft Band - opaska nie zmieniła się mocno od ostatniego roku, ale można zobaczyć

 

Entity Framework Core 1.0

image_thumb[17]

image_thumb[20]

image_thumb[23]

image_thumb[26]

image_thumb[29]

image_thumb[32]

image_thumb[36]

image_thumb[39]

lepsza wydajność EF Core w niektórych scenariuszach w stosunku do EF 6

image_thumb[42]

image_thumb[45]

image_thumb[48]

image_thumb[51]

rozszerzalność

Sqlite

image

image

optymalizacja generowanego SQL

image

image

mądry EF Winking smile

image

 

Inside the Windows Insider Program 

image

 

Getting Started in Open Source with Scott Hanselman 

image

image

cha, cha, w dzień Windows Forms, w nocy Arduino –Winking smile

image

 

HoloLens: Building UWP 2D Apps for Microsoft HoloLens

image

image

AirTap

Scroll

Drag

Zoom

image

image

image

image

image

image

image

image

image

image

image

image

image

 

Building Cross-Platform Experiences for Microsoft Band 

image

image

image

image

image