I want to handle something in my ViewModel whenever the current location retrieved. But it didn't work at the first time I start the app and approve the permission. Only be able to see some logs after I close and start the app again.
init {
viewModelScope.launch {
locationRepository.location.collect {
Log.d(TAG, it.toString())
My repository to connect the location data source as you can see
class LocationRepositoryImpl #Inject constructor(
private val dataSource: LocationDataSource,
#ApplicationScope private val externalScope: CoroutineScope
) : LocationRepository {
override val location: Flow<MapLocation> = dataSource.locationSource
.shareIn(
scope = externalScope,
started = WhileSubscribed()
And the final is LocationDataSource where I put the logic to get the current location.
class LocationDataSource #Inject constructor(
private val client: FusedLocationProviderClient
) {
val locationSource: Flow<MapLocation> = callbackFlow {
val request = LocationRequest.create().apply {
interval = TimeUnit.SECONDS.toMillis(4)
fastestInterval = TimeUnit.SECONDS.toMillis(4)
priority = Priority.PRIORITY_HIGH_ACCURACY
}
val callBack = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
super.onLocationResult(result)
result.lastLocation?.let {
trySend(it.asModel())
}
}
}
//Subscribe to location changes.
client.requestLocationUpdates(request, callBack, Looper.getMainLooper())
awaitClose { client.removeLocationUpdates(callBack) }
The cause is I still did use the onPermissionResult() in my fragment, so after switch to the new requestPermissionLauncher = registerForActivityResult, and call onForegroundPermissionApproved() instead of in init of ViewModel, after approve the location permission. Everything work properly.
Related
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
I'm using FusedLocationProvider in my app and I noticed that when my app is in the background and I start some other app that contains Google Map my original app starts receiving location updates extremely fast (like 1 update per second) despite setting up the fastest interval.
I know that I should unregister when going to background etc but this is not the case here.
Any ideas why this might happen or where I can report it to Google?
This is the activity I start it from (I've removed couple of permissions check just for the visibility)
The full repo can be found here
class MainActivity : AppCompatActivity() {
private val locationController by lazy { LocationController.getInstance(applicationContext) }
lateinit var button: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button = findViewById(R.id.button)
button.setOnClickListener {
if (locationController.isStarted) {
locationController.stop()
button.text = "START LOCATION UPDATES"
} else {
locationController.start()
button.text = "STOP LOCATION UPDATED"
}
}
}
And the LocationController looks like this:
class LocationController(context: Context) {
companion object {
#Volatile private var INSTANCE: LocationController? = null
fun getInstance(context: Context): LocationController {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: LocationController(context).also { INSTANCE = it }
}
}
}
private val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context)
private val locationRequest by lazy {
LocationRequest.create()
.setInterval(INTERVAL_MILLIS)
.setFastestInterval(FASTEST_INTERVAL_MILLIS)
.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
}
private val locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
super.onLocationResult(locationResult)
Log.d("boom", "onLocationResult! ${locationResult.lastLocation}")
}
override fun onLocationAvailability(locationAvailability: LocationAvailability) {
super.onLocationAvailability(locationAvailability)
}
}
var isStarted: Boolean = false
#SuppressLint("MissingPermission")
fun start() {
fusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper())
.addOnSuccessListener {
Log.d("boom", "requestLocationUpdates success!")
}
isStarted = true
}
fun stop() {
fusedLocationProviderClient.removeLocationUpdates(locationCallback)
.addOnSuccessListener {
Log.d("boom", "removeLocationUpdates success!")
}
isStarted = false
}
The constant values I experience it with are:
const val INTERVAL_MILLIS = 30_000L
const val FASTEST_INTERVAL_MILLIS = 10_000L
I think my observable is set incorrectly here. I am using Retrofit2 + Moshi as the deserializer, and the API call from Retrofit is working.
But once I make the API call, I am trying to set up the Observable in my Activity and then use the API call data from the data class.
Here is my view model code:
class DealsViewModel(val repository: MainRepository) : ViewModel() {
val movieList = MutableLiveData<List<DealItems>>()
var job: Job? = null
val loading = MutableLiveData<Boolean>()
val errorMessage = MutableLiveData<String>()
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
onError("Exception handled: ${throwable.localizedMessage}")
}
fun getMovies() {
viewModelScope.launch{
// View Model Scope gives the Coroutine that will be canceled when the ViewModel is cleared.
job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val items = repository.getProduct()
withContext(Dispatchers.Main) {
if (items.isNullOrEmpty()) {
loading.value = false
// put error message in here later
} else {
dealList.postValue(items)
return#withContext
}
}
}
}
}
private fun onError(message: String) {
errorMessage.value = message
loading.value = false
}
override fun onCleared() {
super.onCleared()
job?.cancel()
}
}
And here is my MainActivity code.
I am using JetpackCompose in my activity, LiveData for the API response container. In my main repository is where I am validating a successful API response and then the coroutines for the call are inside of the view model.
My API call is successful, but I am not sure where to call the ViewModel.GetMovies() inside of the activity and I am not sure if the observables are set properly and/or where to pass the API's livedata into my composable function.
Thanks for any help you can provide. I am new to android and trying to use Coroutines for the first time.
class MainActivity: ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val retrofitService = RetrofitService.getInstance()
val viewModel = ViewModelProvider(this,
MyViewModelFactory(MainRepository(retrofitService = retrofitService))).get(DealsViewModel::class.java)
// viewModel.getProducts()
setContent {
myApp {
MyScreenContent()
}
viewModel.movieList.observe(
this, { it ->
if( it != null) {
it.forEach {
var movieLocation = it.movieLocation
val description = it.description
val id = it.id
val title = it.title
val regularPrice = it.regularPrice
}
}
})
return#setContent
}
viewModel.errorMessage.observe(this, {
Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
})
viewModel.loading.observe(
this,
Observer {
if (it) {
}
})
}
}
I assume that it always depends when should you call especially in the activity we have many lifecycles; however, the best way is to use the .also on the livedata/stateflow lazy creation so that you do guarantee as long as the view model is alive, the getMovies is called only one time, and also guarantee the service itself is not called unless someone is listening to it.
You may check the full documentation in this link
Here is a code example
class MyViewModel : ViewModel() {
private val users: MutableLiveData<List<User>> by lazy {
MutableLiveData<List<User>>().also {
loadUsers()
}
}
fun getUsers(): LiveData<List<User>> {
return users
}
private fun loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
When using this code, you do not have to call getMovies at all in the activity, you just listen to the observer.
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.
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.