Udostępnij za pośrednictwem


Zasady projektowania interfejsu API platformy Xamarin.Android

Oprócz podstawowych bibliotek klas bazowych, które są częścią mono, platforma Xamarin.Android jest dostarczana z powiązaniami dla różnych interfejsów API systemu Android, aby umożliwić deweloperom tworzenie natywnych aplikacji systemu Android za pomocą platformy Mono.

W rdzeniu platformy Xamarin.Android istnieje aparat międzyoperacyjny, który łączy świat języka C# ze światem Języka Java i zapewnia deweloperom dostęp do interfejsów API Języka Java z języka C# lub innych języków platformy .NET.

Zasady projektowania

Oto niektóre z naszych zasad projektowania powiązania platformy Xamarin.Android

  • Zgodność z wytycznymi projektowymi programu .NET Framework.

  • Zezwalaj deweloperom na klasy języka Java podklasy.

  • Podklasa powinna współdziałać ze standardowymi konstrukcjami języka C#.

  • Pochodzi z istniejącej klasy.

  • Wywołaj konstruktor podstawowy do łańcucha.

  • Metody zastępowania należy wykonać za pomocą systemu zastępowania języka C#.

  • Umożliwia łatwe wykonywanie typowych zadań w języku Java i trudne zadania w języku Java.

  • Uwidacznianie właściwości JavaBean jako właściwości języka C#.

  • Uwidocznienie silnie typizowanego interfejsu API:

    • Zwiększ bezpieczeństwo typów.

    • Zminimalizuj błędy środowiska uruchomieniowego.

    • Uzyskiwanie funkcji IntelliSense środowiska IDE dla typów zwracanych.

    • Umożliwia korzystanie z dokumentacji podręcznej środowiska IDE.

  • Zachęcamy do eksplorowania interfejsów API w środowisku IDE:

    • Skorzystaj z alternatyw platformy, aby zminimalizować narażenie biblioteki Klaslib języka Java.

    • Uwidacznianie delegatów języka C# (lambd, metod anonimowych i System.Delegate) zamiast interfejsów jedno method, jeśli jest to odpowiednie i odpowiednie.

    • Podaj mechanizm wywoływania dowolnych bibliotek Java ( Android.Runtime.JNIEnv).

Zestawy

Platforma Xamarin.Android zawiera szereg zestawów, które stanowią profil MonoMobile. Strona Zestawy zawiera więcej informacji.

Powiązania z platformą Android są zawarte w Mono.Android.dll zestawie. Ten zestaw zawiera całe powiązanie do korzystania z interfejsów API systemu Android i komunikowania się z maszyną wirtualną środowiska uruchomieniowego systemu Android.

Projekt powiązania

Kolekcje

Interfejsy API systemu Android intensywnie wykorzystują kolekcje java.util do udostępniania list, zestawów i map. Uwidaczniamy te elementy przy użyciu interfejsów System.Collections.Generic w naszym powiązaniu. Podstawowe mapowania to:

Udostępniliśmy klasy pomocnika, aby ułatwić szybsze przeprowadzanie bez kopiowania tych typów. Jeśli to możliwe, zalecamy użycie tych udostępnionych kolekcji zamiast implementacji dostarczonej przez platformę, na przykład List<T> lub Dictionary<TKey, TValue>. Implementacje Android.Runtime wykorzystują natywną kolekcję Java wewnętrznie i dlatego nie wymagają kopiowania do i z kolekcji natywnej podczas przekazywania do elementu członkowskiego interfejsu API systemu Android.

Można przekazać dowolną implementację interfejsu do metody systemu Android akceptującej ten interfejs, np. przekazać element List<int> do konstruktora ArrayAdapter<int(Context, int>, IList<int>). Jednak w przypadku wszystkich implementacji z wyjątkiem implementacji Android.Runtime obejmuje to skopiowanie listy z maszyny wirtualnej Mono do maszyny wirtualnej środowiska uruchomieniowego systemu Android. Jeśli lista zostanie później zmieniona w środowisku uruchomieniowym systemu Android (np. przez wywołanie arrayAdapter<T>. Metoda Add(T) — te zmiany nie będą widoczne w kodzie zarządzanym. Gdyby użyto JavaList<int> elementu, te zmiany byłyby widoczne.

Powtórzone implementacje interfejsów kolekcji, które niejedną z powyższych wymienionych klas pomocnikówtylko marshaling [In]:

// This fails:
var badSource  = new List<int> { 1, 2, 3 };
var badAdapter = new ArrayAdapter<int>(context, textViewResourceId, badSource);
badAdapter.Add (4);
if (badSource.Count != 4) // true
    throw new InvalidOperationException ("this is thrown");

// this works:
var goodSource  = new JavaList<int> { 1, 2, 3 };
var goodAdapter = new ArrayAdapter<int> (context, textViewResourceId, goodSource);
goodAdapter.Add (4);
if (goodSource.Count != 4) // false
    throw new InvalidOperationException ("should not be reached.");

Właściwości

Metody języka Java są przekształcane w właściwości, gdy są to odpowiednie:

  • Para T getFoo() metod Języka Java i void setFoo(T) są przekształcane w Foo właściwość . Przykład: Activity.Intent.

  • Metoda getFoo() Java jest przekształcana w właściwość Foo tylko do odczytu. Przykład: Context.PackageName.

  • Właściwości tylko do ustawiania nie są generowane.

  • Właściwości niegenerowane, jeśli typ właściwości będzie tablicą.

Zdarzenia i odbiorniki

Interfejsy API systemu Android są oparte na języku Java, a jego składniki są zgodne ze wzorcem języka Java na potrzeby podłączania odbiorników zdarzeń. Ten wzorzec zwykle jest kłopotliwy, ponieważ wymaga od użytkownika utworzenia klasy anonimowej i zadeklarowania metod zastąpienia, na przykład w jaki sposób można wykonać elementy w systemie Android za pomocą języka Java:

final android.widget.Button button = new android.widget.Button(context);

button.setText(this.count + " clicks!");
button.setOnClickListener (new View.OnClickListener() {
    public void onClick (View v) {
        button.setText(++this.count + " clicks!");
    }
});

Równoważny kod w języku C# używający zdarzeń to:

var button = new Android.Widget.Button (context) {
    Text = string.Format ("{0} clicks!", this.count),
};
button.Click += (sender, e) => {
    button.Text = string.Format ("{0} clicks!", ++this.count);
};

Należy pamiętać, że oba powyższe mechanizmy są dostępne w środowisku Xamarin.Android. Interfejs odbiornika można zaimplementować i dołączyć go za pomocą elementu View.SetOnClickListener lub dołączyć delegata utworzonego za pomocą dowolnego ze zwykłych paradygmatów języka C# do zdarzenia Click.

Gdy metoda wywołania zwrotnego odbiornika zwraca wartość void, tworzymy elementy interfejsu API na podstawie delegata TEventArgs> procedury obsługi<zdarzeń. Generujemy zdarzenie podobne do powyższego przykładu dla tych typów odbiorników. Jeśli jednak wywołanie zwrotne odbiornika zwraca wartość niepustą i nielogiczną, zdarzenia i procedury obsługi zdarzeń nie są używane. Zamiast tego generujemy określonego delegata dla podpisu wywołania zwrotnego i dodajemy właściwości zamiast zdarzeń. Przyczyną jest radzenie sobie z kolejnością wywołania delegata i obsługą zwrotu. To podejście odzwierciedla czynności wykonywane za pomocą interfejsu API platformy Xamarin.iOS.

Zdarzenia lub właściwości języka C# są generowane automatycznie tylko wtedy, gdy metoda rejestracji zdarzeń systemu Android:

  1. set Ma prefiks, np. ustawienieOnClickListener.

  2. Ma typ zwracany void .

  3. Akceptuje tylko jeden parametr, typ parametru jest interfejsem, interfejs ma tylko jedną metodę, a nazwa interfejsu kończy się na Listener , np. View.OnClick Listener.

Ponadto jeśli metoda interfejsu odbiornika ma zwracany typ wartości logicznej zamiast void, wygenerowana podklasa EventArgs będzie zawierać właściwość Obsłużona. Wartość właściwości Handled jest używana jako wartość zwracana dla metody Listener i domyślnie ma wartość true.

Na przykład metoda Android View.setOnKeyListener() akceptuje interfejs View.OnKeyListener , a metoda View.OnKeyListener.onKey(View, int, KeyEvent) ma typ zwracany przez wartość logiczną. Platforma Xamarin.Android generuje odpowiednie zdarzenie View.KeyPress, które jest widokiem programu EventHandler.KeyEventArgs<>. Klasa KeyEventArgs z kolei ma właściwość View.KeyEventArgs.Handled, która jest używana jako wartość zwracana dla metody View.OnKeyListener.onKey().

Zamierzamy dodać przeciążenia dla innych metod i ctors, aby uwidocznić połączenie oparte na delegatach. Ponadto odbiorniki z wieloma wywołaniami zwrotnymi wymagają dodatkowej inspekcji w celu ustalenia, czy wdrożenie poszczególnych wywołań zwrotnych jest uzasadnione, więc konwertujemy je w miarę ich identyfikowania. Jeśli nie ma odpowiedniego zdarzenia, odbiorniki muszą być używane w języku C#, ale zwróć uwagę, aby można było delegować użycie. Wykonaliśmy również pewne konwersje interfejsów bez sufiksu "Odbiornik", gdy było jasne, że skorzystają z alternatywnej alternatywy delegata.

Wszystkie interfejsy odbiorników implementują element Android.Runtime.IJavaObject interfejs, ze względu na szczegóły implementacji powiązania, więc klasy odbiornika muszą zaimplementować ten interfejs. Można to zrobić przez zaimplementowanie interfejsu odbiornika w podklasie Java.Lang.Object lub dowolnego innego opakowanego obiektu Java, takiego jak działanie systemu Android.

Elementy runnable

Język Java korzysta z interfejsu java.lang.Runnable w celu zapewnienia mechanizmu delegowania. Klasa java.lang.Thread jest godnym uwagi użytkownikiem tego interfejsu. System Android zastosował również interfejs w interfejsie API. Activity.runOnUiThread() i View.post() są godnymi uwagi przykładami.

Interfejs Runnable zawiera pojedynczą metodę void, run(). W związku z tym nadaje się do powiązania w języku C# jako delegata System.Action . Udostępniliśmy przeciążenia w powiązaniu, które akceptują Action parametr dla wszystkich elementów członkowskich interfejsu API, które używają Runnable elementu w natywnym interfejsie API, np. Activity.RunOnUiThread() i View.Post().

Pozostawiliśmy przeciążenia IRunnable zamiast ich zastępowania, ponieważ kilka typów implementuje interfejs i dlatego można je przekazać bezpośrednio jako elementy runnable.

Klasy wewnętrzne

Język Java ma dwa różne typy zagnieżdżonych klas: klasy zagnieżdżone statyczne i klasy niestatyczne.

Statyczne klasy zagnieżdżone języka Java są identyczne z typami zagnieżdżonych języka C#.

Niestatyczne klasy zagnieżdżone, nazywane również klasami wewnętrznymi, są znacznie różne. Zawierają niejawne odwołanie do wystąpienia otaczającego typu i nie mogą zawierać statycznych elementów członkowskich (między innymi różnic poza zakresem tego przeglądu).

Jeśli chodzi o powiązanie i użycie języka C#, zagnieżdżone klasy statyczne są traktowane jako normalne zagnieżdżone typy. Klasy wewnętrzne mają w międzyczasie dwie znaczące różnice:

  1. Niejawne odwołanie do typu zawierającego musi być jawnie podane jako parametr konstruktora.

  2. Podczas dziedziczenia z klasy wewnętrznej klasa wewnętrzna musi być zagnieżdżona w obrębie typu dziedziczącego z typu zawierającego klasę wewnętrzną, a typ pochodny musi dostarczyć konstruktora tego samego typu, co typ zawierający język C#.

Rozważmy na przykład klasę wewnętrzną Android.Service.Wallpaper.WallpaperService.Engine . Ponieważ jest to klasa wewnętrzna, konstruktor WallpaperService.Engine() przyjmuje odwołanie do wystąpienia WallpaperService (porównanie i kontrast z konstruktorem Java WallpaperService.Engine(), który nie przyjmuje parametrów.

Przykładem wyprowadzenia klasy wewnętrznej jest CubeWallpaper.CubeEngine:

class CubeWallpaper : WallpaperService {
    public override WallpaperService.Engine OnCreateEngine ()
    {
        return new CubeEngine (this);
    }

    class CubeEngine : WallpaperService.Engine {
        public CubeEngine (CubeWallpaper s)
                : base (s)
        {
        }
    }
}

Zwróć uwagę, że funkcja CubeWallpaper.CubeEngine jest zagnieżdżona w obiekcie CubeWallpaperCubeWallpaper dziedziczy z klasy zawierającej WallpaperService.Engineelement i CubeWallpaper.CubeEngine ma konstruktor, który przyjmuje typ deklaratora — CubeWallpaper w tym przypadku — wszystko zgodnie z powyższym opisem.

Interfejsy

Interfejsy języka Java mogą zawierać trzy zestawy elementów członkowskich, z których dwa powodują problemy z języka C#:

  1. Metody

  2. Typy

  3. Pola

Interfejsy języka Java są tłumaczone na dwa typy:

  1. Interfejs (opcjonalny) zawierający deklaracje metod. Ten interfejs ma taką samą nazwę jak interfejs Java, z wyjątkiem prefiksu " I ".

  2. (opcjonalnie) klasa statyczna zawierająca dowolne pola zadeklarowane w interfejsie Języka Java.

Typy zagnieżdżone są "przeniesione" jako elementy równorzędne otaczającego interfejsu zamiast typów zagnieżdżonych, a nazwa otaczającego interfejsu jest prefiksem.

Rozważmy na przykład interfejs android.os.Parcelable . Interfejs Parcelable zawiera metody, typy zagnieżdżone i stałe. Metody interfejsu Parcelable są umieszczane w interfejsie Android.OS.IParcelable. Stałe interfejsu parcelowalnego są umieszczane w typie Android.OS.ParcelableConsts. Zagnieżdżone typy android.os.Parcelable.ClassLoaderCreator<T> i android.os.Parcelable.Creator<T> nie są obecnie powiązane z powodu ograniczeń w obsłudze typów ogólnych; jeśli były obsługiwane, będą one obecne jako interfejsy Android.OS.IParcelableClassLoaderCreator i Android.OS.IParcelableCreator. Na przykład zagnieżdżony interfejs android.os.IBinder.DeathRecipient jest powiązany jako interfejs Android.OS.IBinderDeathRecipient .

Uwaga

Począwszy od platformy Xamarin.Android 1.9, stałe interfejsu Java są duplikowane w celu uproszczenia przenoszenia kodu Java. Pomaga to ulepszyć przenoszenie kodu Java, który opiera się na stałych interfejsu dostawcy systemu Android.

Oprócz powyższych typów istnieją cztery dalsze zmiany:

  1. Typ o takiej samej nazwie jak interfejs Java jest generowany w celu zawierania stałych.

  2. Typy zawierające stałe interfejsu zawierają również wszystkie stałe pochodzące z zaimplementowanych interfejsów Języka Java.

  3. Wszystkie klasy, które implementują interfejs Języka Java zawierający stałe, otrzymują nowy zagnieżdżony typ InterfaceConsts, który zawiera stałe ze wszystkich zaimplementowanych interfejsów.

  4. Typ consts jest teraz przestarzały.

W przypadku interfejsu android.os.Parcelable oznacza to, że będzie teraz typ Android.OS.Parcelable zawierający stałe. Na przykład stała Parcelable.CONTENTS_FILE_DESCRIPTOR będzie powiązana jako stała Parcelable.ContentsFileDescriptor, a nie jako stała ParcelableConsts.ContentsFileDescriptor.

W przypadku interfejsów zawierających stałe, które implementują inne interfejsy zawierające jeszcze więcej stałych, jest teraz generowany związek wszystkich stałych. Na przykład interfejs android.provider.MediaStore.VideoColumns implementuje interfejs android.provider.MediaStore.MediaColumns. Jednak przed wersją 1.9 typ Android.Provider.MediaStore.VideoColumnsConsts nie ma możliwości uzyskania dostępu do stałych zadeklarowanych w systemie Android.Provider.MediaStore.MediaColumnsConsts. W związku z tym wyrażenie Języka Java MediaStore.VideoColumns.TITLE musi być powiązane z wyrażeniem języka C# MediaStore.Video.MediaColumnsConsts.Title , które jest trudne do odnalezienia bez czytania wielu dokumentacji języka Java. W wersji 1.9 równoważne wyrażenie języka C# będzie mieć wartość MediaStore.Video.VideoColumns.Title.

Ponadto należy wziąć pod uwagę typ android.os.Bundle , który implementuje interfejs Java Parcelable . Ponieważ implementuje interfejs, wszystkie stałe w tym interfejsie są dostępne "za pośrednictwem" typu pakietu, np. Bundle.CONTENTS_FILE_DESCRIPTOR jest idealnie prawidłowym wyrażeniem języka Java. Wcześniej, aby przemieścić to wyrażenie w języku C#, należy przyjrzeć się wszystkim zaimplementowaniom interfejsów, aby zobaczyć, z którego typu pochodzi CONTENTS_FILE_DESCRIPTOR . Począwszy od platformy Xamarin.Android 1.9, klasy implementowania interfejsów Java, które zawierają stałe, będą miały zagnieżdżony typ InterfaceConsts , który będzie zawierać wszystkie dziedziczone stałe interfejsu. Umożliwi to tłumaczenie Bundle.CONTENTS_FILE_DESCRIPTOR na bundle.InterfaceConsts.ContentsFileDescriptor.

Na koniec typy z sufiksem Consts , takie jak Android.OS.ParcelableConsts , są teraz przestarzałe, inne niż nowo wprowadzone typy InterfaceConsts zagnieżdżone. Zostaną one usunięte w środowisku Xamarin.Android 3.0.

Zasoby

Obrazy, opisy układów, binarne obiekty blob i słowniki ciągów mogą być dołączane do aplikacji jako pliki zasobów. Różne interfejsy API systemu Android są przeznaczone do obsługi identyfikatorów zasobów zamiast bezpośrednio obsługiwać obrazy, ciągi lub binarne obiekty blob.

Na przykład przykładowa aplikacja systemu Android zawierająca układ interfejsu użytkownika ( ), ciąg tabeli międzynarodowych ( main.axmlstrings.xml) i niektóre ikony ( drawable-*/icon.png) będą przechowywać swoje zasoby w katalogu "Resources" aplikacji:

Resources/
    drawable-hdpi/
        icon.png

    drawable-ldpi/
        icon.png

    drawable-mdpi/
        icon.png

    layout/
        main.axml

    values/
        strings.xml

Natywne interfejsy API systemu Android nie działają bezpośrednio z nazwami plików, ale zamiast tego działają na identyfikatorach zasobów. Podczas kompilowania aplikacji systemu Android korzystającej z zasobów system kompilacji spakuje zasoby do dystrybucji i wygeneruje klasę o nazwie Resource zawierającą tokeny dla każdego z uwzględnionych zasobów. Na przykład w przypadku powyższego układu Zasobów jest to, co klasa języka R uwidacznia:

public class Resource {
    public class Drawable {
        public const int icon = 0x123;
    }

    public class Layout {
        public const int main = 0x456;
    }

    public class String {
        public const int first_string = 0xabc;
        public const int second_string = 0xbcd;
    }
}

Następnie należy odwołać Resource.Drawable.icon się do drawable/icon.png pliku lub Resource.Layout.main odwołać layout/main.xml się do pliku albo Resource.String.first_string odwołać się do pierwszego ciągu w pliku values/strings.xmlsłownika .

Stałe i wyliczenia

Natywne interfejsy API systemu Android mają wiele metod, które przyjmują lub zwracają int, które muszą być mapowane na stałe pole, aby określić, co oznacza int. Aby użyć tych metod, użytkownik musi zapoznać się z dokumentacją, aby sprawdzić, które stałe są odpowiednimi wartościami, które są mniejsze niż idealne.

Rozważmy na przykład element Activity.requestWindowFeature(int featureID).

W takich przypadkach staramy się grupować powiązane stałe razem w wyliczenie platformy .NET i mapować ponownie metodę, aby zamiast tego stosować wyliczenie. Dzięki temu możemy zaoferować wybór funkcji IntelliSense potencjalnych wartości.

Powyższy przykład staje się: Activity.RequestWindowFeature(WindowFeatures featureId).

Należy pamiętać, że jest to bardzo ręczny proces, aby ustalić, które stałe należą do siebie i które interfejsy API używają tych stałych. Zgłoś błędy dla jakichkolwiek stałych używanych w interfejsie API, które byłyby lepiej wyrażone jako wyliczenie.