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.

Brak komentarzy: