Combine multiple LiveData values into one using MediatorLiveData - android

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.

Related

How to test ViewModel + Flow

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.

Can't add Firebase document Id to dataClass

I have a data class for data that come from user entries. İt is carrying this data to Firebase. This data class also includes documentId variable which is a empty string by default. I want to add document Id's that Firebase created automatically. I tried every way I could think of. But it takes default value in any way.
Here are the four code snippets about this issue. Data class, adding data activity, and retrieving data activity and their View Models.
Dataclass:
data class AnalyzeModel(
var concept: String?="",
var reason: String?="",
var result: String?="",
var rrRatio: Double?=0.0,
var tarih: Timestamp=Timestamp.now(),
var tradingViewUrl: String="",
var id : String="")
AddAnalyzeActivity, addData function:
fun addData(view: View) {
val tarih = com.google.firebase.Timestamp.now()
val rr = rrText.text.toString()
var doubleRR = rr.toDoubleOrNull()
if (doubleRR == null) { doubleRR = 0.0 }
val analyzeDTO = AnalyzeModel(
conceptText.text.toString(),
reasonForText.text.toString(),
resultAddingText.text.toString(),
doubleRR,
tarih,
chartImage.text.toString()
)
viewModel.save(analyzeDTO)
val intent = Intent(this, PairDetailActivity::class.java)
startActivity(intent)
finish()
}
AddAnalyze ViewModel, save function:
fun save(data: AnalyzeModel) {
database.collection(dbCollection!!).document("Specified").collection("Pairs")
.document(chosenPair!!)
.collection("Analysis")
.add(data)
.addOnFailureListener { exception ->
exception.printStackTrace()
Toast.makeText(getApplication(), exception.localizedMessage, Toast.LENGTH_LONG).show()
}
}
PairViewModel, retrieveData function:
private fun retrieveData() {
val docRef = collectionRef.orderBy("tarih", Query.Direction.DESCENDING)
docRef.addSnapshotListener { value, error ->
try {
if (value != null && !value.isEmpty) {
val allAnalysis= ArrayList<AnalyzeModel>()
val documents = value.documents
documents.forEach {
val analyze = it.toObject(AnalyzeModel::class.java)
if (analyze!=null){
allAnalysis.add(analyze)
}
}
list.value = allAnalysis
} else if (error != null) {
Toast.makeText(Application(), error.localizedMessage, Toast.LENGTH_LONG).show()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
I want to add document IDs that Firebase created automatically.
To solve this, you only need to annotate the field with #DocumentId.
data class AnalyzeModel(
var concept: String?="",
var reason: String?="",
var result: String?="",
var rrRatio: Double?=0.0,
var tarih: Timestamp=Timestamp.now(),
var tradingViewUrl: String="",
#DocumentId 👈
var id : String=""
)
Be also sure to have the latest version of Firestore.

How to transform LiveData<List<Entity>> to LiveData<List<Date>>

My project is an expense tracker where I show a list of Dates under which I have a list of expenses that happened on those dates. I have nested RecyclerViews. Parent RecyclerView is a list of unique dates of all expenses. Child RecyclerView is list of expenses (viewed, of course, under unique dates).
My ViewModel has a list of LiveData of ExpenseEntity. The ViewModel has to have a list of LiveData of Date which contains unique dates. I get my list of ExpenseEntity from a Room database.
My main fragments observes the LiveData of ExpenseEntities because then is when I need to update my parent and child recyclerviews.
I cannot figure out how to use Transformations.map to have a live transforming list of unique dates. How should I make sure the LiveData of Dates is always updated once LiveData of ExpenseEntity is updated?
MainActivityViewModel.kt
class MainActivityViewModel(private val expenseDao: ExpenseDao) : ViewModel() {
val allExpenses : LiveData<List<ExpenseEntity>> = expenseDao.fetchAllExpenses().asLiveData()
val uniqueDates : LiveData<List<Date>> = Transformations.map(allExpenses) {
it.map { expense ->
expense.date!!
}.distinct()
}
...
}
ExpensesFragment.kt
val factory = MainActivityViewModelFactory((activity?.application as SimpleExpenseTrackerApp).db.expenseDao())
expensesViewModel = ViewModelProvider(this, factory).get(MainActivityViewModel::class.java)
binding.rvParentExpenseDates.layoutManager = LinearLayoutManager(requireContext())
expensesViewModel.allExpenses.observe(viewLifecycleOwner){ expensesList ->
if (expensesList.isNotEmpty()){
binding.rvParentExpenseDates.adapter = expensesViewModel.uniqueDates.value?.let {
ParentDatesAdapter(it, expensesList) { expenseId ->
Toast.makeText(requireContext(), "Clicked expense with id: $expenseId", Toast.LENGTH_LONG).show()
}
}
binding.rvParentExpenseDates.visibility = View.VISIBLE
} else {
binding.rvParentExpenseDates.visibility = View.GONE
}
}
ExpenseEntity.kt
#Entity(tableName = "expense-table")
data class ExpenseEntity(
#PrimaryKey(autoGenerate = true)
val id: Int = 0,
#ColumnInfo(name = "date-time")
val dateTime : Date?,
val date : Date?,
#ColumnInfo(name = "account-type")
val accountType : String = "",
val category : String = "",
val amount : Double = 0.0,
val currency : String = "",
val note : String = ""
)
Per the documentation:
These methods permit functional composition and delegation of LiveData
instances. The transformations are calculated lazily, and will run
only when the returned LiveData is observed. Lifecycle behavior is
propagated from the input source LiveData to the returned one.
The issue here is that you never observe the transformed LiveData (uniqueDates) -- you only inspect the value, so the transformation is never applied.
One option, if you need both together, is to map into a joined view:
class MainActivityViewModel(private val expenseDao: ExpenseDao) : ViewModel() {
val allExpenses : LiveData<List<ExpenseEntity>> =
expenseDao.fetchAllExpenses().asLiveData()
val allAndUniqueDatedExpenses: LiveData<Pair<List<ExpenseEntity>, List<Date>> =
Transformations.map(allExpenses) { expenses ->
expenses to expenses.mapNotNull { it.date }.distinct()
}
}
Then simply observe this joined value:
expensesViewModel.allAndUniqueDatedExpenses.observe(this) { (expenses, dates) ->
binding.rvParentExpenseDates.adapter =
ParentDatesAdapter(dates, expenses) { expenseId ->
Toast.makeText(...).show()
}
}
However, I would argue here you don't really need another LiveData transformation. Simply do the transformation inline:
expensesViewModel.allExpenses.observe(this) { expenses ->
val dates = expenses.mapNotNull { it.date }.distinct()
binding.rvParentExpenseDates.adapter =
ParentDatesAdapter(dates, expenses) { expenseId ->
Toast.makeText(...).show()
}
}

How to observe data inside viewmodel?

I have connect my android application to firebase and am using it to retrieve Authentication details and data from firestone. I am using an MVVM architecture and live data for this. The problem is that I need to retrieve email address first and then used this data to query the firestone which contain documents with ID = emailID. You can see my viewmodel. The value for the emailID is null when every I run this. How can I accomplish this while following the MVVP style of coding ?
#Edit: I need to understand how can check if the live data has been initialised with a value in the case where one livedata value depends on the other.
class ProfileViewModel(): ViewModel() {
var random =""
private var _name = MutableLiveData<String>()
val userName
get()=_name
private var _post = MutableLiveData<String>()
val userPost
get()=_post
private var _imgUrl = MutableLiveData<Uri>()
val userImgUrl
get()=_imgUrl
private var _emailId = MutableLiveData<String>()
val userEmailId
get()=_emailId
init{
getUserDataFromProfile()
getUserPostFromFirestone()
}
private fun getUserPostFromFirestone() {
val mDatabaseInstance: FirebaseFirestore = FirebaseFirestore.getInstance()
// _emailId.observe(getApplication(), Observer {
//
// } )
if(_emailId.value!=null){
mDatabaseInstance.collection("users").document(_emailId.value)
.get()
.addOnCompleteListener { task ->
if (task.isSuccessful) {
_post.value = task.result?.data?.get("post").toString()
} else {
// Log.w("firestone", "Error getting documents.", task.exception)
_post.value = "Unable to Retrieve"
}
}
}
}
private fun getUserDataFromProfile() {
val mAuth = FirebaseAuth.getInstance()
val currentUser = mAuth.currentUser
random = currentUser?.displayName!!
_name.value = currentUser?.displayName
_post.value = "Unknown"
_imgUrl.value = currentUser?.photoUrl
_emailId.value = currentUser?.email
}
}
If you write a wrapper over the Firebase call and expose it as a LiveData (or, in this case, I'll pretend it's wrapped in a suspendCoroutineCancellable), in which case whenever you want to chain stuff, you either need MediatorLiveData to combine multiple LiveDatas into a single stream (see this library I wrote for this specific purpose) or just switchMap.
private val auth = FirebaseAuth.getInstance()
val imgUrl: LiveData<Uri> = MutableLiveData<Uri>(auth.currentUser?.photoUrl)
val emailId: LiveData<String> = MutableLiveData<String>(auth.currentUser?.email)
val post = emailId.switchMap { emailId ->
liveData {
emit(getUserByEmailId(emailId))
}
}
you can set observer to LiveData and remove it when you don't need it:
class ProfileViewModel : ViewModel() {
private val _email = MutableLiveData<String>()
private val emailObserver = Observer<String> { email ->
//email is here
}
init {
_email.observeForever(emailObserver)
}
override fun onCleared() {
_email.removeObserver(emailObserver)
super.onCleared()
}
}
Try using coroutines for the sequential execution of the code. so once you get the output of one and then the second one starts executing. If this isnt working Please let me know i can try help you.
init{
viewModelScope.launch{
getUserDataFromProfile()
getUserPostFromFirestone()
}
}

Android (Kotlin) - How do I wait for an asynchronous task to finish?

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
}

Categories

Resources