Czym jest User - Dywagacje

0

Hej,

Mam aplikację, ktora warunkuje funkcjonalności zaleznie od tego czy użytkownik jest zalogowany czy nie.
I tu mam dosc szerokie pytanie w jaki sposob wprowadzac aplikacje w dwa stany czy sprawdzic raz przy uruchomieniu i nastepnie nie musiec sprawdzac usera w kazdym widoku/fragmencie. W jaki sposob to jest obslugiwane przy komercyjnych aplikacjach?

  • Jeden globalny viewModel, ktory trzyma obiekt zalogowanego badz nie usera? To troche bez sensu bo ciężko jest skomunikować ze soba dwa viewModele podpięte do jednego fragmentu.

  • Dwie aktywnosci, jedna dla zalogowanych druga dla nie zalogowanych?

  • Klasa User ktora ma wszystkie wlasciwosci danego usera ale dodatkowo ma metody na pobieranie jego danych plus roznorakie isCartEmpty czy isAddressProvided?

Obecnie w mojej aplikacji mam jedna aktywnosc i zalozmy 8 fragmentow. Kazdy fragment ma swoj viewModel, ktory przy tworzeniu instancji wywoluje getCurrentUser(). Jak mozna sie domyslic przy kilku viewModelach to nie jest jakis problem ale dodajac funkcjonalnosci dojde spokojnie do np > 20 fragmentow i viewModeli i to juz sie wydaje po prostu bez sensu zeby za kazdym razem pobierac usera skoro tak na prawde nic sie w nim nie zmieni oprocz np produktow w koszyku czy adresu. Nie wyobrazam sobie tez zeby mialo to sens przy duzej aplikacji ecommerce.

Podzielcie sie prosze przemysleniami jak byscie to widzieli.

0

Po pierwsze to powinienieś wydzielić jakąś warstwę data/repository, przechowywać Usera albo w bazie danych / preferences albo po prostu w pamięci. Możesz różnież dodać warstwę UseCasów ale nie wszyscy to lubią. Poczytaj również o dependecy injection. Czyli np.

class UserRepository(
    private val networkResource; UserApi
) {

    fun getUser() = networkResource.getUser()

   tutaj sobie go możesz zapisać czy trzymać w pamięci
}
class GetUserUseCase(
    private val repo; UserRepository
) {

  fun get() = repo.getUser()

 tutaj dodatkowo jakiś mapping jak jest potrzebny
}
class Some1ViewModel(
    private val getUser; GetUserUseCase
) : ViewModel {

 pobrasz usera wdl potrzeb
}
class Some2ViewModel(
    private val getUser; GetUserUseCase
) : ViewModel {

 pobrasz usera wdl potrzeb
}

etc
Takie podejście zapewnia Ci separacje.

Współdzielone ViewModele mają trochę inne zastosowanie. Ważny też jest tzw. Scope w jakim istnieje współdzielony ViewModel.
Np. jak robisz jakiś flow (zamówienie i płatność) który zawiera kilka stepów to dobrze jest kiedy każdy ekran ma swój VM a stan poszczególnych stepów zapisujesz w SVM.

0

Zdaje sobie sprawę, że troche nie jasno opisałem jakie jest dokładnie pytanie.
Najbardziej drażni mnie w tym projekcie że odpytuje o usera za każdym razem we viewModelu gdy ten jest tworzony więc praktycznie na każdym widoku co uważam za bezsensowne w dłuższej perspektywie. Zastanawiam się nad optymalizacją w jaki sposób mieć jedną instancję usera i aktualizować ją gdy zajdzie potrzeba ale każdy viewModel dostawał by istniejącą już instancje, w domyśle tą samą. Nie wiem czy jasno to opisuje, brak mi jeszcze szerszego kontekstu chyba żeby to rzeczowo wyjaśnić.

Na ten moment po przejściu do przykładowego fragmentu, tworzony jest też viewModel, która na wstępie pyta repozytorium(nie wazne czy remote czy local) o dane użytkownika a wolałbym żeby dostawał już gotowca, każdy viewModel operowałby na tym samym.

Repo


class UserRepositoryImpl
@Inject
constructor(
    private val databaseInteractor: DatabaseInteractor,
    private val authenticator: Authenticator,
    private val dispatcherProvider: DispatcherProvider,
    private val inputDatabaseUserMapper: NullableInputDatabaseUserMapper,
    private val outputDatabaseUserMapper: NullableOutputDatabaseUserMapper
) : UserRepository {

    private val currentUserUid = authenticator.getCurrentUserUid()

    override suspend fun createAccount(
        email: String,
        password: String,
        firstName: String,
        lastName: String
    ): Result<Nothing> = withContext(dispatcherProvider.io) {

        try {

            authenticator
                .createUserWithEmailAndPassword(email, password)
                .await()

            val currentUid = authenticator.getCurrentUserUid()

            currentUid?.let { uid ->

                val user = User(
                    uid,
                    firstName,
                    lastName,
                    email,
                    listOf(),
                    emptyMap()
                )
                val databaseUser = outputDatabaseUserMapper.mapToEntity(user)

                databaseInteractor
                    .setDocumentInCollection(
                        COLLECTION_USERS,
                        uid,
                        databaseUser
                    )
                    .await()

                Result.Success(null)

            } ?: Result.Error.AuthenticationError(null)
        } catch (exc: Exception) {
            Result.Error.NetworkError(exc.message)
        }
    }

    override suspend fun loginUser(email: String, password: String): Result<Nothing> =

        withContext(dispatcherProvider.io) {

            try {
                authenticator
                    .loginUserWithEmailAndPassword(
                        email,
                        password
                    )
                    .await()

                Result.Success(null)
            } catch (exc: Exception) {
                Result.Error.NetworkError(exc.message)
            }
        }

    override suspend fun getUserData(): Result<User> =

        withContext(dispatcherProvider.io) {

            currentUserUid?.let { uid ->

                try {
                    val documentSnapshot = databaseInteractor
                        .getSingleDocument(COLLECTION_USERS, uid)
                        .await()
                    val response = documentSnapshot.toObject(DatabaseUser::class.java)
                    val result = inputDatabaseUserMapper.mapFromDatabase(response)

                    Result.Success(result)
                } catch (exc: Exception) {
                    Result.Error.NetworkError(exc.message)
                }
            } ?: Result.Error.AuthenticationError(null)
        }

    override suspend fun addToCart(productUid: String): Result<Nothing> =

        withContext(dispatcherProvider.io) {

            currentUserUid?.let { uid ->

                try {
                    databaseInteractor
                        .addToFieldInDocument(
                            COLLECTION_USERS,
                            uid,
                            productUid
                        )
                        .await()

                    Result.Success(null)
                } catch (exc: Exception) {
                    Result.Error.NetworkError(exc.message)
                }
            } ?: Result.Error.AuthenticationError(null)
        }

    override suspend fun removeFromCart(productUid: String): Result<Nothing> =

        withContext(dispatcherProvider.io) {

            currentUserUid?.let { uid ->

                try {

                    databaseInteractor
                        .removeFromFieldInDocument(
                            COLLECTION_USERS,
                            uid,
                            productUid
                        )
                        .await()

                    Result.Success(null)
                } catch (exc: Exception) {
                    Result.Error.NetworkError(exc.message)
                }
            } ?: Result.Error.AuthenticationError(null)
        }

    override suspend fun clearUserCart(): Result<Nothing> =

        withContext(dispatcherProvider.io) {

            currentUserUid?.let { uid ->

                try {

                    databaseInteractor
                        .deleteFieldInDocument(
                            COLLECTION_USERS,
                            uid
                        )
                        .await()

                    Result.Success(null)
                } catch (exc: Exception) {
                    Result.Error.NetworkError(exc.message)
                }
            } ?: Result.Error.AuthenticationError(null)
        }

    override suspend fun updateAddress(userAddress: Map<String, String>): Result<Nothing> =

        withContext(dispatcherProvider.io) {

            currentUserUid?.let { uid ->

                try {

                    databaseInteractor
                        .updateDocument(
                            COLLECTION_USERS,
                            uid,
                            userAddress
                        )
                        .await()

                    Result.Success(null)
                } catch (exc: Exception) {
                    Result.Error.NetworkError(exc.message)
                }
            } ?: Result.Error.AuthenticationError(null)
        }
}

viewModel


@HiltViewModel
class ProductDetailFragmentViewModel
@Inject
constructor(
    savedStateHandle: SavedStateHandle,
    private val productRepository: ProductRepository,
    private val userRepository: UserRepository
) : ViewModel() {

    private val productUid = savedStateHandle.get<String>("productUid")

    private val _currentProduct = MutableLiveData<Result<Product>>()
    val currentProduct: LiveData<Result<Product>>
        get() = _currentProduct

    private val _currentUser = MutableLiveData<Result<User>>()
    val currentUser: LiveData<Result<User>>
        get() = _currentUser

    private val _navigate = MutableLiveData<Event<Boolean>>()
    val navigate: LiveData<Event<Boolean>>
        get() = _navigate

    val isNotInCart = Transformations.map(_currentUser) {
        it.data?.cart?.let { userCart -> productUid !in userCart } ?: true
    }

    private fun getSingleProduct() {

        productUid?.let { uid ->
            viewModelScope.launch {

                val result = productRepository.getSingleProduct(uid)
                _currentProduct.postValue(result)
            }
        }
    }

    private fun getCurrentUser() {

        viewModelScope.launch {
            val result = userRepository.getUserData()
            _currentUser.postValue(result)
        }
    }

    private fun addToCart() {

        productUid?.let { uid ->
            viewModelScope.launch {
                userRepository.addToCart(uid)
            }
        }
    }

    fun handleAddToCartClick() {

        currentUser.value?.data?.let {
            addToCart()
            getCurrentUser()
        } ?: navigate()
    }

    private fun navigate() {
        _navigate.value = Event(true)
    }

    init {
        getSingleProduct()
        getCurrentUser()
    }
}
AsAs napisał(a):

Współdzielone ViewModele mają trochę inne zastosowanie. Ważny też jest tzw. Scope w jakim istnieje współdzielony ViewModel.
Np. jak robisz jakiś flow (zamówienie i płatność) który zawiera kilka stepów to dobrze jest kiedy każdy ekran ma swój VM a stan poszczególnych stepów zapisujesz w SVM.

To jest świetny przykład użycia, dziękuje!

1

Możesz injectować obiekt User bezpośrednio do VMów

class MyViewModel(
 private val user:User,
...
) {
   zrob cos z user
}

albo możesz injectować LiveData<User>

class MyViewModel(
 private val user: LiveData<User>,
...
) {
   zrob cos z user.value

   obserwuj user.observerForever ale pamietaj zeby odpiac observer w onCleared
}

mozesz tez dodac LD do samego repo jako public

class UserRepo(
....
) {
   private val _user = MutableLiveData<User>()
   val user : LiveData<User> 
       get() = _user

  init {
    load user and put into the _user
} 
  fun fetchUser() {
    load user and put into the _user
 }
}
class MyViewModel(
   private val userRepo: UserRepo
) {

   zrob cos z userRepo.user.value

   obserwuj userRepo.user.observerForever ale pamietaj zeby odpiac observer w onCleared
}

Kwestia jaką masz potrzebe, czy User ma być jako instancja usera czy potrzebujesz mieć możliwość przeładowania usera z VM

0

Dzięki @AsAs !
liveData jako user w repo miałem w poprzednim projekcie i działać działało ale nie widziałem tego typu rozwiązania w repo, które przeglądałem na GH i myślałem, że to nie jest dobry pomysł, poza tym observeForever może być problematyczne ale to jest jakieś wyjście, szczególnie, że u mnie repo jest ActivityRetainedScope więc ld w pamięci dokładnie tyle ile chce. Przetestuje :)

Jest to jednak dość standardowe podejście, czy w ten sposób ktoś by napisał pobieranie informacji o zalogowanym użytkowniku w faktycznym projekcie w pracy?

1

No tak, są takie projekty. W jednych używa się LiveData, w innych StateFlows a w innych RxSubjectów. Kwstia podejscia rekatywnego i użytej architektury.
Większość projektów GH to są jakieś show-case a jak masz projekt komercyjny to piszesz w taki sposób żeby rowiązywało Twój konkretny problem.
Większość będzie dokładnie tak jak Ty masz, czyli get w każdym VM, ja osobiście wole podejście rekatywne. Obiekt sobie "czeka" i na subscribe dostaje zawsze najnowszą instancje.

Jak masz 2 VM i 1 niereaktywne repo, to jeżeli zmieni Ci się wartość obiektu w repo i zdejmiesz fragment ze stacka, to jeżeli nie dasz jakiegoś refresha na onResume to zostajesz ze starą wartością w pierwszym VM. Przy podejsciu reaktywnym wykorzustując observer pattern nie myślisz nawet o takich problemach. Ważne jest żeby "odpinać" subscriberów bo inaczej kończy się to memory leakami itp.

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