Jakie technologie do napisania aplikacji na Androida?

0

Hej, nie klepałem nic na Androida przez 2 lata.

Ktoś aktualny w temacie mógłby wypowiedzieć się na temat obecnie używanych architektur w apkach ? Wcześniej stosowałem zwyczajne MVP i było ok. Jak to jest teraz ? Kumpel w robocie stosuje MVVM i jakieś liby do bindowania zależności, Daggery itd. Nie chciałbym za dużo czasu poświęcać na naukę nowych technologii na Andka, chcę to po prostu jak najszybciej w miarę sensownie naklepać. Apka to ma być 4-5 prostych ekranów. Proste use casy, zwykłe uderzenia do resta bez większej logiki, logowanie przez fb/google/customowo.

Widoki się robi nadal w xmlu z łapy czy jakieś lepsze podejście ?

W sumie to chciałbym też wypuścić to na iOS .. może lepiej react ?

Pozdro

3

Czy piszesz coś konkretnego,czy na razie sobie odświeżasz temat i się bawisz?

chciałbym też wypuścić to na iOS .. może lepiej react

A rzuć okiem na Fluttera. Nie namawiam na siłę, ale parę osób które znam (także z 4P) i które siedzą w React'cie, po pierwszym zetknięciu z Flutterem były trochę zmieszane, ale po chwili przyznały, że jest całkiem OK. Sam się obecnie nim bawię, ale że to mój pierwszy poważniejszy kontakt z mobilkami, za bardzo nie mam porównania. Niemniej mam właśnie apkę podobną do tego, czego Ty potrzebujesz - kilka prostych widoków, między którymi się przełączasz - poszło w miarę bezproblemowo. Znaczy - trochę z tym walczyłem, bo jak pisałem - uczyłem się tego od zera, ale obecnie to takie coś w jeden wieczór bym ogarnął.

2

Jak prosta aplikacja i mało kodu to może jednak lepiej natywnie oddzielnie 2 zrobić niż bawić się w cross technologie. Najwięcej tutków i sprawdzonych rozwiązań do szybkiego ogarnięcia. Na Androidzie widoki w xmlu tylko już używa się więcej ConstraintLayoutów zamiast zagnieżdzonych RelativeLayoutów/LinearLayoutów + jest już komponent, który przeżywa obrót ekranu i można w nim spokojnie pisać logikę z requestem czyli ViewModel i przekazywanie danych do widoku przez LiveDaty. Ogólnie teraz na Androidzie pisze się w Kotlinie (jak znasz Javę to przeszkoczysz w kilka dni, bo API Javowe jest w pełni dostępne), gdzie masz Koina, który jest uproszczonym Daggerem, do zapytań sieciowych to standardowo OkHttp, Retrofit, RxJava 2/courotines

0

@cerrato: Piszę coś konkretnego i możesz zostać moim specem od marketingu haha :D

@viader

Dzięki. Piszę w Kotlinie backend także no-problem. Nie kumam o co chodzi Ci z tym komponentem do obrotu ekranu ? Nie ma tu się obracać ekran w tej apce. Tego Koina warto użyć ? Architektura z ViewModel chyba też mi obca. Masz jakieś simple projecty / tutki ? Korutyny słyszałem, że ciekawe. Warto zamiast Retrofita ?

1

Z obrotem ekranu chodzi niekoniecznie o obrót ekranu, tylko o zmianę konfiguracji aplikacji (np. obrót ekranu) i przetrwanie w tym czasie, jak widok jest ubity, takich rzeczy jak zapytanie internetowe itp.

Przykładowa apka w MVVM - https://github.com/chrisbanes/tivi

Korutyny są ortogonalne do Retrofita. Możesz używać korutyn w Retroficie. Moim zdaniem warto.

0
Bambo napisał(a):

Tego Koina warto użyć ?

Do prostych aplikacji jest całkiem poręczny, ale nie nazwałbym go "uproszczonym Daggerem", bo to nie jest Dependency Injection, tylko typowy service locator (ze sporą ilością Kotlinowego cukru składniowego). W przypadku aplikacji na parę ekranów można się zastanawiać, czy nawet ten Koin jest nam w ogóle potrzebny. (Jest też kilka innych alternatyw, jeśli chcemy ominąć Daggera - Kodein, Toothpick. Nie ma powodu traktować akurat Koina jako jakiś złoty standard.)

0

Ok poczytałem trochę sobie blogów o ViewModelach, korutynach itd.

Chciałbym to wszystko ograć beż żadnego frameworka DI - Daggera, Koina itd - apka mi się po prostu wydaje prosta.

Zastanawiam się tylko nad architekturą trochę i wstrzykiwaniem repozytoriów do ViewModeli oraz Api od retrofita do repozytoriów.. Najprostsze rozwiązania jakie widziałem to robienie RetrofitFactory jaki singletonu i zwracanie z niego po prostu api. Czytałem tylko, ze tu może być problem z memory leakami. Tak samo ludzie robili repo, że tworzyli singletony i tak używali w ViewModelach.

Jakieś lepsze sposoby na ogranie tego ręcznie bez DI frameworków ? Może gdzieś to spiąć w App ? Customowe fabryki do ViewModeli i refleksja ?

Pozdrawiam

1

Przykład aplikacji z DI bez żadnych bibliotek do tego - https://github.com/handstandsam/ShoppingApp. Nie ma tam tylko żadnej warstwy prezentacji w stylu ViewModel.

Tak jak napisałeś, najlepsze co możesz zrobić, to spiąć to wszystko w onCreate() klasy Application i potem dobierać się do tego z Activity, itp. Jeśli koniecznie chcesz korzystać z ViewModel, to musisz stworzyć własną fabrykę i korzystać z niej w ViewModelProviders.of(), żeby przekazywać parametry do konstruktorów.

0

@Michał Sikora:

Dzięki. A co proponujesz poza ViewModelami ? czy to nie jest obecnie takie topowe podejście ? Myślałem, że MVP już dawno odeszło.

0
Bambo napisał(a):

Dzięki. A co proponujesz poza ViewModelami ? czy to nie jest obecnie takie topowe podejście ?

ViewModel pewnie jest obecnie najbardziej popularnym podejściem. Mi się to rozwiązanie nie podoba i skorzystałbym z własnych klas i onRetainNonConfigurationInstance/onRetainCustomNonConfigurationInstance. W zasadzie z tego mechanizmu korzysta też ViewModel tylko dodaje dodatkowy narzut w postaci dodatkowego cyklu życia, wymuszonego dziedziczenia i korzystania z service locatora w oparciu o typy. Aczkolwiek w większości projektów komercyjnych korzystałem z niego, bo jest to szybsze, prostsze i bardziej znajome dla większości ludzi.

Bambo napisał(a):

Myślałem, że MVP już dawno odeszło.

Zależy co dla Ciebie znaczy MVP. Takiego klasycznego, że ma się interfejs View, klasę Presenter i one gadają miedzy sobą wywołując nawzajem na sobie jakieś metody, to faktycznie już raczej nie używa.

I teraz tak… ja tylko nie lubię tej biblioteki - https://developer.android.com/topic/libraries/architecture/viewmodel. Bo jak najbardziej jestem za jednokierunkowym przepływem danych i nasłuchiwaniem w widoku na dane (view model) z warstwy prezentacji. A że Google postanowił sobie nazwać prezenter ViewModel i wprowadzać zamieszanie, to już ich sprawa.

0
Michał Sikora napisał(a):
Bambo napisał(a):

Dzięki. A co proponujesz poza ViewModelami ? czy to nie jest obecnie takie topowe podejście ?

ViewModel pewnie jest obecnie najbardziej popularnym podejściem. Mi się to rozwiązanie nie podoba i skorzystałbym z własnych klas i onRetainNonConfigurationInstance/onRetainCustomLastNonConfigurationInstance. W zasadzie z tego mechanizmu korzysta też ViewModel tylko dodaje dodatkowy narzut w postaci dodatkowego cyklu życia, wymuszonego dziedziczenia i korzystania z service locatora w oparciu o typy. Aczkolwiek w większości projektów komercyjnych korzystałem z niego, bo jest to szybsze, prostsze i bardziej znajome dla większości ludzi.

Metoda onRetainNonConfigurationInstance jest deprecated od niepamiętnych czasów (API 13 czy 15).

onRetainCustomNonConfigurationInstance (i getLastCustomNonConfigurationInstance) są deprecated od listopada, z zaleceniem używania ViewModels:

onRetainCustomNonConfigurationInstance has been deprecated. Use a ViewModel for storing objects that need to survive configuration changes.

źródło

Taka jest oficjalnie zalecana praktyka. Nie ma powodu iść pod prąd i wyważać drzwi otwartych, reimplementując rozwiązania z SDK.

A że Google postanowił sobie nazwać prezenter ViewModel i wprowadzać zamieszanie, to już ich sprawa.

Architektura jest dobrze znana każdemu .NET-owcowi. ViewModel ma modelować widok, i tyle. Architecture Components pozwala tak pisać. Jeśli ktoś pakuje do viewmodelu logikę (zamiast wyekstrahować ją gdzie indziej), libka oczywiście nie będzie w stanie go przed tym powstrzymać, ale winowajcą takiego zamieszania będzie ów programista, a nie Google.

0
V-2 napisał(a):

Metoda onRetainNonConfigurationInstance jest deprecated od niepamiętnych czasów (API 13 czy 15).

Nie, nie jest.

onRetainCustomNonConfigurationInstance (a nie "custom last") jest deprecated od listopada, z zaleceniem używania ViewModels:

onRetainCustomNonConfigurationInstance has been deprecated. Use a ViewModel for storing objects that need to survive configuration changes.

źródło

Racja, jakbym był zmuszony korzystać z ComponentActivity, to faktycznie korzystałbym z metody, która jest deprecated. Dopiero jakby ją usunęli albo zrobili final, to zrobiłbym dodatkowy krok i dodał jeden dodatkowy ViewModel do przechowywania mojej konfiguracji.

Taka jest oficjalnie zalecana praktyka. Nie ma powodu iść pod prąd i wyważać drzwi otwartych, reimplementując rozwiązania z SDK.

Fragment, Loader i AsyncTask też były/są oficjalnymi zalecanymi praktykami. I też nie widzę sensu reimplementacji istniejących rozwiązań. Wolę pisać lepsze.

Architektura jest dobrze znana każdemu .NET-owcowi. ViewModel ma modelować widok, i tyle. Architecture Components pozwala tak pisać. Jeśli ktoś pakuje do viewmodelu logikę (zamiast wyekstrahować ją gdzie indziej), libka oczywiście nie będzie w stanie go przed tym powstrzymać, ale winowajcą takiego zamieszania będzie ów programista, a nie Google.

Ale to Google sam daje takie przykłady (i zarządzanie samą klasą), gdzie ViewModel ma logikę, dostęp do DAO, repozytoriów, serwisów itd. U nich ViewModel to nie jest model danych, który ma zostać zaprezentowany przez widok, tylko kontroler w warstwie prezentacji.

0
Michał Sikora napisał(a):
V-2 napisał(a):

Metoda onRetainNonConfigurationInstance jest deprecated od niepamiętnych czasów (API 13 czy 15).

Nie, nie jest.

Tu rzeczywiście, od-deprekowali ją. Choć historia tych zmian była dość burzliwa: https://www.reddit.com/r/androiddev/comments/b908fr/can_someone_explain_to_me_why_aac_is_trying_to/

Fragment, Loader i AsyncTask też były/są oficjalnymi zalecanymi praktykami. I też nie widzę sensu reimplementacji istniejących rozwiązań. Wolę pisać lepsze.

Każdy lubi być architektem i pisać lepsze :) A potem my, biedne szaraki, zostajemy w projekcie z Własną Biblioteką Michała. Którego nie da się już o nic zapytać. Ani wyguglować w necie odpowiedzi, gdy pojawi się jakiś problem z "lepszym rozwiązaniem". Bo świat zewnętrzny na oczy go nie widział.

Nie mówię teoretycznie, pracowałem z takimi ludźmi... Zamiast Retrofita zastawałem w projekcie Bibliotekę Sebastiana (skądinąd bardzo zdolnego, poszedł później pracować w jednym z gigantów - choć podejrzewam, że tam nie dostał już tyle twórczej swobody).

Fragmenty - przy wszystkich ich niedociągnięciach i słabościach - są jednak używane z powodzeniem do dziś. A gdybym chciał sięgnąć po alternatywę, to bym ograniczył się do użycia innych konstruktów natywnych (np. Activity i własne layouty). Ewentualnie sięgnął po jakieś rozwiązanie, które rzeczywiście funkcjonuje w społeczności: coś w stylu Flow & Mortar czy Conductor. A i to niechętnie. Jeśli masz lepsze rozwiązanie niż ViewModels, to - jak to się mówi - "put your money where your mouth is", i wrzuć na GitHuba jako bibliotekę. Jeśli zaoferuje konkretne przewagi nad oficjalnym (a nie tylko będące kwestią prywatnego gustu), zostanie podchwycone. Natomiast pisanie sobie własnego rozwiązania w projektowym zaciszu, to jest na dłuższą metę najgorszy możliwy wybór.

Ale to Google sam daje takie przykłady, gdzie ViewModel ma logikę, dostęp do DAO, repozytoriów, serwisów itd. U nich ViewModel to nie jest model danych, który ma zostać zaprezentowany przez widok, tylko kontroler w warstwie prezentacji.

Nieprawda. W szkieletowych przykładach może tak to wygląda, dla uproszczenia. Wystarczy jednak poczytać oficjalną dokumentację.

Nie szukając daleko, na dzień dobry na stronie https://developer.android.com/topic/libraries/architecture/viewmodel masz podlinkowany artykuł ViewModels and LiveData: Patterns + AntiPatterns, który omawia antywzorzec "Fat ViewModels", zalecając "moving some logic out to a presenter [...] adding a Domain layer", itd.

Jeśli ktoś powie ci, że przygotowuje lepsze rozwiązanie niż X - a z rozmowy wynika, że nie przeczytał głównej strony dokumentacji do X, lub w każdym razie pisałby takie rzeczy jak ktoś, kto by jej nie przeczytał - to jaki byś powziął na ten temat pogląd? :) Serio pytam

1
V-2 napisał(a):

Każdy lubi być architektem i pisać lepsze :) A potem my, biedne szaraki, zostajemy w projekcie z Własną Biblioteką Michała. Którego nie da się już o nic zapytać. Ani wyguglować w necie odpowiedzi, gdy pojawi się jakiś problem z "lepszym rozwiązaniem". Bo świat zewnętrzny na oczy go nie widział.

Gdzie ja napisałem, że w komercyjnym projekcie nie używam powszechnych rozwiązań? Bo zaznaczyłem coś przeciwnego. Że właśnie ze względu na łatwość i dostępność używam tych ze standardowych bibliotek, żeby nie mieszać za dużo.

Jeśli masz lepsze rozwiązanie niż ViewModels, to - jak to się mówi - "put your money where your mouth is", i wrzuć na GitHuba jako bibliotekę.

W przypadku ViewModeli nie trzeba żadnej biblioteki. Ale rozumiem, że nie to jest celem pytania i powiedzmy, że zamienimy w tym wypadku na bibliotekę zastępującą fragmenty. Może i mógłbym. Pewnie by wyszło tak sobie ze względu na takie czynniki jak umiejętności, przypadki brzegowe, ilość czasu, ilość osób pracujących nad biblioteką i ją testujących. Nie wiem czego to ma dowodzić. W projektach niekomercyjnych, własnych albo krótkich (gdzie byłem sam) zawsze korzystałem z alternatywnych bibliotek, które mnie nie irytowały, albo pisałem własne rozwiązania.

Jeśli zaoferuje konkretne przewagi nad oficjalnym (a nie tylko będące kwestią prywatnego gustu), zostanie podchwycone.

To akurat, już słowem dygresji, nie wydaje mi się, żeby było prawdą.

Nieprawda. W szkieletowych przykładach może tak to wygląda, dla uproszczenia. Wystarczy jednak poczytać oficjalną dokumentację.

Nie szukając daleko, na dzień dobry na stronie https://developer.android.com/topic/libraries/architecture/viewmodel masz podlinkowany artykuł ViewModels and LiveData: Patterns + AntiPatterns, który omawia antywzorzec "Fat ViewModels", zalecając "moving some logic out to a presenter [...] adding a Domain layer", itd.

Google sam ułatwia złe rozwiązania. Dodanie cyklu życia do klasy, ułatwiony dostęp do zasobów systemowych w postaci AndroidViewModel, rozszerzenia w postaci modułu SavedStateHandle. Ja już pomijam patologie, że ktoś czyta bezpośrednio z bazy danych w ViewModelu. Przykłady w praktyce:

Jedyna aplikacja, która robi rzeczy rozsądnie w ramach tej biblioteki to Tivi. W pozostałych niektóre ViewModele wyglądają ok, bo mało w ogóle mogą robić. Niektóre zależą od frameworka. Niektóre robią milion rzeczy od dużej ilości logiki po zarządzanie wątkami.

Jeśli ktoś powie ci, że przygotowuje lepsze rozwiązanie niż X - a z rozmowy wynika, że nie przeczytał głównej strony dokumentacji do X, lub w każdym razie pisałby takie rzeczy jak ktoś, kto by jej nie przeczytał - to jaki byś powziął na ten temat pogląd? :) Serio pytam

Nie wiem czy chcesz mi tendencyjnie zarzucić, że piszę jakbym nie czytał dokumentacji. Ale odpowiadając - pewnie miałbym wątpliwości, co do takiej osoby.

0

Dziękuję serdecznie za odpowiedzi. Udało mi się mniej więcej wszystko ze sobą ograć. Zastanawia mnie jedynie połączenie korutyn i live daty.

Bo architekturę mam mniej więcej taką:

LiveModele są "głupie" i zawierają tylko model widoku. Do nich wstrzykuję jakieś Serwisy udostępniające dane i wykonujące logikę. Do Serwisów tych wstrzykuję po prostu Api retrofita.

I teraz tak, dla przykładu załóżmy, że omawiamy pobieranie danych Usera.

Retrofit zatem ma taką metodę:

suspend fun getUser(): User

W serwisie opakuję to w try catch i wrapuję w jakiś ServiceReponse<T>, który dodatkowo zawiera info o błędzie.

Pytanie teraz czy Serwisy mają zwracać LiveData<ServiceResponse<User>>> czy suspend <ServiceResponse<User>> czy jeszcze co innego ?

Gdzieś muszę się pozbyć tego suspend ?

0

suspend ostatecznie używasz w jakimś zakresie korutyn. Warto obejrzeć tę prezentację od Romana. Plus uzupełnienie, bo w prezentacji jest błąd - https://medium.com/@elizarov/deadlocks-in-non-hierarchical-csp-e5910d137cc.

Co do kodu, to koncepcyjnie mogłoby to tak wyglądać. Lepiej by było opakować te kilka LiveData w jedną, ale już mi się nie chciało na potrzeby przykładu.

class AuthActivity : AppCompatActivity() {
  override fun onCreate(inState: Bundle?) {
    super.onCreate(savedInstanceState)
    val viewModel = ViewModelProviders
      .of(this, AuthViewModelFactory)
      .get(AuthViewModel::class.java)
    setContentView(R.layout.activity_main)

    val emailField = findViewById<EditText>(R.id.email)
    val passwordField = findViewById<EditText>(R.id.password)
    val error = findViewById<TextView>(R.id.error)
    val signInButton = findViewById<Button>(R.id.signInButton)

    viewModel.isSigningIn.observe(this, Observer { isSigning ->
      signInButton.isEnabled = !isSigning
    })

    viewModel.authError.observe(this, Observer { error.text = it })

    viewModel.user.observe(this, Observer { user ->
      if (user != null) TODO("Navigate to another screen")
    })

    signInButton.setOnClickListener {
      val email = "${emailField.text}"
      val password = "${passwordField.text}"
      viewModel.signIn(SignInRequest(email, password))
    }
  }
}

sealed class Result<T>
data class Failure<T>(val reason: String) : Result<T>()
data class Success<T>(val value: T) : Result<T>()

data class User(val name: String, val age: Int)

data class SignInRequest(val email: String, val password: String) {
  fun hasInvalidCredentials(): Boolean = email.isBlank() || password.isBlank()
}

interface AuthService {
  suspend fun signIn(request: SignInRequest): Result<User>
}

class AuthViewModel(private val authService: AuthService) : ViewModel(), CoroutineScope {
  private val job = Job()
  override val coroutineContext get() = job

  private val _isSigningIn = MutableLiveData<Boolean>()
  val isSigningIn: LiveData<Boolean> get() = _isSigningIn

  private val _authError = MutableLiveData<String?>()
  val authError: LiveData<String?> get() = _authError

  private val _user = MutableLiveData<User?>()
  val user: LiveData<User?> get() = _user

  fun signIn(request: SignInRequest) {
    _authError.postValue(null)

    val invalidCredentials = request.hasInvalidCredentials()
    if (invalidCredentials) {
      _authError.postValue("Email or password cannot be blank!")
      return
    }

    launch(context = coroutineContext) {
      _isSigningIn.postValue(true)
      val response = authService.signIn(request)
      _isSigningIn.postValue(false)

      when (response) {
        is Failure -> _authError.postValue(response.reason)
        is Success -> _user.postValue(response.value)
      }
    }
  }

  override fun onCleared() = job.cancel()
}
0

ViewModel to jest słowo z wieloma znaczeniami:

  1. ViewModel w MVVM jako komponent sterujący widokiem
  2. ViewModel w Architecture Components
  3. ViewModel w podejściu z bindingami w xmlu gdzie trzyma tylko dane

Warto się upewnić o którym znaczeniu słowa się rozmawia. Google na pewno nie jest wzorcem do naśladowania, bez opensourcowych bibliotek do Androida byłaby bieda w programowaniu aplikacji na ten system.

0

Dzięki Waszej pomocy zacząłęm w wolnych chwilach ruszać temat do przodu .. trochę mało czasu ostatnio, ale no staram się. Po tych wszystkich tutkach i Waszych odpowiedziach naszły mnie takie pytania.

Czym się różni takie rozwiązanie:

fun login(loginForm: LoginForm): LiveData<ServiceResult<AuthToken>> {
        val liveData = MutableLiveData<ServiceResult<AuthToken>>()

        CoroutineScope(Dispatchers.IO).launch {
            val test = safeCall { api.login(loginForm) }
            liveData.postValue(test)
        }

        return liveData
}

Od takiego:

private val _authToken = MutableLiveData<ServiceResult<AuthToken>?>()
val authToken: LiveData<ServiceResult<AuthToken>?> = _authToken

fun login(loginForm: LoginForm) {

        CoroutineScope(Dispatchers.IO).launch {
            val test = safeCall { api.login(loginForm) }
            _authToken.postValue(test)
        }
}

Czy poza po prostu sposobem implementacji ma to jakieś konswekencje większe ? W 1 przypadku podpinam obserwator pod metodę, w 2 obserwuję zmienną.

Kolejna rzecz, do której nie wiem jak podejść:
Korzystam z Glide'a do ładowania obrazków do ImageView. Chciałbym zrobić coś takiego, że ładuję z API listę 10 zdjęć (urli) i chciałbym, żeby to działało tak, że wyświetlam pierwszy i po kliknięciu na jakiś button od razu się wyświetla drugi w tym miejscu (żeby nie trzeba było czekać na załadowanie kolejnego tylko żeby był on już załadowany pod spodem). Myślałem, żeby zrobić jakiś własny view i nałożyć na siebie 10 image view, ale nie wiem czy to dobry pomysł ?

2

To pierwsze rozwiązanie wydaje mi się mocno niepraktyczne. LiveData to po prostu strumień danych. Taki bardzo ubogi Flux/Observable, który zawsze wypycha dane na głównym wątku. Może pokaż jak wyglądałoby użycie tej pierwszej funkcji.

Co do Glidea to nie korzystam, więc nie wiem, ale powinien mieć jakiś mechanizm, żeby załadować obrazki do cache'a przed ich wyświetleniem. Większość bibliotek do ładowania obrazków na to pozwala. Jeśli nie ma czegoś takiego, to możesz też skorzystać np. z ViewPagera ustawić mu offscreenPageLimit na ile tam potrzebujesz (wtedy Glide załaduje odpowiednio więcej obrazków) i dodać własną obsługę kliknięć itd.

0

Ok, to drugie rozwiązanie jest lepsze. Tylko teraz trochę załapałem mind fucka z odpowiedzialnością poszczególnych warstw. Bo tak: z jednej strony mamy ViewModele, które zwracają live daty, na które się subskrybujuemy. Z drugiej strony mamy Retrofita zwracającego Response<T>. Teoretycznie do ViewModelu mogę wrzucić całą logikę i ok. No ale czytałem, że w ViewModelach nie powinno być logiki. Zrobiłem zatem jeszcze jedną wartstwę serwisów, do których wstrzykuję api retrofita, tam ogarniam logikę całą i zwracam LiveDaty. No ale w takim razie wychodzi na to, że ViewModele są tylko przelotką i nic nie robią :O Nie wiem co robię nie tak. Do tego widziałem, gdzieś jeszcze, że używa się Transformations::map i ::switchMap. To dobra praktyka ? Może moja warstwa serwisów nie powinna zwracać LiveDaty tylko po prostu jakieś resulty Succes<T>, Error<T>, które zostałyby ograne w ViewModelach ?

1

No ja bym nie zwracał z serwisów LiveData tylko zawieszającą funkcję i z jej użyciem mapował itd. Te wszystkie Transformations są kiepskie moim zdaniem, bo się wykonują na głównym wątku. W ogóle LiveData jest dla mnie dosyć średnią konstrukcją.

Głównymi zadaniami ViewModeli jest przechowywanie danych dla widoku i długotrwających operacji na czas zmiany konfiguracji aplikacji, żeby nie dochodziło do wycieków pamięci. Na moje to właśnie tam powinny żyć LiveData, ale może ktoś bardziej doświadczony z tą klasą ma lepsze podejście, bo piszę to bardziej na podstawie własnych przemyśleń i doświadczeń ludzi z pracy czy jakichś projektów na GH.

0

@Michał Sikora:

Wielkie dzięki. Zmieniłem to i wygląda lepiej. Teraz serwisy zwracają mi sealed class ServiceResult (Succes<T> i Failure) i w LiveModelach ogrywam to w korutynach i postuje do Live Data. Potem w Fragmentach się subskrybuje po prostu do wartości, a nie do metod. No i zawsze w ViewModelu mam LiveDatę z ErroCodem do obsługi błędu, który się pojawi. Chyba zaczynam czaić w miarę.

Rozkminiam teraz tylko Live Daty na globalny element widoku. Bo chciałem zrobić customowy App Bar (Jake Wharton pisał, że ten defaultowy jest do d**y, poza tym no mam pomysł na swój). No i na tym swoim AppBarze będę miał punkty usera wyświetlone. On będzie sharowany oczywiście między fragmentami. To muszę się na niego zasubskrybować w takim razie w aktywności, a nie w każdym fragmencie tak ? Czy są jakieś inne patenty na to ?

Swoją drogą, dobrym pomysłem jest rozszerzenie Constraint Layout jeśli chce swój AppBar ?

Swoją droga fajna odskocznia porobić sobie takie rzeczy z dala od typowego backendu w robocie :D

1

Toolbar nie jest zły, ale ma rozdmuchane API i czasami potrafi się dziwnie zachowywać. A już na pewno nie korzystałbym z niego w połączeniu z Activity.setSupportActionBar(). Zazwyczaj lepsze i szybsze efekty można osiągnąć korzystając z layoutów i wsadzając do nich to, co akurat potrzebujemy. Nie wiem czy musisz aż rozszerzać ConstraintLayout, ale to już zależy od konkretnego przypadku. Na pewno nie jest to błędne czy złe.

Jeśli chcesz współdzielić dane, to masz dwa wyjścia. Albo mieć jeden AppBar w aktywności i pod nim kontener na fragmenty, albo w każdym fragmencie nowy AppBar. Drugie rozwiązanie jest mniej upierdliwe w praktyce, bo można AppBar dostosować do każdego ekranu dużo łatwiej niż jakby miał żyć w aktywności. Wtedy AppBar może obserwować dane ze współdzielonego pomiędzy fragmentami ViewModelu. Taki ViewModel żyje aktywności i możesz się do niego z fragmentu dobrać w ten sposób.

class SharedViewModel : ViewModel() {
  private val _counter = MutableLiveData<Int>()
  val counter: LiveData<Int> get() = _counter

  private val timer = Timer()

  init {
    timer.schedule(object : TimerTask() {
      override fun run() {
        _counter.postValue(_counter.value?.inc() ?: 0)
      }
    }, 0L, 1000L)
  }

  override fun onCleared() {
    timer.cancel()
  }
}

class FragmentA : Fragment() {
  private lateinit var sharedViewModel: SharedViewModel

  override fun onCreate(inState: Bundle?) {
    super.onCreate(inState)
    sharedViewModel = ViewModelProviders.of(requireActivity()).get(SharedViewModel::class.java)
    sharedViewModel.counter.observe(this, Observer<Int> { println(it) })
  }
}

class FragmentB : Fragment() {
  private lateinit var sharedViewModel: SharedViewModel

  override fun onCreate(inState: Bundle?) {
    super.onCreate(inState)
    sharedViewModel = ViewModelProviders.of(requireActivity()).get(SharedViewModel::class.java)
    sharedViewModel.counter.observe(this, Observer<Int> { println(it) })
  }
}

Zobacz, że teraz w ViewModelProviders.of() nie podajesz fragmentu tylko aktywność, żeby ViewModel był związany cyklem życia aktywności a nie fragmentu.

0

Co na myśli pisząc, że lepsze efekty korzystając z Layoutów ? W sensie rozszerzyć tak jak mówię klasę Layoutu ? Np Constraint ?
Co do AppBara jednak będę miał różny w zależności od Fragmentu :)

1

Tak chodzi mi o jakikolwiek ViewGroup - LinearLayout, FrameLayout, ConstraintLayout. Pytanie czy trzeba tworzyć od razu całą klasę do tego. Czasami wystarczy po prostu coś takiego bez narzutu na klasę. Zależy jak często taki AppBar byłby używany itd.

https://github.com/JakeWharton/SdkSearch/blob/master/search/ui-android/src/main/res/layout/search.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
  <!-- TODO https://issuetracker.google.com/issues/78151519 -->
  <LinearLayout
      android:id="@+id/toolbar"
      android:layout_width="0dp"
      android:layout_height="?android:attr/actionBarSize"
      android:background="?android:attr/colorPrimary"
      android:elevation="4dp"
      android:orientation="horizontal"
      android:theme="@style/ThemeOverlay.SdkSearch.Dark.ActionBar"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent"
      tools:ignore="UnusedIds"
      >
    <ImageView
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:layout_gravity="center_vertical"
        android:importantForAccessibility="no"
        android:paddingEnd="@dimen/horizontal_spacing"
        android:paddingStart="@dimen/horizontal_spacing"
        android:src="@drawable/ic_search_white_24dp"
        />
    <EditText
        android:id="@+id/query"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:background="@null"
        android:hint="@string/search"
        android:imeOptions="actionGo|flagNoExtractUi"
        android:inputType="textCapWords|textNoSuggestions"
        android:privateImeOptions="nm"
        android:selectAllOnFocus="true"
        />
    <ImageView
        android:id="@+id/clear_query"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:layout_gravity="center_vertical"
        android:background="?android:attr/selectableItemBackgroundBorderless"
        android:contentDescription="@string/clear_search_query"
        android:paddingEnd="@dimen/horizontal_spacing"
        android:paddingStart="@dimen/horizontal_spacing"
        android:src="@drawable/ic_close_white_24dp"
        android:visibility="invisible"
        />
  </LinearLayout>
  <androidx.recyclerview.widget.RecyclerView
      android:id="@+id/results"
      android:layout_width="0dp"
      android:layout_height="0dp"
      android:scrollbars="vertical"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@id/toolbar"
      />
</androidx.constraintlayout.widget.ConstraintLayout>
0

Dzięki, faktycznie tworzenie klasy pod taki toolbar jest bezsensowne. Na każdy ekran mam trochę inny toolbar i copy paste nie jest taki straszny z częścią elementów.

Jeszcze tylko takie pytanie o Nav Controller. Mam takie dziwne zachowanie, że jak przejdę z ekranu logowania do ekranu home, gdzie ładują mi się i wyświetlają obrazki (poprzez Glide'a) to potem przy cofaniu (back button na telefonie) zamiast wrocić do ekranu logowania ładują mi się kolejne zdjęcia. Zdefiniowałem przy akcji popUpTo oraz popUpToInclusive="true" i zachowało się ok, czyli z ekranu home wyszło mi z apki. Ale nie do końca kumam to zachowanie. Jeśli ze startowego ekranu przeszło do home, to tamten odłożył się na stosie i przy kliknięciu 'back' nie powinno wrócić do niego ściągając obecny "home" ze stosu ? Przy przechodzaniu z ekranu "home" do dalszych ekranów (gdzie de facto nie mam jeszce żadnych view modeli) wszystko działa ok. Wchodzę tam i jak wracam strzałką na tel to wraca do home normalnie.

I jeszcze jedno .. obecnie layout tabowy nadal powinienem robić za pomocą TabLayout czy są jakieś nowsze, lepsze komponenty ?

1

NavControllerem nigdy się nawet nie bawiłem, więc nie mam pojęcia. Jeśli chodzi o zakładki, to TabLayout + ViewPager jak najbardziej spoko.

0

@Michał Sikora:
Mam jeszcze pytanie odnośnie korutyn.

U Ciebie w przykładzie, który dałeś tworzysz sobie Joba jako pole klasy, potem gettera na niego, aby używać w launchu i robisz override onClear. Rozumiem, że to jest po to, że jak user wyjdzie z fragmentu podpiętego pod dany ViewModel, a będzie się wykonywać operacja to po prostu job zostanie zamknięty.

Drugie pytanie: czy launch nie powinien być na całej metodzie, a nie tyllko jej fragmencie ? Myślałem, że cała logika powinna się wykonywać w innym wątku.

U mnie np metoda z ViewModelu wygląda tak (nie mam nigdzie trzymanego joba). Czy to jest duży bląd ?:

fun setShowMe(show: Boolean) = CoroutineScope(Dispatchers.IO).launch {
    _processing.postValue(true)

    val v = userSettings.value ?: return RuntimeException("Settings not found")

    val result = settingsService.save(
        UserSettings(
            name = v.name
        )
    ) ?: return RuntimeException("Save with error")

    _userSettings.postValue(result)

    _processing.postValue(false)
}
2

launch może być na całej metodzie. Zależy jak bardzo zasobożerna jest logika. Zauważ że w moim przykładzie walidacja danych, to jakiś prosty if, który nie robi niczego ciężkiego. Więcej zajmie zmiana wątku niż ta logika. Ale zrobiłem tam mały błąd, bo przed wystartowaniem korutyny mogłem robić .value = x zamiast .postValue(x).

Z kolei Job, jak zauważyłeś, jest do posprzątania zasobów. Uważam, że to bardzo duży błąd nie korzystać z niego. Skutki mogą być od niezauważalnego marnowania baterii po wycieki pamięci i wywalanie się aplikacji.

0

@Michał Sikora:
a to sprzątanie zasobów nie za późno się odbywa ? Nie powinno ono być na onStop ?

1 użytkowników online, w tym zalogowanych: 0, gości: 1