I try to access the login state and other user information everywhere in my app.
Here is how I plan on doing it:
Create a UserState data class:
data class UserState(
val username: String = "",
val profileImageUrl: String = "",
var isLoggedIn: Boolean = false,
val isPremiumUser: Boolean = false,
val coins: Int = 0
)
Make it a singleton with hilt and inject it into my AuthRepository:
#Provides
#Singleton
fun provideUserState() = UserState()
#Singleton
#Provides
fun provideAuthRepository(userState: UserState): AuthRepository {
return AuthRepositoryImpl(userState)
}
then when the authentication (firebase + custom backend) is succesfull or other auth functions get called I update the userState with the userData:
class AuthRepositoryImpl #Inject constructor(
private var userState: UserState
) : AuthRepository {
override fun getUserState(): UserState {
return userState
}
override suspend fun authenticateUser(token: String){
val responseUser = authenticateUser(token)
if (responseUser != null) {
userState = UserState(
username = responseUser.username,
profileImageUrl = responseUser.profileImageUrl,
isLoggedIn = true,
isPremiumUser = responseUser.profileImageUrl,
coins = responseUser.coins
)
}
}
}
Now comes the part where I am not sure on how to do it, how do I observe the userState from my ViewModels?
If I call for example the logOut function in my AuthRepository from ViewModel X I want to see/get the change in ViewModel Y.
I call the getUserState method for example in the init block of my MainViewModel:
init {
_mainUserState.value = authRepository.getUserState()
}
So the problem is it only gets updated on the start, how can I observe changes that get made to the UserState Singleton?
I'm not sure if flows are the answer and if yes how exactly to use them int his scenario
You can do something like this with flow in you'r auth repository:
private val _authData = MutableStateFlow<AuthEvent>(AuthEvent.Nothing)
val authData: Flow<AuthEvent> = _authData
then emit like this when login/logout
_authData.emit(AuthEvent.Login)
AuthEvent is just a sealed class
Add the AuthState to AuthEvent.Login class
you can do shared flow too
then in you'r viewModel observe it under viewModelScope
viewModelScope.launch {
authRepository.getAuthFlow().collect() { data ->
when (data) {
is AuthEvent.Logout -> {
sendUiEvent(MainUIEvent.Logout)
_state.value = state.value.copy(isLoggedIn = false, authData = null)
}
is AuthEvent.Login -> {
_state.value = state.value.copy(isLoggedIn = true, authData = data.authData)
sendUiEvent(MainUIEvent.LoggedIn)
}
is AuthEvent.Nothing -> {
}
}
}
}
this appoarch allows you to handle auth status and navigation from you mainViewModel
Related
I want to write a simple test for my viewModel to check if it gets data from repository. The app itself working without problem but in test, i have the following test failed.
It looks like the viewModel init block not running, because it suppose to call getUpcomingMovies() method in init blocks and post value to upcomingMovies live data object. When i test it gets null value.
Looks like i am missing a minor thing, need help to solve this.
Here is the test:
#ExperimentalCoroutinesApi
class MoviesViewModelShould: BaseUnitTest() {
private val repository: MoviesRepository = mock()
private val upcomingMovies = mock<Response<UpcomingResponse>>()
private val upcomingMoviesExpected = Result.success(upcomingMovies)
#Test
fun emitsUpcomingMoviesFromRepository() = runBlocking {
val viewModel = mockSuccessfulCaseUpcomingMovies()
assertEquals(upcomingMoviesExpected, viewModel.upcomingMovies.getValueForTest())
}
private fun mockSuccessfulCaseUpcomingMovies(): MoviesViewModel {
runBlocking {
whenever(repository.getUpcomingMovies(1)).thenReturn(
flow {
emit(upcomingMoviesExpected)
}
)
}
return MoviesViewModel(repository)
}
}
And viewModel:
class MoviesViewModel(
private val repository: MoviesRepository
): ViewModel() {
val upcomingMovies: MutableLiveData<UpcomingResponse> = MutableLiveData()
var upcomingMoviesPage = 0
private var upcomingMoviesResponse: UpcomingResponse? = null
init {
getUpcomingMovies()
}
fun getUpcomingMovies() = viewModelScope.launch {
upcomingMoviesPage++
repository.getUpcomingMovies(upcomingMoviesPage).collect { result ->
if (result.isSuccess) {
result.getOrNull()!!.body()?.let {
if (upcomingMoviesResponse == null) {
upcomingMoviesResponse = it
} else {
val oldMovies = upcomingMoviesResponse?.results
val newMovies = it.results
oldMovies?.addAll(newMovies)
}
upcomingMovies.postValue(upcomingMoviesResponse ?: it)
}
}
}
}
}
And the result is:
expected:<Success(Mock for Response, hashCode: 1625939772)> but was:<null>
Expected :Success(Mock for Response, hashCode: 1625939772)
Actual :null
I have a Home screen in my application that makes a call to get user information.
But this call is being made infinite times, both inside the LaunchedEffect function and in the Init() of the viewModel.
Meu código da viewModel está assim:
class HomeViewModel(
private val service: HomeServices,
pref: SecurePreferences
): ViewModel() {
private val _userInfo = MutableStateFlow(Result(State.Loading, null))
val userInfo: StateFlow<Result> get() = _userInfo
// init {
// val userId = pref.get<String>(Constants.KEY_USER_UID)!!
// getUserInformation(userId)
// }
fun getUserInformation(userId: String) = viewModelScope.launch{
RequestHandler.doRequest { service.getUserInformation(userId) }.then(
onSuccess = { user ->
Log.d("Success", user.name)
_userInfo.emit(Result(State.Success, user))
},
onError = { message ->
Log.d("Error", "Failed! $message.")
_userInfo.emit(Result(State.Failed, null))
},
onFinish = {}
)
}
}
data class Result(val state: State, val user: UserInformationResponse?)
enum class State {
Success,
Failed,
Loading
}
My HomeScreen code looks like this:
#Composable
fun HomeScreen() {
val viewModel = getViewModel<HomeViewModel>()
val posts = remember { PostsMock.postList }
val context = LocalContext.current
val pref = SecurePreferences(context)
val userId = pref.get<String>(Constants.KEY_USER_UID)!!
LaunchedEffect(userId){
viewModel.getUserInformation(userId)
}
val userInfo = viewModel.userInfo.collectAsState()
when (userInfo.value.state) {
State.Loading -> {......
could anyone help me with this?
I want to store some preferences using DataStore. But the problem is that my application can have multiple users and therefor needs to store these preferences in separate files. I got a working example using only one user but I'm struggling to support multiple users.
Here is an example of my code:
class DataStorageRepository(private val context: Context, private val userRepository: UserRepository) {
private object PreferencesKeys {
val SETTING_ONE = intPreferencesKey("setting_one")
}
// retrieve datastore for currently logged in user.
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = userRepository.currentRegistration().name)
val userPreferencesFlow: Flow<UserPreferences> = context.dataStore.data.map { preferences ->
val settingOne = preferences[PreferencesKeys.SETTING_ONE] ?: 0
UserPreferences(settingOne)
}
suspend fun storeSettingOne(settingOne: Int) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.SETTING_ONE] = settingOne
}
}
data class UserPreferences(val lastUsedToAccountTab: Int)
}
I'm using Koin and I tried unloading the DataStorageRepository on logout and recreating it on login but the DataStore seems to stay alive until the app is killed and I get the following crash:
java.lang.IllegalStateException: There are multiple DataStores active
for the same file: [...] You should either maintain your DataStore as
a singleton or confirm that there is no two DataStore's active on the
same file (by confirming that the scope is cancelled).
I also tried to use a CoroutineScope and kill that when I log out, but after recreating the scope on login the DataStore doesn't seem to get recreated.
Does DataStore support a way to close the connection or to handle multiple files?
Put this line inside companion object { }
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settingPrefs")
My Code
class SettingPrefs(private val context: Context) {
companion object {
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settingPrefs")
private val soundKey = booleanPreferencesKey("sound")
private val vibrateKey = booleanPreferencesKey("vibrate")
}
val getSound: Flow<Boolean>
get() = context.dataStore.data.map {
it[soundKey] ?: true
}
suspend fun setSound(value: Boolean) {
context.dataStore.edit { it[soundKey] = value }
}
val getVibration: Flow<Boolean>
get() = context.dataStore.data.map {
it[vibrateKey] ?: true
}
suspend fun setVibration(value: Boolean) {
context.dataStore.edit { it[vibrateKey] = value }
}
}
You can use different key for different user or manual keep DataStore singleton.
For exception:
java.lang.IllegalStateException: There are multiple DataStores active for the same file: [...] You should either maintain your DataStore as a singleton or confirm that there is no two DataStore's active on the same file (by confirming that the scope is cancelled).
androidx.datastore:datastore-*:1.0.0-alpha07 is released.
Put this at the top level of your kotlin file so there is only one instance of it.
private val Context.dataStore by preferencesDataStore("settings")
class Xxx{
}
https://developer.android.com/jetpack/androidx/releases/datastore#1.0.0-alpha07.
The Context.createDataStore extension function has been removed and replaced with globalDataStore property delegate. Call globalDataStore once at the top level in your kotlin file. For example:
val Context.myDataStore by dataStore(...)
Put this at the top level of your kotlin file so there is only one instance of it. (I57215, b/173726702)
At the moment I was posting this question I found a solution to this problem. In order to solve my problem I needed to combine my previous two solutions. So on logout I unload the DataStorageRepository and on login I reload it again. I also needed to create a CoroutineScope that I cancel on logout.
My Module
val loggedInModule = module {
single { DataStorageRepository(get(), get()) }
}
I created a scope and passed it to the DataStore
var loggedInScope: CoroutineScope = CoroutineScope(Dispatchers.Default)
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = userRepository.currentRegistration().name, scope = loggedInScope)
On Login
loggedInScope = CoroutineScope(Dispatchers.Default)
loadKoinModules(loggedInModule)
On Logout
loggedInScope.cancel()
unloadKoinModules(loggedInModule)
Just put your declaration datastore out of your DataStorageRepository class
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name="settings")
class DataStorageRepository(context: Context) {
private var appContext = context.applicationContext
val mData: Flow<String?> = appContext.dataStore.data.map { preferences ->
preferences[YOUR_KEY]
}
suspend fun insertData(value: String) {
appContext.dataStore.edit { preferences ->
preferences[YOUR_KEY] = authToken
}
}
companion object {
private val KEY = stringPreferencesKey("data")
}
}
This is what I'm using in my project:
private object UserIdBasedPrefDs {
val lock = Any()
#GuardedBy("lock")
#Volatile
var currentId: String = ""
#GuardedBy("lock")
#Volatile
var INSTANCE: DataStore<Preferences>? = null
}
fun Context.happyStore( // rename what ever you like.
userId: String,
// below 3 optional params are same as int the `preferencesDataStore`.
corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
produceMigrations: (Context) -> List<DataMigration<Preferences>> =
{ listOf() },
scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
): DataStore<Preferences> = UserIdBasedPrefDs.run {
// check if current user id has been changed.
if (userId != currentId && INSTANCE != null) {
synchronized(lock) {
// release previous saved.
INSTANCE = null
// reset new user id.
currentId = userId
}
}
// below is the same logic inside the `preferencesDataStore` delegate.
INSTANCE ?: synchronized(lock) {
if (INSTANCE == null) {
INSTANCE = PreferenceDataStoreFactory.create(
corruptionHandler = corruptionHandler,
migrations = produceMigrations(applicationContext),
scope = scope
) {
applicationContext.preferencesDataStoreFile(userId)
}
}
INSTANCE!!
}
}
Hope it can be helpful to you.
LeakCanary is telling me that one of my ViewModels is leaking but after playing around for 2 days I can't get the leak to go away.
Here is why LeakCanary shows
Here is the Fragment getting the ViewModel
viewModel = ViewModelProvider(this).get(ViewBreederViewModel::class.java).apply {
getStrains(arguments?.getString(BREEDER_ID_KEY, "")!!)
}
Here is the ViewModel
class ViewBreederViewModel(application: Application) : AndroidViewModel(application) {
private val breederRepository = BreederRepository(application)
val strainList = MutableLiveData<List<MinimalStrain>>()
fun getStrains(breederId: String) {
viewModelScope.launch {
breederRepository.getMinimalStrains(breederId).observeForever {
strainList.value = it
}
}
}
}
Here is the BreederRepository:
class BreederRepository(context: Context) {
private val dao: BreederDao
private val breederApi = RetrofitClientInstance.getInstance(context).breederAndStrainIdsApi
init {
val database: Db = Db.getInstance(
context
)!!
dao = database.breederDao()
}
suspend fun getMinimalStrains(breederId: String): LiveData<List<MinimalStrain>> =
withContext(Dispatchers.IO) {
dao.getMinimalStrains(breederId)
}
}
Here is the Db class
#Database(
entities = [Breeder::class, Strain::class],
version = 1,
exportSchema = true)
#TypeConverters(RoomDateConverter::class)
abstract class Db : RoomDatabase() {
abstract fun breederDao(): BreederDao
companion object {
private var instance: Db? = null
#JvmStatic
fun getInstance(context: Context): Db? {
if (instance == null) {
synchronized(Db::class) {
instance = Room.databaseBuilder(
context.applicationContext,
Db::class.java, "seedfinder_db"
)
.build()
}
}
return instance
}
}
}
You're using observeForever, which, as the name suggest, will keep observing forever, even after your ViewModel is cleared. Room does not require using a suspend method for DAO methods that return a LiveData and that is never the right approach in any case - LiveData is already asynchronous.
Instead, you should be transforming your LiveData, using your breederId as the input to your strainList LiveData:
class ViewBreederViewModel(application: Application) : AndroidViewModel(application) {
private val breederRepository = BreederRepository(application)
private val currentBreederId = MutableLiveData<String>()
// Here we use the switchMap method from the lifecycle-livedata-ktx artifact
val strainList: LiveData<String> = currentBreederId.switchMap {
breederId -> breederRepository.getMinimalStrains(breederId)
}
private fun setBreederId(breederId: String) {
currentBreederId.value = breederId
}
}
Where your getMinimalStrains becomes:
fun getMinimalStrains(breederId: String): LiveData<List<MinimalStrain>> =
dao.getMinimalStrains(breederId)
And you use it by setting your breederId in your UI and observing your strainList as before:
viewModel = ViewModelProvider(this).get(ViewBreederViewModel::class.java).apply {
setBreederId(arguments?.getString(BREEDER_ID_KEY, "")!!)
}
viewModel.strainList.observe(viewLifecycleOwner) { strainList ->
// use your updated list
}
If you're using Saved State module for ViewModels (which is the default if you're using the latest stable Fragments / Activity libraries), then you can use SavedStateHandle, which is automatically populated from your Fragment's arguments and skip the setBreederId() entirely:
class ViewBreederViewModel(
application: Application,
savedStateHandle: SavedStateHandle
) : AndroidViewModel(application) {
private val breederRepository = BreederRepository(application)
// Here we use the switchMap method from the lifecycle-livedata-ktx artifact
val strainList: LiveData<String> = savedStateHandle
.getLiveData(BREEDER_ID_KEY) // Automatically populated from arguments
.switchMap {
breederId -> breederRepository.getMinimalStrains(breederId)
}
}
Which means your code can simply become:
viewModel = ViewModelProvider(this).get(ViewBreederViewModel::class.java)
viewModel.strainList.observe(viewLifecycleOwner) { strainList ->
// use your updated list
}
And if you use the fragment-ktx artifact, you can simplify this further to:
// Move this to where you declare viewModel
val viewModel: ViewBreederViewModel by viewModels()
viewModel.strainList.observe(viewLifecycleOwner) { strainList ->
// use your updated list
}
I Am using MVVM architecture to simple project. Then i stack in this case, when i have to return value from Model DataSource (Lambda function) to Repository then ViewModel will observe this repository. Please correct me if this not ideally and give me some advise for the true MVVM in android. i want to use LiveData only instead of RxJava in this case, because many sample in Github using RxJava.
In my Model i have class UserDaoImpl, code snippet like below
class UserDaoImpl : UserDao {
private val resultCreateUser = MutableLiveData<AppResponse>()
private val mAuth : FirebaseAuth by lazy {
FirebaseAuth.getInstance()
}
override fun createUser(user: User) {
mAuth.createUserWithEmailAndPassword(user.email, user.password)
.addOnCompleteListener {
//I DID NOT REACH THIS LINE
println("hasilnya ${it.isSuccessful} ")
if(it.isSuccessful){
val appResponse = AppResponse(true, "oke")
resultCreateUser.postValue(appResponse)
}else{
val appResponse = AppResponse(false, "not oke -> ${it.result.toString()}")
resultCreateUser.postValue(appResponse)
}
}
.addOnFailureListener {
println("hasilnya ${it.message}")
val appResponse = AppResponse(false, "not oke -> ${it.message}")
resultCreateUser.postValue(appResponse)
}
}
override fun getResultCreateUser() = resultCreateUser
}
And this is my Repository snippet code
class RegisterRepositoryImpl private constructor(private val userDao: UserDao) : RegisterRepository{
companion object{
#Volatile private var instance : RegisterRepositoryImpl? = null
fun getInstance(userDao: UserDao) = instance ?: synchronized(this){
instance ?: RegisterRepositoryImpl(userDao).also {
instance = it
}
}
}
override fun registerUser(user: User) : LiveData<AppResponse> {
userDao.createUser(user)
return userDao.getResultCreateUser() as LiveData<AppResponse>
}
}
Then this is my ViewModel
class RegisterViewModel (private val registerRepository: RegisterRepository) : ViewModel() {
val signUpResult = MutableLiveData<AppResponse>()
fun registerUser(user: User){
println(user.toString())
val response = registerRepository.registerUser(user)
signUpResult.value = response.value
}
}
If i execute the snippet code above, the result always nullpointer in signUpResult
This is my Activity
lateinit var viewModel: RegisterViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_register)
initializeUI()
}
private fun initializeUI() {
val factory = InjectorUtils.provideRegisterViewModelFactory()
viewModel = ViewModelProviders.of(this, factory).get(RegisterViewModel::class.java)
viewModel.signUpResult.observe(this, Observer {
//IT always null
if(it.success){
// to HomeActivity
Toast.makeText(this, "Success! ${it.msg}", Toast.LENGTH_SHORT).show()
}else{
Toast.makeText(this, "FALSE! ${it.msg}", Toast.LENGTH_SHORT).show()
}
})
register_btn.setOnClickListener {
val username = name.text.toString()
val email = email.text.toString()
val password = password.text.toString()
val phone = number.text.toString()
val user = User(0, username,"disana", email, password, "disana")
viewModel.registerUser(user)
}
}
Crash occured when i press register button
I'm not 100% sure, but I think the problem is in your ViewModel, where you are trying to pass by reference MutableLiveData. Your Activity is observing signUpResult MutableLiveData, but you are never posting new value, you are trying to change reference of that LiveData to one in Repository.
val signUpResult = MutableLiveData<AppResponse>()
fun registerUser(user: User){
println(user.toString())
val response = registerRepository.registerUser(user)
signUpResult.value = response.value
}
I think that the solution here is to let your ViewModel return LiveData, which is returned from Repository.
fun registerUser(user: User): MutableLiveData<AppResponse> {
println(user.toString())
return registerRepository.registerUser(user)
}
And you need to observe function registerUser(user) in your Activity.
viewModel.registerUser(user).observe(this, Observer {
But now you encountered another problem. By this example you will trigger observe method every time your button is clicked. So you need to split in repository your function, you need to make one only for returning userDao.getResultCreateUser() as LiveData<AppResponse>, and the other to trigger userDao.create(user) .
So you can make two functions in your repository
override fun observeRegistrationResponse() : LiveData<AppResponse> {
return userDao.getResultCreateUser() as LiveData<AppResponse>
}
override fun registerUser(user: User) {
userDao.createUser(user)
}
Now also in ViewModel you need to make separate function for observing result and for sending request for registration.
fun observeRegistrationResponse(): LiveData<AppResponse> {
return registerRepository.observeRegistrationResponse()
}
fun registerUser(user: User){
println(user.toString())
registerRepository.registerUser(user)
}
And finally you can observe in your function initializeUI
viewModel.observeRegistrationResponse().observe(this, Observer {
And send registration request on button click
viewModel.registerUser(user)
Sorry for long response, but I tried to explain why you need to change your approach. I hope I helped you a bit to understand how LiveData works.