I'm trying to create a BLE service that will scan for devices and using rxKotlin create an observable that will allow another class to observe when a device is found. I'm confused on how to create the observable that will allow another class to subscribe and tutorials are all over the place. Can someone give me a pointer on how to do so or a good tutorial.
Bluetoothservice class callback where devices are discovered
var foundDeviceObservable: Observable<BluetoothDevice> = Observable.create { }
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
with(result.device) {
var foundName = if (name == null) "N/A" else name
foundDevice = BluetoothDevice(
foundName,
address,
address,
result.device.type.toString()
)
foundDeviceObservable.subscribe {
//Update Observable value?
}
}
}
}
class DeviceListViewModel(application: Application) : AndroidViewModel(application) {
private val bluetoothService = BLEService()
//Where I am trying to do logic with device
fun getDeviceObservable(){
bluetoothService.getDeviceObservable().subscribe{ it ->
}
}
Solution
Was able to find the solution after reading user4097210's reply. Just had to change the found device to
var foundDeviceObservable: BehaviorSubject<BluetoothDevice> = BehaviorSubject.create()
and then call the next method in the callback
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
with(result.device) {
var foundName = if (name == null) "N/A" else name
foundDevice = BluetoothDevice(
foundName,
address,
address,
result.device.type.toString()
)
foundDeviceObservable.onNext(foundDevice)
}
}
}
use BehaviorSubject
// create a BehaviorSubject
var foundDeviceObservable: BehaviorSubject<BluetoothDevice> = BehaviorSubject()
// call onNext() to send new found device
foundDeviceObservable.onNext(foundDevice)
// do your logic use foundDeviceObservable
foundDeviceObservable.subscribe(...)
Related
Did anyone implement google autocomplete suggestion text field or fragment in a jetpack compose project? If so kindly guide or share code snippets as I'm having difficulty in implementing it.
Update
Here is the intent that I'm triggering to open full-screen dialog, but when I start typing within it gets closed, and also I'm unable to figure out what the issue is and need a clue about handling on activity result for reading the result of the predictions within this compose function.
Places.initialize(context, "sa")
val fields = listOf(Place.Field.ID, Place.Field.NAME)
val intent = Autocomplete.IntentBuilder(
AutocompleteActivityMode.FULLSCREEN,fields).build(context)
startActivityForResult(context as MainActivity,intent, AUTOCOMPLETE_REQUEST_CODE, Bundle.EMPTY)
I am using the MVVM architecture and this is how I implemented it:
GooglePlacesApi
I've created an api for reaching google api named GooglePlacesApi
interface GooglePlacesApi {
#GET("maps/api/place/autocomplete/json")
suspend fun getPredictions(
#Query("key") key: String = <GOOGLE_API_KEY>,
#Query("types") types: String = "address",
#Query("input") input: String
): GooglePredictionsResponse
companion object{
const val BASE_URL = "https://maps.googleapis.com/"
}
}
The #Query("types") field is for specifiying what are you looking for in the query, you can look for establishments etc.
Types can be found here
Models
So I created 3 models for this implementation:
GooglePredictionsResponse
The way the response looks if you are doing a GET request with postman is:
Google Prediction Response
You can see that we have an object with "predictions" key so this is our first model.
data class GooglePredictionsResponse(
val predictions: ArrayList<GooglePrediction>
)
GooglePredictionTerm
data class GooglePredictionTerm(
val offset: Int,
val value: String
)
GooglePrediction
data class GooglePrediction(
val description: String,
val terms: List<GooglePredictionTerm>
)
I only needed that information, if you need anything else, feel free to modify the models or create your own.
GooglePlacesRepository
And finally we create the repository to get the information (I'm using hilt to inject my dependencies, you can ignore those annotations if not using it)
#ActivityScoped
class GooglePlacesRepository #Inject constructor(
private val api: GooglePlacesApi,
){
suspend fun getPredictions(input: String): Resource<GooglePredictionsResponse>{
val response = try {
api.getPredictions(input = input)
} catch (e: Exception) {
Log.d("Rently", "Exception: ${e}")
return Resource.Error("Failed prediction")
}
return Resource.Success(response)
}
}
Here I've used an extra class I've created to handle the response, called Resource
sealed class Resource<T>(val data: T? = null, val message: String? = null){
class Success<T>(data: T): Resource<T>(data)
class Error<T>(message: String, data:T? = null): Resource<T>(data = data, message = message)
class Loading<T>(data: T? = null): Resource<T>(data = data)
}
View Model
Again I'm using hilt so ignore annotations if not using it.
#HiltViewModel
class AddApartmentViewModel #Inject constructor(private val googleRepository: GooglePlacesRepository): ViewModel(){
val isLoading = mutableStateOf(false)
val predictions = mutableStateOf(ArrayList<GooglePrediction>())
fun getPredictions(address: String) {
viewModelScope.launch {
isLoading.value = true
val response = googleRepository.getPredictions(input = address)
when(response){
is Resource.Success -> {
predictions.value = response.data?.predictions!!
}
}
isLoading.value = false
}
}
fun onSearchAddressChange(address: String){
getPredictions(address)
}
}
If you need any further help let me know
I didn't include UI implementation because I assume it is individual but this is the easier part ;)
#Composable
fun MyComponent() {
val context = LocalContext.current
val intentLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {
when (it.resultCode) {
Activity.RESULT_OK -> {
it.data?.let {
val place = Autocomplete.getPlaceFromIntent(it)
Log.i("MAP_ACTIVITY", "Place: ${place.name}, ${place.id}")
}
}
AutocompleteActivity.RESULT_ERROR -> {
it.data?.let {
val status = Autocomplete.getStatusFromIntent(it)
Log.i("MAP_ACTIVITY", "Place: ${place.name}, ${place.id}")
}
}
Activity.RESULT_CANCELED -> {
// The user canceled the operation.
}
}
}
val launchMapInputOverlay = {
Places.initialize(context, YOUR_API_KEY)
val fields = listOf(Place.Field.ID, Place.Field.NAME)
val intent = Autocomplete
.IntentBuilder(AutocompleteActivityMode.OVERLAY, fields)
.build(context)
intentLauncher.launch(intent)
}
Column {
Button(onClick = launchMapInputOverlay) {
Text("Select Location")
}
}
}
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?
I am rewriting a java class to kotlin replacing callback with a suspend function. This is my java code:
#IgnoreExtraProperties
public class DeviceType {
public String manufacturer;
public String marketName;
public String model;
public DeviceType(String manufacturer, String marketName, String model) {
this.manufacturer = manufacturer;
this.marketName = marketName;
this.model = model;
}
public DeviceType(){}
public DeviceType(Context context) {
DeviceName.with(context).request(new DeviceName.Callback() {
#Override
public void onFinished(DeviceName.DeviceInfo info, Exception error) {
if (error == null) {
manufacturer = info.manufacturer;
marketName = info.marketName;
model = info.model;
} else
Log.e("DeviceType: ", error.getMessage());
}
});
}
#Override
public String toString() {
if (model == null) {
return "No device type recognized!";
} else {
if (marketName.equals(model))
return manufacturer + " " +marketName;
else
return manufacturer + " " +marketName+ " (" +model+ ")";
}
}
DeviceName class belongs to library AndroidDeviceNames.
Below is my new code in Kotlin:
#IgnoreExtraProperties
data class DeviceType(
var manufacturer: String? = null,
var marketName: String? = null,
var model: String? = null
) {
constructor(context: Context) : this(
context.deviceType()?.manufacturer,
context.deviceType()?.marketName,
context.deviceType()?.model
)
override fun toString(): String {
val stringSuffix =
if (marketName == model)
""
else
" ($model)"
return model?.let { "$manufacturer $marketName$stringSuffix" }
?: "No device type recognized!"
}
}
/**
* return DeviceType "from" UI Context
*/
fun Context.deviceType(): DeviceType? = runBlocking {
withContext(Dispatchers.IO) {
/*
delay(1000L)
DeviceType("Nokia","Banana","R2D2")
^
This works!
*/
DeviceName
.with(this#deviceType)
.awaitWith(this#deviceType)
// ^ that doesn't!
}
}
suspend fun DeviceName.Request.awaitWith(context: Context): DeviceType? = suspendCoroutine { cont ->
DeviceName.with(context).request { info, error ->
if (error == null) {
cont.resume(DeviceType(
info.manufacturer,
info.marketName,
info.model
))
} else
cont.resumeWithException(Throwable(error.message))
.let {
Log.e(
"FirebaseUserData",
"DeviceName.Request.awaitWith(): $error.message"
)
}
}
}
Executing deviceType().toString()) in MainActivity makes infinite looping in runBlocking() function.
The fundamental question is of course "why my implementation of awaitWith() does not work?", but I am also interested, taking first steps in kotlin and coroutines, if I should provide additional solutions for exception handling, as I read the "coroutines may hide exceptions".
And one more question:
Is Dispatcher.IO here OK? DeviceName gets data from Google API json query.
Should I use that dispatcher type also for coroutines related to firebase DB?
First of all, responding to the question's title, the loop is happening because the constructor is calling Context.deviceType() that calls DeviceName.Request.awaitWith that calls the constructor again:
cont.resume(DeviceType(
info.manufacturer,
info.marketName,
info.model
))
The Context.deviceType() return a DeviceType by itself, but you desire to use it to configure each attribute in the initialization. Each DeviceType's attribute initialization instantiate a DeviceType which each attribute instantiate another DeviceType and so on....
Using Dispatcher.IO is OK and even desired when it comes to IO operations, like network, but you are not quite using it.
The runBlocking call blocks the current thread. The way you are using is like that:
## Assume we are on Thread (A)
fun Context.deviceType(): DeviceType? = runBlocking { ## Still in thread (A)
withContext(Dispatchers.IO) { ## Execute in an IO thread pool, but (A) is waiting
DeviceName
.with(this#deviceType)
.awaitWith(this#deviceType)
} ## Returns to thread (A)
} # Resumes Thread (A)
So, although this is kinda running in an IO dispatcher, the calling thread is blocked until the execution is finished, making it synchronous and indifferent.
Actually my goal was to see the deviceType() function output in non-coroutine environment. This function will be used anyway in other suspend functions or coroutine scope.
This is DeviceType class with its public functions without additional constructor:
#IgnoreExtraProperties
data class DeviceType(
var manufacturer: String? = null,
var marketName: String? = null,
var model: String? = null
) {
override fun toString(): String {
val stringSuffix =
if (marketName == model)
""
else
" ($model)"
return model?.let { "$manufacturer $marketName$stringSuffix" }
?: "No device type recognized!"
}
}
fun Context.deviceTypeByRunBlocking(): DeviceType? = runBlocking {
withContext(Dispatchers.IO) {
DeviceName
.with(this#deviceTypeNoSuspend)
.awaitWith(this#deviceTypeNoSuspend)
}
}
suspend fun Context.deviceType(): DeviceType? =
DeviceName
.with(this#deviceType)
.awaitWith(this#deviceType)
private suspend fun DeviceName.Request.awaitWith(context: Context): DeviceType? =
suspendCoroutine { cont ->
DeviceName.with(context).request { info, error ->
if (error == null) {
cont.resume(
DeviceType(
info.manufacturer,
info.marketName,
info.model
)
//.also{Log.d("TAG","Inside awaitWith(): $it")}
)
} else
cont.resumeWithException(Throwable(error.message))
.let {
Log.e(
"TAG",
"DeviceName.Request.awaitWith(): $error.message"
)
}
}
}
Main Activity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GlobalScope.launch { Log.d("MainActivity", "${this#MainActivity.deviceType()}") }
//^ this works
Log.d("MainActivity", "${this.deviceTypeByRunBlocking()}")
//^ this still does not, loops in joinBlocking(), isComplete = false
}
}
I know that using GlobalScope is not recommended, but for testing it is fine for me.
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")
}
What I trying to do is listen to socket data and convert into an observable string that my UI can Subscribe this event and do Change on UI
So far I created a class SocketConnection maintain in dagger connection happen properly and received data and able to do with interface correctly, but want to apply with rxkotlin.
Using Socket.io,kotlin
SocketConnection class
class SocketConnection : SocketStreamListener {
private var socket: Socket? = null
var responseSocket :ResponseHandler?= null
companion object {
var instance = SocketConnection()
}
override fun createSocket(socketQuery: SocketQuery): Socket? {
try {
val okHttpClient = UnsafeOkHttpClient.getUnsafeOkHttpClient()
IO.setDefaultOkHttpWebSocketFactory(okHttpClient)
IO.setDefaultOkHttpCallFactory(okHttpClient)
val opts = IO.Options()
opts.reconnection = false
opts.callFactory = okHttpClient
opts.webSocketFactory = okHttpClient
opts.query = "userID=" + socketQuery.userID + "&token=" + socketQuery.token
socket = IO.socket(CommonContents.BASE_API_LAYER, opts)
L.d("Socket object created")
} catch (e: URISyntaxException) {
L.e("Error creating socket", e)
}
return socket
}
override fun createSocketListener(socket: Socket) {
L.d("inside the socket Listner")
socket.connect()?.on(Socket.EVENT_CONNECT, {
L.d("connected")
listenSocketEvents()
//socketDataListener()
createMessageListener()
})?.on(Socket.EVENT_DISCONNECT,
{
L.d("disconnected")
return#on
})
}
/**
* function used to listen a socket chanel data
*/
private fun listenSocketEvents() {
/* socket?.on("1502", { args ->
// This Will Work
L.d("Socket market depth event successfully")
val socketData = args[0] as String
L.d(socketData)
// instance.data = Observable.just(socketData)
//data!!.doOnNext({ socketData })
*//*
data = args[0] as String
for (i in 0 until arr.size) {
arr[i].socketStreamingData(data)
}*//*
})*/
}
// This Will Not Work
fun socketDataListener(): Observable<String>{
return Observable.create({
subscibe ->
// L.d("Socket market depth event successfully")
socket?.on("1502", { args ->
L.d("Socket market depth event successfully")
val socketData = args[0] as String
subscibe.onNext(socketData)
})
})
}
}
Repository
fun getSocketData(): Observable<String> {
// L.e("" + SocketConnection.instance.socketDataListener())
return SocketConnection.instance.createMessageListener()
}
ViewModel
fun getSocketData(): Observable<String>{
return groupRepository.getSocketData()
}
OnFragement (UI)
private fun getSocketUpdate(){
subscribe(watchlistViewModel.getSocketData()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
L.d("SocketData : " + it.count())
}, {
L.e("Error")
}))
}
In this UI using disposable subscribe method into base class.
Please let me know what i doing wrong thanx in advance
Instead of creating an Observable every time a message is sent, I suggest using a Subject for that, since it has a similar "nature" as the Socket connection.
val subject = PublishSubject.create<String>()
...
fun listenSocketEvents() {
socket?.on("1502") { args ->
val socketData = args[0] as String
subject.onNext(socketData)
}
}
fun observable(): Observable<String>{
return subject
}
You can then listen to the changes on the subject via (repository layer etc not included, you'd have to do that yourself)
private fun getSocketUpdate() {
disposable = socketConnection.observable()
.subscribeOn(Schedulers.io())
.observeOn(...)
.subscribe({...}, {...})
}
As a side note, your singleton instance is not how you'd do that in kotlin.
Instead of having an instance field in a companion object, you should make the declare the class as object SocketConnection.
This will automatically give you all singleton features. (I do not know whether it is smart to use a singleton with socket.io, but I assume that you know what you're doing :-) )