How to observe data inside viewmodel? - android

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()
}
}

Related

How to use firebase to update your Android App

I've seen that there are ways to update an app with Firebase Remote Config. Some sort of "Force Update" Notification. If anyone can explain it to me, that would be great.
How to use Firebase to update your Android App?
There are multiple ways in which you can update an Android app. The first one would be to store data in a database. Firebase has two real-time databases, Cloud Firestore and the Realtime Database. You can one or the other, according to the use case of your app. For that I recommend you check the following resource:
https://firebase.google.com/docs/database/rtdb-vs-firestore
When it comes to Remote Config, please notice that nowadays you can propagate Remote Config updates in real-time. That being said, there is no need to force anything. So I highly recommend that a look at that.
For Force update in a simple case the idea is
with firebase remort config sends the version number which you want for your application to be forced
then compare remort version with the local application version
if there is a mismatch then show a permanent dialog (cancelable=false) with a button when the user clicks on that button to open the application in the play store .
Check out this Small Class created for force update with remort config
class ForceUpdateChecker(private val context: Context, private val onUpdateNeededListener: OnUpdateNeededListener?) {
interface OnUpdateNeededListener {
fun onUpdateNeeded(updateUrl: String?)
}
fun check() {
val remoteConfig = FirebaseRemoteConfig.getInstance()
if (remoteConfig.getBoolean(KEY_UPDATE_REQUIRED)) {
val currentVersion = remoteConfig.getString(KEY_CURRENT_VERSION)
val appVersion = getAppVersion(context)
val updateUrl = remoteConfig.getString(KEY_UPDATE_URL)
if (!TextUtils.equals(currentVersion, appVersion)
&& onUpdateNeededListener != null
) {
onUpdateNeededListener.onUpdateNeeded(updateUrl)
}
}
}
private fun getAppVersion(context: Context): String {
var result = ""
try {
result = context.packageManager
.getPackageInfo(context.packageName, 0).versionName
result = result.replace("[a-zA-Z]|-".toRegex(), "")
} catch (e: PackageManager.NameNotFoundException) {
Log.e(TAG, e.message!!)
}
return result
}
class Builder(private val context: Context) {
private var onUpdateNeededListener: OnUpdateNeededListener? = null
fun onUpdateNeeded(onUpdateNeededListener: OnUpdateNeededListener?): Builder {
this.onUpdateNeededListener = onUpdateNeededListener
return this
}
fun build(): ForceUpdateChecker {
return ForceUpdateChecker(context, onUpdateNeededListener)
}
fun check(): ForceUpdateChecker {
val forceUpdateChecker = build()
forceUpdateChecker.check()
return forceUpdateChecker
}
}
companion object {
private val TAG = ForceUpdateChecker::class.java.simpleName
const val KEY_UPDATE_REQUIRED = "force_update_required"
const val KEY_CURRENT_VERSION = "force_update_current_version"
const val KEY_UPDATE_URL = "force_update_store_url"
fun with(context: Context): Builder {
return Builder(context)
}
}}
Call this like this in baseActivity (or from your landing page just not in splash screen)
ForceUpdateChecker.with(this).onUpdateNeeded(this).check();
In application on create add this
val firebaseRemoteConfig = FirebaseRemoteConfig.getInstance()
// set in-app defaults
val remoteConfigDefaults: MutableMap<String, Any> = HashMap()
remoteConfigDefaults[ForceUpdateChecker.KEY_UPDATE_REQUIRED] = false
remoteConfigDefaults[ForceUpdateChecker.KEY_CURRENT_VERSION] = "1.0"
remoteConfigDefaults[ForceUpdateChecker.KEY_UPDATE_URL] =
"https://play.google.com/store/apps/details?id=com.com.classified.pems"
firebaseRemoteConfig.setDefaultsAsync(remoteConfigDefaults)
firebaseRemoteConfig.fetch(60) // fetch every minutes
.addOnCompleteListener { task ->
if (task.isSuccessful) {
Log.d(TAG, "remote config is fetched.")
firebaseRemoteConfig.fetchAndActivate()
}
}

Why Room LiveData not returning the values?

I have a problem with Room that return LiveData.
I create Dao with function to returns list of data. I suppose to return as LiveData. But, it doesn't work as expected.
Dao function
#Transaction
#Query("SELECT * FROM AllocationPercentage WHERE id IN (:ids)")
fun getByIds(ids: List<Long>): LiveData<List<AllocationPercentageWithDetails>>
Here is how I observe it inside the ViewModel:
class AllocationViewModel(
private val getAllocationByIdUseCase: GetAllocationByIdUseCase,
private val getDetailByIdUseCase: GetAllocationPercentageByIdUseCase
) : ViewModel() {
var allocationUiState: LiveData<AllocationUiState> = MutableLiveData()
private set
var allocationPercentageUiState: LiveData<List<AllocationPercentageUiState>> = MutableLiveData()
private set
val mediatorLiveData = MediatorLiveData<List<AllocationPercentageUiState>>()
fun getAllocationById(allocationId: Long) = viewModelScope.launch(Dispatchers.IO) {
val result = getAllocationByIdUseCase(allocationId) // LiveData
allocationUiState = Transformations.map(result) {
AllocationUiState(allocation = it.allocation)
}
mediatorLiveData.addSource(result) { allocation ->
Log.d(TAG, "> getAllocationById")
val ids = allocation.percentages.map { percentage -> percentage.id }
val detailResult: LiveData<List<AllocationPercentageWithDetails>> =
getDetailByIdUseCase(ids) // LiveData
allocationPercentageUiState = Transformations.map(detailResult) { details ->
Log.d(TAG, ">> Transform : $details")
details.map {
AllocationPercentageUiState(
id = it.allocationPercentage.id,
percentage = it.allocationPercentage.percentage,
description = it.allocationPercentage.description,
currentProgress = it.allocationPercentage.currentProgress
)
}
}
}
}
}
The allocationPercentageUiState is observed by Fragment.
Log.d(TAG, "observeViewModel: ${it?.size}")
val percentages = it ?: return#observe
setAllocationPercentages(percentages) // update UI
}
allocationViewModel.mediatorLiveData.observe(viewLifecycleOwner) {}
And getDetailByIdUseCase just a function which directly return result from Dao.
class GetAllocationPercentageByIdUseCase(private val repository: AllocationPercentageRepository) {
operator fun invoke(ids: List<Long>): LiveData<List<AllocationPercentageWithDetails>> {
return repository.getAllocationPercentageByIds(ids)
}
}
Any idea why? Thank you.
Combining var with LiveData or MutableLiveData doesn't make sense. It defeats the purpose of using LiveData. If something comes along and observes the original LiveData that you have in that property, it will never receive anything. It will have no way of knowing there's a new LiveData instance it should be observing instead.
I can't exactly tell you how to fix it because your code above is incomplete, so I can't tell what you're trying to do in your mapping function, or whether it is called in some function vs. during ViewModel initialization.

Combine multiple LiveData values into one using MediatorLiveData

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.

Retrieve data from Android DataStore and save to global var

I am making android app and I wants save configuration in Android DataStore. I have created a class and the values from EditText are correct save to DataStore. I using tutorial from YouTube: https://www.youtube.com/watch?v=hEHVn9ATVjY
I can view the configuration in the config view correctly (textview fields get the value from the datastore):
private fun showConfigurationInForm(){
mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
mainViewModel.readMqttAddressFlow.observe(this) { mqqtAdress ->
binding.conMqttAddress.setText(mqqtAdress)
}
}
This function show actual config in EditText, and this is great
But the config I will use to connect to MQTT Server, and how can I save the config to Varchar and use to another function?
I create var in class:
class ConfigurationActivity : AppCompatActivity() {
private lateinit var binding: ActivityConfigurationBinding
private lateinit var mainViewModel: MainViewModel
var variMqttAddress = ""
(...)
And in function getValueFromDatastoreAndSaveToVar I want to get and save values from DataStore to variable variMqttAddress
private fun getValueFromDatastoreAndSaveToVar(){
mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
mainViewModel.readMqttAddressFlow.observe(this) { mqqtAdress ->
variMqttAddress = mqqtAdress
}
}
but it doesn't work. when debugging I have an empty value in var
Log.d(TAG, "variMqttAddress:: $variMqttAddress")
___________
2021-02-16 12:42:20.524 12792-12792 D/DEBUG: variMqttAddress::
Please help
When using flows with DataStore, value will be fetched asynchronously meaning you wont have the value right away, try printing log inside observe method and then create your MQttClient with the url
private fun getValueFromDatastoreAndSaveToVar(){
mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
mainViewModel.readMqttAddressFlow.observe(this) { mqqtAdress ->
variMqttAddress = mqqtAdress
//varImqttAddress will be available at this point
Log.d(TAG, "variMqttAddress:: $variMqttAddress")
val mqttClient = MqttAsyncClient(varImqttAddress, clientId, MemoryPersistence())
}
}
other way is to use, collect/first on flows for blocking get but it requires to be inside a coroutinescope
Quick Tip: I think you can initialise mainViewModel globally once and access it in all methods instead of reassigning them in each
method. Seems redundant
UPDATE
If you have multiple values coming from different LiveData instances, then you can create a method something like validateParatmers(), which will have checks for all the parameters before creating instance like
private fun getValueFromDatastoreAndSaveToVar(){
mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
mainViewModel.readMqttAddressFlow.observe(this) { mqqtAdress ->
variMqttAddress = mqqtAdress
Log.d(TAG, "variMqttAddress:: $variMqttAddress")
validateParametersAndInitMqtt() //add checks after observing ever livedata
}
mainViewModel.readMqttPortFlow.observe(this) {mqttPort ->
variMqttPass = mqttPort.toString()
validateParametersAndInitMqtt()
}
mainViewModel.readMqttUserFlow.observe(this) { mqttUser ->
variMqttUser = mqttUser
validateParametersAndInitMqtt()
}
mainViewModel.readMqttPassFlow.observe(this) { mqttPass ->
variMqttPass = mqttPass
validateParametersAndInitMqtt()
}
}
private fun validateParametersAndInitMqtt(){
if(variMqttAddress.isEmpty() || variMqttPass.isEmpty()
|| variMqttUser.isEmpty() || variMqttPass.isEmpty()){
//if any one is also empty, then don't proceed further
return
}
//create socket instance here, all your values will be available
}
Thank you for your help
I did not add earlier that in addition to the address of the MQQT server in the configuration, it also stores the port, user and password.
I think I am doing something wrong, in every YouTube tutorial it is shown how to "download" one configuration parameter. My function that retrieves data now looks like this:
private fun getValueFromDatastoreAndSaveToVar(){
mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
mainViewModel.readMqttAddressFlow.observe(this) { mqqtAdress ->
variMqttAddress = mqqtAdress
Log.d(TAG, "variMqttAddress:: $variMqttAddress")
}
mainViewModel.readMqttPortFlow.observe(this) {mqttPort ->
variMqttPass = mqttPort.toString()
}
mainViewModel.readMqttUserFlow.observe(this) { mqttUser ->
variMqttUser = mqttUser
}
mainViewModel.readMqttPassFlow.observe(this) { mqttPass ->
variMqttPass = mqttPass
}
}
in the repository class, I create a flow for each value
//Create MQTT Address flow
val readMqttAddressFlow: Flow<String> = dataStore.data
.catch { exception ->
if(exception is IOException){
Log.d("DataStore", exception.message.toString())
emit(emptyPreferences())
}else {
throw exception
}
}
.map { preference ->
val mqqtAdress = preference[PreferenceKeys.CON_MQTT_ADDRESS] ?: "none"
mqqtAdress
}
//Create MQTT Port flow
val readMqttPortFlow: Flow<Int> = dataStore.data
.catch { exception ->
if(exception is IOException){
Log.d("DataStore", exception.message.toString())
emit(emptyPreferences())
}else {
throw exception
}
}
.map { preference ->
val mqqtPort = preference[PreferenceKeys.CON_MQTT_PORT] ?: 0
mqqtPort
}
(.....)
now the question is am I doing it right?
now how to create MQttClient only when I have all parameters in variables?
can do some sleep of the function that is supposed to create the MQQTClient until the asychnronic function assigns values to variables?

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