I have a datastore in my android app where I am storing my profile details. and retrieving as follows
suspend fun saveUser(user: User) {
dataStore.edit {
it[USER_ID] = user.id
it[USER_NAME] = user.name
it[USER_MOBILE] = user.phone
it[USER_EMAIL] = user.email
it[USER_IMAGE] = user.image
it[USER_ADDRESS] = user.address
}
}
val userDate = dataStore.data
.catch { e ->
if (e is IOException) {
Log.e("PREFERENCE", "Error reading preferences", e)
emit(emptyPreferences())
} else {
throw e
}
}
.map { pref ->
val userId = pref[USER_ID] ?: ""
val userName = pref[USER_NAME] ?: ""
val userEmail = pref[USER_EMAIL] ?: ""
val userImage = pref[USER_IMAGE] ?: ""
val userPhone = pref[USER_MOBILE] ?: ""
val userAddress = pref[USER_ADDRESS] ?: ""
User(
name = userName,
image = userImage,
address = userAddress,
phone = userPhone,
id = userId,
email = userEmail
)
}
Along with it I am saving the availibility status of the User
suspend fun saveIsAvailable(boolean: Boolean) {
dataStore.edit {
it[USER_IS_AVAILABLE] = boolean
}
}
I am collecting user profile details like this in my viewmodel
viewModelScope.launch(Default) {
RiderDataStore.userDate.collect {
user.postValue(it)
}
}
Whenever I change the User availibility my user details flow also gets triggered which is unneccessary and causes ui jittering (image reloads). Why does this happen and how to enable the flow to only trigger if the data changes specifically of the user detail.
This is because you update a user property (in DataStore) and at the same time with userDate.collect you're observing all changes made to the user (in DataStore). Your current code has no way to distinguish between "good" and "bad" updates of the user.
Since you seem to ignore the availability in your DataStore Flow called userDate, your returned User objects should indeed stay identical after availability changes. The default behavior for Kotlin Flow is to emit on every change, even if the data is identical. But you can fix that simply by adding a .distinctUntilChanged() after the map operator like:
val userDate = dataStore.data
.catch { e ->
if (e is IOException) {
Log.e("PREFERENCE", "Error reading preferences", e)
emit(emptyPreferences())
} else {
throw e
}
}
.map { pref ->
val userId = pref[USER_ID] ?: ""
val userName = pref[USER_NAME] ?: ""
val userEmail = pref[USER_EMAIL] ?: ""
val userImage = pref[USER_IMAGE] ?: ""
val userPhone = pref[USER_MOBILE] ?: ""
val userAddress = pref[USER_ADDRESS] ?: ""
User(
name = userName,
image = userImage,
address = userAddress,
phone = userPhone,
id = userId,
email = userEmail
)
}.distinctUntilChanged()
See also docs. It makes sure identical data is not emited over and over.
Related
When I use the code below I can get data from firebase but when I want to access it with MVVM it returns null.
database.collection("Order")
.get()
.addOnCompleteListener { it ->
if (it.isSuccessful) {
val itemName = it.result.documents[0].data?.get("itemName")
val id = it.result.documents[0].data?.get("id")
It returns null inside Order.kt. I don't realize what the problem is there. I can't find any similar questions here.
FirebaseOrderService.kt
object FirebaseOrderService {
private const val TAG = "FirebaseOrderService"
suspend fun getOrderData(): Order? {
val db = FirebaseFirestore.getInstance()
return try {
db.collection("Order")
.document().get().await().toOrder()
} catch (e: Exception) {
Log.e(TAG, "Error getting order details", e)
FirebaseCrashlytics.getInstance().log("Error getting order details")
FirebaseCrashlytics.getInstance().setCustomKey("id", "1")
FirebaseCrashlytics.getInstance().recordException(e)
null
}
}
SuccessShoppingViewModel.kt
class SuccessShoppingViewModel: ViewModel() {
private val _orderList = MutableLiveData<Order>()
val order: LiveData<Order> = _orderList
init {
viewModelScope.launch {
_orderList.value = FirebaseOrderService.getOrderData()
_orderList
}
}
Order.kt
#Parcelize
data class Order(
val id: String = "",
val picUrl: String = "",
val itemName: String = "",
val itemPrice: Double = 0.0,
val itemAmount: String = "",
val itemQuantatiy: Int = 0
) : Parcelable {
companion object {
fun DocumentSnapshot.toOrder(): Order? {
return try {
val id = getString("id")!!
val picUrl = getString("picUrl")!!
val itemName = getString("itemName")!!
val itemPrice = getLong("itemPrice")?.toDouble()!!
val itemAmount = getString("itemAmount")!!
val itemQuantatiy = getLong("itemQuantatiy")?.toInt()!!
Order(id, picUrl, itemName, itemPrice, itemAmount, itemQuantatiy)
} catch (e: Exception) {
Log.e(TAG, "Error converting order", e)
FirebaseCrashlytics.getInstance().log("Error converting order")
FirebaseCrashlytics.getInstance().setCustomKey("id", id)
FirebaseCrashlytics.getInstance().recordException(e)
null
}
}
private const val TAG = "Order"
}
}
You're getting null because of the following line of code:
db.collection("Order")
.document().get().await().toOrder()
When you are using the above line of code, it means that you are creating a reference to a document with a random ID. Calling CollectionReferenc#document() method, without passing any arguments:
Returns a DocumentReference pointing to a new document with an auto-generated ID within this collection.
So what you're actually doing, you're creating a reference that points to a document that doesn't exist. To solve this problem, you have to pass the ID of the document to the document() function like this:
db.collection("Order")
.document("eBW6...zIO1").get().await().toOrder()
// 👆
I'm doing a small project to learn flow and the latest Android features, and I'm currently facing the viewModel's testing, which I don't know if I'm performing correctly. can you help me with it?
Currently, I am using a use case to call the repository which calls a remote data source that gets from an API service a list of strings.
I have created a State to control the values in the view model:
data class StringItemsState(
val isLoading: Boolean = false,
val items: List<String> = emptyList(),
val error: String = ""
)
and the flow:
private val stringItemsState = StringtemsState()
private val _stateFlow = MutableStateFlow(stringItemsState)
val stateFlow = _stateFlow.asStateFlow()
and finally the method that performs all the logic in the viewModel:
fun fetchStringItems() {
try {
_stateFlow.value = stringItemsState.copy(isLoading = true)
viewModelScope.launch(Dispatchers.IO) {
val result = getStringItemsUseCase.execute()
if (result.isEmpty()) {
_stateFlow.value = stringItemsState
} else {
_stateFlow.value = stringItemsState.copy(items = result)
}
}
} catch (e: Exception) {
e.localizedMessage?.let {
_stateFlow.value = stringItemsState.copy(error = it)
}
}
}
I am trying to perform the test following the What / Where / Then pattern, but the result is always an empty list and the assert verification always fails:
private val stringItems = listOf<String>("A", "B", "C")
#Test
fun `get string items - not empty`() = runBlocking {
// What
coEvery {
useCase.execute()
} returns stringItems
// Where
viewModel.fetchStringItems()
// Then
assert(viewModel.stateFlow.value.items == stringItems)
coVerify(exactly = 1) { viewModel.fetchStringItems() }
}
Can someone help me and tell me if I am doing it correctly? Thanks.
I have an email, password, and phone number, that are all live data values updated in real time by a fragment as the user types.
How can I have a live data variable that observes all three of those variables, and returns something based on all of their combined results?
ViewModel
class AuthenticationViewModel: ViewModel() {
var email: MutableLiveData<String> = MutableLiveData("")
var password: MutableLiveData<String> = MutableLiveData("")
var phoneNumber: MutableLiveData<Int> = MutableLiveData(0)
val isFormValid: MediatorLiveData<Boolean> = {
// if email is valid
// and if password is valid
// and if phoneNumber is valid
// return true
// How do I do this?
}
}
Fragment
binding.emailInput.addTextChangedListener { email ->
viewModel.email.value = email.toString()
}
viewModel.isFormValid.observe(this, {
// do what I want in real time
})
Try adding each liveData as a source like this and check the other liveData's value when one of them trigger a change:
ViewModel
class AuthenticationViewModel: ViewModel() {
val email: MutableLiveData<String> = MutableLiveData("")
val password: MutableLiveData<String> = MutableLiveData("")
val phoneNumber: MutableLiveData<Int> = MutableLiveData(0)
val isFormValid: MediatorLiveData<Boolean> = MediatorLiveData().apply {
addSource(email) { emailValue -> isValidEmail(emailValue) && isValidPassword(password.value) && isValidPhoneNumber(phoneNumber.value) }
addSource(password) { passwordValue -> isValidEmail(email.value) && isValidPassword(passwordValue) && isValidPhoneNumber(phoneNumber.value) }
addSource(phoneNumber) { phoneNumberValue -> isValidEmail(email.value) && isValidPassword(password.value) && isValidPhoneNumber(phoneNumberValue) }
}
}
And then just observe the livedata as usual:
Fragment
binding.emailInput.addTextChangedListener { email ->
viewModel.email.value = email.toString()
}
viewModel.isFormValid.observe(this, {
// do what I want in real time
})
solution inspired by https://medium.com/codex/how-to-use-mediatorlivedata-with-multiple-livedata-types-a40e1a59e8cf , kinda the same but not using Triple and a new class for it, also you can add as many as you want.
I am trying to write some basic information about users to a Firebase realtime database, but when running setValue() nothing happens in the console and after many hours of trying I still can't figure out the problem.
This is the code in the fragment that calls the registerUser function:
registerButton.setOnClickListener {
firstNameLayout.error = null
lastNameLayout.error = null
emailFieldLayout.error = null
passwordFieldLayout.error = null
var fieldCheck = true
if(TextUtils.isEmpty(firstNameField.text)) {
firstNameLayout.error = "Please Enter First Name";
fieldCheck = false
}
if (TextUtils.isEmpty(lastNameField.text)) {
lastNameLayout.error = "Please Enter Last Name"
fieldCheck = false
}
if(TextUtils.isEmpty(emailField.text)) {
emailFieldLayout.error = "Please Enter Email Address";
fieldCheck = false
// Todo: Make sure format is correct
}
if(TextUtils.isEmpty(passwordField.text)){
passwordFieldLayout.error = "Please Choose a Password"
fieldCheck = false
// Todo: Stricter password requirements
}
if(!fieldCheck) {
return#setOnClickListener
}
registerButton.startAnimation()
val fName = firstNameField.text.toString().trim()
val lName = lastNameField.text.toString().trim()
val email = emailField.text.toString().trim()
val password = passwordField.text.toString().trim()
try {
registerButton.doneLoadingAnimation(2, bitmap)
(activity as LoginRegister).registerUser(email, password, fName, lName)
} catch (e: Exception) {
Toast.makeText(activity, e.message, Toast.LENGTH_SHORT).show()
registerButton.revertAnimation()
}
}
This is the registerUser function body from the parent activity of the fragment:
fun registerUser(email: String, password: String, fName: String, lName: String) {
//Registrerer med Firebase Authentication
auth.createUserWithEmailAndPassword(email, password)
.addOnCompleteListener(this) { task ->
if (task.isSuccessful) {
// Sign in success, update UI with the signed-in user's information
Log.d(TAG, "createUserWithEmail:success")
val user : FirebaseUser = task.result!!.user!!
val uid: String = user.uid
val dbUser = User(fName, lName, email)
writeNewUser(uid, dbUser)
val intent = Intent(this#LoginRegister, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
intent.putExtra("user_email", email)
intent.putExtra("user_first_name", fName)
intent.putExtra("user_last_name", lName)
startActivity(intent)
finish()
} else {
// If sign in fails, display a message to the user.
Log.w(TAG, "createUserWithEmail:failure", task.exception)
Toast.makeText(baseContext, "Authentication failed.", Toast.LENGTH_SHORT).show()
}
}
}
This is the writeNewUser function body
fun writeNewUser(userId: String, user: User) {
database = FirebaseDatabase.getInstance().getReference("Users").child(userId)
database.child("users").child(userId).setValue(user)
}
The User object is instantiated from this kotlin class:
data class User(val fName: String? = null, val lName: String? = null, val email: String? = null) {
val firstName = fName?.capitalize(Locale.ROOT)
val lastName = lName?.capitalize(Locale.ROOT)
val emailAddress = email
}
Firebase:
Firebase nodes
Anyone know the reason behind this?
put the URL of database in the getInstance() was the solution for me, now it works perfect
example:
private val database = FirebaseDatabase.getInstance("https://conect-c91b3-default-rtdb.firebaseio.com/").getReference()
// to test if is working
database.child("users").setValue("test")
I solved it, I was using another database location than the default, and I didn't catch the fact that I needed to pass the database URL in the getInstance() method. Passing the correct database url fixed the issue
fun writeNewUser(userId: String, user: User) {
database = FirebaseDatabase.getInstance().getReference("Users").child(userId)
database.child("users").child(userId).setValue(user)
}
I guess you duplicate a couple of child nodes in above snippet as it implies a group of users has a single user, and this particular user has a group of users, doesn't make sense, so you can remove these duplicates:
fun writeNewUser(userId: String, user: User) {
database = FirebaseDatabase.getInstance().getReference("Users").child(userId)
database.setValue(user)
}
UPDATE:
The firebase nodes are case-sensitive, so change "Users" to lowercase "users"
fun writeNewUser(userId: String, user: User) {
database = FirebaseDatabase.getInstance().getReference("users").child(userId)
database.setValue(user)
}
I am new to Android and Kotlin and am currently working on a centralized API router class.
To achieve this I am using the Fuel Framework.
For the doAsync function, I use the Anko for Kotlin library.
To retrieve an authorization token from the API I currently use this method:
private fun Login(username: String, password: String, callback: (Map<Boolean, String>) -> Unit) {
"/auth/token.json".httpPost()
.header(mapOf("Content-Type" to "application/json"))
.body("""{"username":"$username", "password":"$password"}""", Charsets.UTF_8)
.response { request, response, result ->
request.headers.remove("Accept-Encoding")
when (result) {
is Result.Failure -> {
// val data = result.get()
val ex = result.getException()
val serverResponseJson = response.data.toString(Charsets.UTF_8)
var exceptionMessage = ex.message
val jelement = JsonParser().parse(serverResponseJson)
val jobject = jelement.asJsonObject
val serverResponseError = if (jobject.has("Error")) jobject.get("Error").asString else jobject.get("detail").asString
callback(mapOf(Pair(false, serverResponseError)))
}
is Result.Success -> {
val data = result.get()
val returnJson = data.toString(Charsets.UTF_8)
Log.println(Log.ASSERT, "RESULT_LOGIN", returnJson)
callback(mapOf(Pair(true, returnJson)))
}
}
}
}
I invoke this login method at
val btnLogin = findViewById<Button>(R.id.btn_login)
btnLogin.setOnClickListener { _ ->
doAsync {
val username = findViewById<EditText>(R.id.input_username_login)
val password = findViewById<EditText>(R.id.input_password_login)
Login(username.text.toString(), password.text.toString()) {
// Request was successful
if (it.containsKey(true)) {
// Parse return Json
// e.g. {"id":"36e8fac0-487a-11e8-ad4e-c471feb11e42","token":"d6897a230fd7739e601649bf5fd89ea4b93317f6","expiry":"2018-04-27T17:49:48.721278Z"}
val jelement = JsonParser().parse(it.getValue(true))
val jobject = jelement.asJsonObject
// save field for class-scope access
Constants.token = jobject.get("token").asString
Constants.id = jobject.get("id").asString
}
else{
Toast.makeText(this#LoginActivity, it.getValue(false), Toast.LENGTH_SHORT).show()
}
}
}[30, TimeUnit.SECONDS]
var test = Constants.id;
}
In a separate Constants class, I store the token and id like this:
class Constants {
companion object {
val baseUrl: String = "BASE_URL_TO_MY_API"
val contentTypeJson = "application/json"
lateinit var STOREAGE_PATH: String
// current user details
lateinit var id: String
lateinit var token: String
lateinit var refresh_token: String
// logged in User
lateinit var user: User
}
How do I make sure that the test variable is set after the asynchronous task is done? Currently, I run into
lateinit property id has not been initialized
I have come across the option to limit the task to a timeout such as I have done with [30, TimeUnit.SECONDS], unfortunately, this did not help.
Thanks for the help! Cheers.
I think the problem is where you want to access the result:
val btnLogin = findViewById<Button>(R.id.btn_login)
btnLogin.setOnClickListener { _ ->
doAsync {
val username = findViewById<EditText>(R.id.input_username_login)
val password = findViewById<EditText>(R.id.input_password_login)
var test: String? = null
Login(username.text.toString(), password.text.toString()) {
// Request was successful
if (it.containsKey(true)) {
// Parse return Json
// e.g. {"id":"36e8fac0-487a-11e8-ad4e-c471feb11e42","token":"d6897a230fd7739e601649bf5fd89ea4b93317f6","expiry":"2018-04-27T17:49:48.721278Z"}
val jelement = JsonParser().parse(it.getValue(true))
val jobject = jelement.asJsonObject
// save field for class-scope access
Constants.token = jobject.get("token").asString
Constants.id = jobject.get("id").asString
}
else{
Toast.makeText(this#LoginActivity, it.getValue(false), Toast.LENGTH_SHORT).show()
}
}
test = Constants.id // here test variable surely set if result was successful, otherwise it holds the null value
test?.let{
resultDelivered(it)
}
}[30, TimeUnit.SECONDS]
}
fun resultDelivered(id: String){
// here we know that the async job has successfully finished
}