W poprzednim wpisie wyjaśniłam Wam, do czego może nam się przydać Zenject podczas tworzenia projektów w Unity i dlaczego warto go stosować. W kolejnych artykułach pokażę, z jakich elementów się składa i jakie są najlepsze praktyki korzystania z niego.
Zenjecta pobieramy stąd i instalujemy jako UnityPackage (wystarczy dwa razy kliknąć mają otwarty projekt). Na stronie projektu Github znajdziemy szczegółową dokumentację całego pluginu. Powiem szczerze, że może ona wydać się przytłaczająca na pierwszy (i drugi…) rzut oka. Ja skupię się na tych konkretnych funkcjonalnościach, z których najczęściej korzystam w pracy.
Na początek zapamiętajcie jedno:
Container <- Contexts <- Installer <- Bind(ing)
Czyli np. powiązanie interfejsu z jego implementacją to jest binding, bindingi definiowane są w instalatorach, instalatory podpinane są do kontekstów, a aktywne dla wszystkich kontekstów bindingi trzymane są w kontenerze. Kontener również służy do „resolvowania” (rozwiązywania?) zależności – czyli dana klasa mówi „kochany systemie ja potrzebuję do działa obiektu z interfejsem IXyz i configa XyzConfig” a kontener na to „oho, widzę, że interfejs IXyz jest zbindowany do klasy CoolXyz jako singleton, to przekażę ten obiekt i jeszcze do tego widzę, że jako instancja został zbindowany obiekt z konfiguracją XyzConfig, to go też przekażę”.
Konteksty
Konteksty pozwalają nam na definiowanie miejsc, w których możemy dodawać elementy do kontenera. Będziemy używać dwóch rodzajów kontekstów:
- SceneContext – wystarczy dodać go na scenie, aby nasze MonoBehavioury miały wstrzykiwane zależności! Na scenie tworzymy nowy pusty obiekt, nazywamy go na przykład „Context” i dodajemy do niego skrypt „SceneContexts”:

Służy on do definiowania instalatorów za pomocą ScriptableObject bądź podpiętych MonoBehaviourów. Dzięki temu możemy też tworzyć instalatory, które wymagają jakichś obiektów Unity – np. prefaba, obiektu ze sceny czy ScriptableObjecta. Różnym typom instalatorów przyjrzymy się w dalszej części artykułu.
- ProjectContext – w nim definiujemy instalatory globalne, dla wszystkich scen. ProjectContext załaduje się tylko raz, przy starcie aplikacji.
Ma to swoje plusy i minusy. Plusem jest to, że obliczenia związanie ze spinaniem kontenera wykonujemy tylko raz i są one niezmienne przez cały czas działania aplikacji. Nie musimy też martwić się, że w którymś skrypcie użyjemy klasy instalowanej w instalatorze znajdującym się na innej scenie. Minusem jest to, że instalowanie wielu instalatorów potrafi być naprawdę obliczeniożerne – Zenject musi stworzyć drzewo zależności i wstrzykiwać je w obiekty w odpowiedniej kolejności. Dlatego zbyt duża ilość instalatorów (a raczej bindingów) spowolni nam znacznie czas wczytywania się aplikacji, a zbyt długo widoczny loading screen może zaowocować szybką stratą użytkowników.
Aby dodać ProjectContext do naszego projektu, musimy w folderze Resources stworzyć odpowiedni prefab. W tym celu klikamy prawym przyciskiem myszy na odpowiednim katalogu i wybieramy Create -> Zenject -> ProjectContext. Zauważmy, że prefab ma przypięty skrypt bardzo podobny do SceneContextu. I tak, to wszystko – Zenject automagicznie odpali installery dodane do tego prefaba. 🙂
Podsumowując – to co przypniemy pod SceneContext będzie działało tylko wtedy, gdy będziemy mieli aktualnie załadowaną tę scenę, natomiast ProjectContext instaluje zależności raz, globalnie, dla całej aplikacji. W przypadku, gdy używamy addytywnego ładowania scen i posiadamy jakąś scenę np. Core, która jest ciągle aktywna, jej kontekst może nam zastąpić ProjectContext.
Instalatory
Instalatory są miejscem, w którym definiujemy nasze bindingi. Ja najczęściej korzystam z następujących:
- Installer – zwykła klasa instalatora, możemy jej używać do grupowania bindingów tak, by powiązane ze sobą funkcjonalności definiowane były w oddzielnym miejscu.
- MonoInstaller – jest to instalator, który możemy przypiąć do GameObjectu. Zazwyczaj mamy co najmniej jeden MonoInstaller na scenie – np. MainSceneInstaller, w którym instalujemy „zwykłe” Installery funkcjonalności najwyższego poziomu. MonoInstallery przydają się też do bindowania skryptów znajdujących się na scenie – np. jakiś PopUpManager, który z jednej strony musi być na scenie, żeby włączać Canvas z okienkiem, a z drugiej strony fajnie jakby był dostępny poprzez wstrzykiwanie do klas działających wewnątrz naszych systemów – na przykład wyświetlenie popupu „Please wait” w trakcie wysyłania zapytania HTTP bądź ładowania reklamy.
- ScriptableObjectInstaller – instalator, który przechowujemy jak ScriptableObject. Możemy w nim sobie przypinać na przykład prefaby bądź inne ScriptableObjecty. Najczęstszym zastosowaniem takiego instalatora jest właśnie przekazanie do naszych systemów logiki konfiguracji przechowywanej w postaci ScriptableObject. Dzięki temu zyskujemy jedno, konkretne miejsce w którym możemy w razie czego łatwo podmienić konfigurację (na przykład do testów).
Dobre praktyki
Jak z każdą rzeczą na tym świecie, Dependency Injection jest świetnym narzędziem, o ile jest użyte poprawnie. W złych rękach potrafi zamienić utrzymanie każdego projektu w koszmar.
Jeśli chodzi o używanie instalatorów, kluczowa jest ich dobra organizacja. Przepis na porażkę jest taki:
- Definiuj bindingi w losowych instalatorach, bez dzielenia na podinstalatory
- Posiadaj koszmarnie długie klasy instalatorów, w których panuje bałagan
- Podpinaj większość instalatorów pod instalator danej sceny, a jak się okaże, że jednak jest to potrzebne w kilku miejscach, to przenieś do ProjectContextu
- Profit – gdy dochodzi nowa klasa do podpięcia, nie wiadomo w którym instalatorze ją zbindować, a jak już znajdziemy niezłe miejsce to okazuje się, że jakaś inna klasa jest nieobecna w danych kontekście, więc przenosimy bindowanie również tamtej klasy, więc mamy wyjątek „Zenject exception – zenject nie mógł stworzyć instancji danej klasy, gdyż nie udało się znaleźć odpowiedniego bindingu” w 5 innych klasach…
- Jeśli deadline jest na wczoraj, zapewne skończy się to wrzuceniem prawie wszystkiego w ProjectContext i wydłużeniem czasu ładowania się apki (i kosmicznym bałaganem…)
Ufff, koszmarek, naprawdę.
Na dobry początek starajmy się tworzyć instalator dla każdej funkcjonalności, a potem grupujmy instalatory powiązanych ze sobą funkcjonalności w kolejnych instalatorach. Dobra, przemyślana organizacja (stosowana od samego początku!) pomoże nam uchronić się od opisanego wyżej scenariusza.
Nowy programista, widząc instalator najwyższego poziomu, w którym są instalowanie tylko i wyłącznie inne instalatory (brakuje definicji bindingów) zastanowi się pięć razy, zanim „na pałę” wrzuci gdzieś luźny binding. Dzięki logicznej hierarchii powinno być też dużo łatwiej znaleźć miejsce na zbindowanie naszych nowych klas.
W kolejnym artykule przybliżę Wam różne rodzaje bindingów oraz sposoby wstrzykiwania ich do klas.
Ciężko się pisze o tym po polsku 🙂 Jeśli coś jest dla Was niezrozumiałe, dawajcie znać w komentarzach, postaram się doprecyzować!
1 thought on “Zenject in depth cz.2 – Contexts & Installers”