I have a confusion about how Dispatchers work in Kotlin
Task
In my Application class I intend to access my database via Room, take out the user , take out his JWT accessToken and set it in another object that my retrofit Request inteceptor uses.
However I want all this code to be blocking , so that when the Application class has ran to its completion , the user has been extracted and set in the Inteceptor.
Problem
My application class runs to completion BEFORE the user has been picked from the database.
Session class is the one which accesses Room
This is how my session class looks
class Session(private val userRepository: UserRepository, private var requestHeaders: RequestHeaders) {
var authenticationState: AuthenticationState = AuthenticationState.UNAUTHENTICATED
var loggedUser: User? by Delegates.observable<User?>(null) { _, _, user ->
if (user != null) {
user.run {
loggedRoles = roleCsv.split(",")
loggedRoles?.run {
if (this[0] == "Employer") {
employer = toEmployer()
} else if (this[0] == "Employee") {
employee = toEmployee()
}
}
authenticationState = AuthenticationState.AUTHENTICATED
requestHeaders.accessToken = accessToken
}
} else {
loggedRoles = null
employer = null
employee = null
authenticationState = AuthenticationState.UNAUTHENTICATED
requestHeaders.accessToken = null
}
}
var loggedRoles: List<String>? = null
var employee: Employee? = null
var employer: Employer? = null
init {
runBlocking(Dispatchers.IO) {
loggedUser = userRepository.loggedInUser()
Log.d("Session","User has been set")
}
}
// var currentCity
// var currentLanguage
}
enum class AuthenticationState {
AUTHENTICATED, // Initial state, the user needs to secretQuestion
UNAUTHENTICATED, // The user has authenticated successfully
LOGGED_OUT, // The user has logged out.
}
This is my Application class
class MohreApplication : Application()
{
private val session:Session by inject()
private val mohreDatabase:MohreDatabase by inject() // this is integral. Never remove this from here. This seeds the data on database creation
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this#MohreApplication)
modules(listOf(
platformModule,
networkModule,
....
))
}
Log.d("Session","Launching application")
}
My Koin module which creates the session
val platformModule = module {
// single { Navigator(androidApplication()) }
single { Session(get(),get()) }
single { CoroutineScope(Dispatchers.IO + Job()) }
}
In my Logcat first "Launching Application" prints out and THEN "User has been set"
Shouldn't it be reverse? . This is causing my application to launch without the Session having the user and my MainActivity complains.
by inject() is using kotlin lazy initialization. Only when session.loggedUser is queried will the init block be fired.
In your case, when you call session.loggedUser in the MainActivity, the init block will fire and block the calling thread.
What you can do is.
import org.koin.android.ext.android.get
class MohreApplication : Application()
{
private lateinit var session: Session
private lateinit var mohreDatabase: MohreDatabase // this is integral. Never remove this from here. This seeds the data on database creation
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this#MohreApplication)
modules(listOf(
platformModule,
networkModule,
....
))
}
session = get()
mohreDatabase = get()
Log.d("Session","Launching application")
}
Related
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()
}
}
I am new to the coroutine concept. I'm not very confident in the code I've written. Basically I have a queue of user events that I send to my server. Adding an event is done through the main thread and sends it into a coroutine. If the event was successfully sent, I delete it from the queue, if the send failed, it is not deleted and will be retried for the next cycle. To solve the concurrency issues I used a mutex. Can you tell me if pretty good or horrible and a solution in this case?
My code:
data class GeoDataEvent(
val location : Location,
val category : Int
)
// This class is instantiated in my android service
class GeoDataManager(private var mainRepository: MainRepository) {
private var eventQueue : Queue<GeoDataEvent> = LinkedList()
private val eventQueueMutex : Mutex = Mutex()
fun enqueueEvent(location: Location, category : Int){
CoroutineScope(Dispatchers.IO).launch {
eventQueueMutex.withLock {
eventQueue.add(
GeoDataEvent(
location = location,
category = category
)
)
}
}
}
// Called at each new location by Android location service
private fun processNewLocation(location: Location){
/* Some code */
handleEventQueue()
}
private fun handleEventQueue(){
CoroutineScope(Dispatchers.IO).launch {
eventQueueMutex.withLock {
if (eventQueue.isNotEmpty()) {
mainRepository.getAuthToken()?.let { token ->
eventQueue.peek()?.let { event ->
if (sendEvent(token, event)){
eventQueue.remove()
}
}
}
}
}
}
}
private suspend fun sendEvent(token : String, event : GeoDataEvent) : Boolean {
mainRepository.sendGeoDataEvent(token, event).let { res ->
return res.isSuccessful
}
}
}
Thank you for your help
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?
In my Fragment's onViewCreated, I have a redirect based on auth state, and also bind the Frag's Observers, like so:
class TodoMvvmFragment : Fragment() {
private val loginViewModel by viewModel<LoginViewModel>()
private val todoViewModel by viewModel<TodoViewModel>()
val TAG = "TODO_FRAG"
var rvAdapter:TodoListAdapter? = null
var observersBound = false
///...omitted
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
rvAdapter = TodoListAdapter(todoViewModel, this )
todo_rv.let {
it.adapter = rvAdapter
it.layoutManager = LinearLayoutManager(requireActivity())
}
loginViewModel.authenticationState.observe(this, Observer {
when(it) {
LoginViewModel.AuthenticationState.UNAUTHENTICATED -> {
findNavController().navigate(R.id.action_global_loginFragment)
}
LoginViewModel.AuthenticationState.AUTHENTICATED -> {
// Viewmodel calls aspects of the repository that rely on AUTHSTATE
// to be AUTHENTICATED, so don't bind observers until we are.
if (!observersBound) {
observersBound = true
todoViewModel.init()
bindObservers()
addListeners()
}
}
else -> {
Log.d(TAG, it.toString())
}
}
})
}
Here is the problematic Observer:
fun bindObservers() {
todoViewModel.getFilteredTodos().observe(this, Observer{
if (it.data == null) {
handleException("Error with todos by timestamp query", it.exception)
} else {
val todos: List<Todo> = it.data.map { todoOrException ->
if( todoOrException.data == null) {
handleException("error with individual Todo", todoOrException.exception)
null
} else {
todoOrException.data
}
}.filterNotNull()
rvAdapter?.submitList(todos)
}
})
This percolates down to the repo, which creates a firebase query. Notice the .document(auth.uid.toString())... if this query is created before the user is authorized, no data is returned.
fun allTodosQuery(): Query {
return firestore.collection(PATH_ROOT)
.document(PATH_APPDATA)
.collection(PATH_USERS)
.document(auth.uid.toString())
.collection(PATH_TODOS)
.orderBy(FIELD_TIMESTAMP, Query.Direction.DESCENDING)
Back in the authentication state listener, I am binding the observers ONLY when auth state changes to authenticated. I'm also guarding that with a boolean so that it only happens once. When I don't do that, and just bind the observers in onViewCreated after the auth state listener, I don't get any data on the initial load of the app.
My question is how to keep livedata that depend on authstate being authenticated from omitting nothing when they are bound to before the user is authenticated. Essentially, how do I keep from fetching a bad query with null auth.uid from the repo before the user is authenticated?
Thanks for allowing me to rubber duck. Here's a solution:
var _queryResult = Transformations.switchMap(_authState){
if (_authState.value == LoginViewModel.AuthenticationState.AUTHENTICATED) {
repo.getAllTodosByTimestamp()
} else {
MutableLiveData<ListOrException<TodoOrException>>()
}
}
That way, we don't construct or fire off the query (down in the repo) until authenticated.
By using LiveData's latest version "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03", I have developed a code for a feature called "Search Products" in the ViewModel using LiveData's new building block (LiveData + Coroutine) that performs a synchronous network call using Retrofit and update different flags (isLoading, isError) in ViewModel accordingly. I am using Transforamtions.switchMap on "query" LiveData so whenever there is a change in "query" from the UI, the "Search Products" code starts its executing using Transformations.switchMap. Every thing is working fine, except that i want to cancel the previous Retrofit Call whenever a change happens in "query" LiveData. Currently i can't see any way to do this. Any help would be appreciated.
class ProductSearchViewModel : ViewModel() {
val completableJob = Job()
private val coroutineScope = CoroutineScope(Dispatchers.IO + completableJob)
// Query Observable Field
val query: MutableLiveData<String> = MutableLiveData()
// IsLoading Observable Field
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
val products: LiveData<List<ProductModel>> = query.switchMap { q ->
liveData(context = coroutineScope.coroutineContext) {
emit(emptyList())
_isLoading.postValue(true)
val service = MyApplication.getRetrofitService()
val response = service?.searchProducts(q)
if (response != null && response.isSuccessful && response.body() != null) {
_isLoading.postValue(false)
val body = response.body()
if (body != null && body.results != null) {
emit(body.results)
}
} else {
_isLoading.postValue(false)
}
}
}
}
You can solve this problem in two ways:
Method # 1 ( Easy Method )
Just like Mel has explained in his answer, you can keep a referece to the job instance outside of switchMap and cancel instantance of that job right before returning your new liveData in switchMap.
class ProductSearchViewModel : ViewModel() {
// Job instance
private var job = Job()
val products = Transformations.switchMap(_query) {
job.cancel() // Cancel this job instance before returning liveData for new query
job = Job() // Create new one and assign to that same variable
// Pass that instance to CoroutineScope so that it can be cancelled for next query
liveData(CoroutineScope(job + Dispatchers.IO).coroutineContext) {
// Your code here
}
}
override fun onCleared() {
super.onCleared()
job.cancel()
}
}
Method # 2 ( Not so clean but self contained and reusable)
Since liveData {} builder block runs inside a coroutine scope, you can use a combination of CompletableDeffered and coroutine launch builder to suspend that liveData block and observe query liveData manually to launch jobs for network requests.
class ProductSearchViewModel : ViewModel() {
private val _query = MutableLiveData<String>()
val products: LiveData<List<String>> = liveData {
var job: Job? = null // Job instance to keep reference of last job
// LiveData observer for query
val queryObserver = Observer<String> {
job?.cancel() // Cancel job before launching new coroutine
job = GlobalScope.launch {
// Your code here
}
}
// Observe query liveData here manually
_query.observeForever(queryObserver)
try {
// Create CompletableDeffered instance and call await.
// Calling await will suspend this current block
// from executing anything further from here
CompletableDeferred<Unit>().await()
} finally {
// Since we have called await on CompletableDeffered above,
// this will cause an Exception on this liveData when onDestory
// event is called on a lifeCycle . By wrapping it in
// try/finally we can use this to know when that will happen and
// cleanup to avoid any leaks.
job?.cancel()
_query.removeObserver(queryObserver)
}
}
}
You can download and test run both of these methods in this demo project
Edit: Updated Method # 1 to add job cancellation on onCleared method as pointed out by yasir in comments.
Retrofit request should be cancelled when parent scope is cancelled.
class ProductSearchViewModel : ViewModel() {
val completableJob = Job()
private val coroutineScope = CoroutineScope(Dispatchers.IO + completableJob)
/**
* Adding job that will be used to cancel liveData builder.
* Be wary - after cancelling, it'll return a new one like:
*
* ongoingRequestJob.cancel() // Cancelled
* ongoingRequestJob.isActive // Will return true because getter created a new one
*/
var ongoingRequestJob = Job(coroutineScope.coroutineContext[Job])
get() = if (field.isActive) field else Job(coroutineScope.coroutineContext[Job])
// Query Observable Field
val query: MutableLiveData<String> = MutableLiveData()
// IsLoading Observable Field
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
val products: LiveData<List<ProductModel>> = query.switchMap { q ->
liveData(context = ongoingRequestJob) {
emit(emptyList())
_isLoading.postValue(true)
val service = MyApplication.getRetrofitService()
val response = service?.searchProducts(q)
if (response != null && response.isSuccessful && response.body() != null) {
_isLoading.postValue(false)
val body = response.body()
if (body != null && body.results != null) {
emit(body.results)
}
} else {
_isLoading.postValue(false)
}
}
}
}
Then you need to cancel ongoingRequestJob when you need to. Next time liveData(context = ongoingRequestJob) is triggered, since it'll return a new job, it should run without problems. All you need to left is cancel it where you need to, i.e. in query.switchMap function scope.