Migrate from LiveData to Kotlin Flow (#518)

* Automated update of relay data - Sun Sep 21 06:21:05 UTC 2025

* Automated update of relay data - Sun Sep 28 06:20:40 UTC 2025

* refactor: new close button like ios(but not liquid glass)

* Automated update of relay data - Sun Oct  5 06:20:09 UTC 2025

* Automated update of relay data - Sun Oct 12 06:20:12 UTC 2025

* Automated update of relay data - Sun Oct 19 06:21:51 UTC 2025

* Automated update of relay data - Sun Oct 26 06:21:31 UTC 2025

* Automated update of relay data - Sun Nov  2 06:22:16 UTC 2025

* Automated update of relay data - Sun Nov  9 06:21:43 UTC 2025

* Automated update of relay data - Sun Nov 16 06:22:37 UTC 2025

* Automated update of relay data - Sun Nov 23 06:22:51 UTC 2025

* Automated update of relay data - Sun Nov 30 06:24:08 UTC 2025

* Chore: Remove unused `lifecycle-livedata-ktx` dependency

This commit removes the `androidx.lifecycle:lifecycle-livedata-ktx` library from the project's dependencies.

The `[libraries]` and `[bundles]` sections in `gradle/libs.versions.toml` have been updated to reflect this removal, as the dependency is no longer in use.

* Refactor: Remove unused `runtime-livedata` dependency

* Refactor: Migrate `LocationChannelManager` and `GeohashBookmarksStore` to StateFlow

This commit refactors `LocationChannelManager` and `GeohashBookmarksStore` to use `StateFlow` instead of `LiveData` for managing and exposing their state. This change aligns with modern Android development practices and improves testability.

**Key Changes:**

- **`LocationChannelManager`**:
    - All `MutableLiveData` properties (`permissionState`, `availableChannels`, `selectedChannel`, etc.) have been replaced with `MutableStateFlow`.
    - Consumers now access these properties as `StateFlow`.
    - State updates have been changed from `postValue()` to direct `.value` assignments, simplifying thread management within the manager which already uses a dedicated coroutine scope.

- **`GeohashBookmarksStore`**:
    - `bookmarks` and `bookmarkNames` are now exposed as `StateFlow` instead of `LiveData`.
    - State updates similarly use `.value` assignment.

- **Nullability**:
    - The non-nullable nature of `StateFlow`'s value reduces the need for null-checks in both the manager classes and their consumers, leading to safer code.

* Refactor: Migrate from LiveData to StateFlow for Nostr components

This commit replaces `LiveData` with `StateFlow` across core Nostr-related classes to align with modern Android architecture and improve state management. This change affects `NostrClient`, `NostrRelayManager`, `LocationNotesManager`, and `GeohashRepository`.

**Key Changes:**

-   **`NostrClient`**:
    -   `isInitialized` and `currentNpub` are now `StateFlow` instead of `LiveData`.
    -   `relayConnectionStatus` and `relayInfo` now return `StateFlow` from `NostrRelayManager`.

-   **`NostrRelayManager`**:
    -   Public properties `relays` and `isConnected` are migrated from `MutableLiveData` to `MutableStateFlow`.
    -   Updates are now pushed using `.value` instead of `.postValue()`.

-   **`LocationNotesManager`**:
    -   All public `LiveData` properties (`notes`, `geohash`, `initialLoadComplete`, `state`, `errorMessage`) are converted to `StateFlow`.
    -   The class documentation is updated to reflect the use of `StateFlow`.

-   **`GeohashRepository`**:
    -   Methods `updateGeohashPeople` and `updateReactiveParticipantCounts` now call `set...` methods on the `state` object instead of `post...`, reflecting the removal of `LiveData` from the underlying state management.

* Refactor: Migrate ChatState from LiveData to StateFlow

This commit refactors the `ChatState`, `ChatViewModel`, and `GeohashViewModel` to use `StateFlow` instead of `LiveData` for managing and exposing UI state. This migration improves state management by leveraging modern coroutine-based flows.

**Key Changes:**

- **`ChatState.kt`**:
    - Replaced all `MutableLiveData` instances with `MutableStateFlow`.
    - Exposed state properties as `StateFlow` instead of `LiveData`.
    - Removed `MediatorLiveData` for computed properties (`hasUnreadChannels`, `hasUnreadPrivateMessages`) and replaced them with `Flow.combine` to create derivative `StateFlows`.
    - Simplified non-nullable `getters` to directly return the `.value` of the `StateFlows`.
    - Removed `postValue` helpers that are no longer necessary.

- **`ChatViewModel.kt`**:
    - Updated all state properties to be `StateFlow`, reflecting the changes in `ChatState`.

- **`GeohashViewModel.kt`**:
    - Changed state properties (`geohashPeople`, `geohashParticipantCounts`, etc.) from `LiveData` to `StateFlow`.
    - Replaced `observeForever` on `LiveData` from `LocationChannelManager` with `viewModelScope.launch` blocks that `.collect()` from the underlying flows.

* Refactor: Migrate UI from LiveData to StateFlow

This commit replaces `LiveData.observeAsState()` with `StateFlow.collectAsState()` across various UI components. This change aligns the codebase with modern Android development practices, using Kotlin Flows for reactive UI state management.

No functional changes are intended. The primary goal is to remove the dependency on `androidx.lifecycle.livedata` from the composable functions.

**Affected Components:**
- `ChatScreen`
- `SidebarComponents`
- `ChatHeader`
- `LocationChannelsSheet`
- `LocationNotesSheet`
- `LocationNotesButton`
- `GeohashPeopleList`
- `LocationNotesSheetPresenter`

* Refactor: Use `collectAsStateWithLifecycle` for UI state collection

* Refactor: move CloseButton to core/ui/component

* Refactor: remove AI generated comments

* Refactor: fix combine to map and use WhileSubscribed

* Refactor: Pass CoroutineScope to ChatState

This commit refactors the `ChatState` class to accept a `CoroutineScope` in its constructor instead of creating its own.

**Key Changes:**

- **`ChatState.kt`**: The constructor now requires a `CoroutineScope`. This scope is used for the `stateIn` operators that convert `Flows` into `StateFlows` (`hasUnreadChannels`, `hasUnreadPrivateMessages`), ensuring they operate within the lifecycle of the provided scope.
- **`ChatViewModel.kt`**: The `viewModelScope` is now passed to the `ChatState` constructor during its instantiation. This ties the lifecycle of the state's coroutines directly to the `ViewModel`'s lifecycle.

* Test: Use `TestScope` for coroutines in `CommandProcessorTest`

This commit refactors `CommandProcessorTest` to use a `TestScope` and `UnconfinedTestDispatcher` for managing coroutines.

This ensures that coroutine-based operations within the test are executed in a controlled and predictable manner, improving test reliability. The `coroutineScope` for `CommandProcessor` and the `scope` for `ChatState` are now both configured to use this test-specific scope.

---------

Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
yet300
2025-12-13 15:58:48 +07:00
committed by GitHub
co-authored by GitHub Action
parent b2febcee88
commit e96330e50b
23 changed files with 426 additions and 397 deletions
@@ -5,13 +5,14 @@ import android.location.Geocoder
import android.location.Location
import android.location.LocationManager
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.util.Locale
/**
@@ -46,11 +47,11 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
private val membership = mutableSetOf<String>()
private val _bookmarks = MutableLiveData<List<String>>(emptyList())
val bookmarks: LiveData<List<String>> = _bookmarks
private val _bookmarks = MutableStateFlow<List<String>>(emptyList())
val bookmarks: StateFlow<List<String>> = _bookmarks.asStateFlow()
private val _bookmarkNames = MutableLiveData<Map<String, String>>(emptyMap())
val bookmarkNames: LiveData<Map<String, String>> = _bookmarkNames
private val _bookmarkNames = MutableStateFlow<Map<String, String>>(emptyMap())
val bookmarkNames: StateFlow<Map<String, String>> = _bookmarkNames.asStateFlow()
// For throttling / preventing duplicate geocode lookups
private val resolving = mutableSetOf<String>()
@@ -68,8 +69,8 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
val gh = normalize(geohash)
if (gh.isEmpty() || membership.contains(gh)) return
membership.add(gh)
val updated = listOf(gh) + (_bookmarks.value ?: emptyList())
_bookmarks.postValue(updated)
val updated = listOf(gh) + (_bookmarks.value)
_bookmarks.value = updated
persist(updated)
// Resolve friendly name asynchronously
resolveNameIfNeeded(gh)
@@ -79,12 +80,12 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
val gh = normalize(geohash)
if (!membership.contains(gh)) return
membership.remove(gh)
val updated = (_bookmarks.value ?: emptyList()).filterNot { it == gh }
_bookmarks.postValue(updated)
val updated = (_bookmarks.value).filterNot { it == gh }
_bookmarks.value = updated
// Remove stored name to avoid stale cache growth
val names = _bookmarkNames.value?.toMutableMap() ?: mutableMapOf()
val names = _bookmarkNames.value.toMutableMap()
if (names.remove(gh) != null) {
_bookmarkNames.postValue(names)
_bookmarkNames.value = names
persistNames(names)
}
persist(updated)
@@ -108,7 +109,7 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
}
}
membership.clear(); membership.addAll(seen)
_bookmarks.postValue(ordered)
_bookmarks.value = ordered
}
} catch (e: Exception) {
Log.e(TAG, "Failed to load bookmarks: ${e.message}")
@@ -118,7 +119,7 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
if (!namesJson.isNullOrEmpty()) {
val mapType = object : TypeToken<Map<String, String>>() {}.type
val dict = gson.fromJson<Map<String, String>>(namesJson, mapType)
_bookmarkNames.postValue(dict)
_bookmarkNames.value = dict
}
} catch (e: Exception) {
Log.e(TAG, "Failed to load bookmark names: ${e.message}")
@@ -127,14 +128,14 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
private fun persist() {
try {
val json = gson.toJson(_bookmarks.value ?: emptyList<String>())
val json = gson.toJson(_bookmarks.value)
prefs.edit().putString(STORE_KEY, json).apply()
} catch (_: Exception) {}
}
private fun persistNames() {
try {
val json = gson.toJson(_bookmarkNames.value ?: emptyMap<String, String>())
val json = gson.toJson(_bookmarkNames.value)
prefs.edit().putString(NAMES_STORE_KEY, json).apply()
} catch (_: Exception) {}
}
@@ -144,8 +145,8 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
fun clearAll() {
try {
membership.clear()
_bookmarks.postValue(emptyList())
_bookmarkNames.postValue(emptyMap())
_bookmarks.value = emptyList()
_bookmarkNames.value = emptyMap()
prefs.edit()
.remove(STORE_KEY)
.remove(NAMES_STORE_KEY)
@@ -209,9 +210,9 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
}
if (!name.isNullOrEmpty()) {
val current = _bookmarkNames.value?.toMutableMap() ?: mutableMapOf()
val current = _bookmarkNames.value.toMutableMap()
current[gh] = name
_bookmarkNames.postValue(current)
_bookmarkNames.value = current
persistNames(current)
}
} catch (e: Exception) {
@@ -10,12 +10,12 @@ import android.location.LocationManager
import android.os.Bundle
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.*
import java.util.*
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/**
* Manages location permissions, one-shot location retrieval, and computing geohash channels.
@@ -53,28 +53,26 @@ class LocationChannelManager private constructor(private val context: Context) {
private var dataManager: com.bitchat.android.ui.DataManager? = null
// Published state for UI bindings (matching iOS @Published properties)
private val _permissionState = MutableLiveData(PermissionState.NOT_DETERMINED)
val permissionState: LiveData<PermissionState> = _permissionState
private val _permissionState = MutableStateFlow(PermissionState.NOT_DETERMINED)
val permissionState: StateFlow<PermissionState> = _permissionState
private val _availableChannels = MutableLiveData<List<GeohashChannel>>(emptyList())
val availableChannels: LiveData<List<GeohashChannel>> = _availableChannels
private val _availableChannels = MutableStateFlow<List<GeohashChannel>>(emptyList())
val availableChannels: StateFlow<List<GeohashChannel>> = _availableChannels
private val _selectedChannel = MutableLiveData<ChannelID>(ChannelID.Mesh)
val selectedChannel: LiveData<ChannelID> = _selectedChannel
private val _selectedChannel = MutableStateFlow<ChannelID>(ChannelID.Mesh)
val selectedChannel: StateFlow<ChannelID> = _selectedChannel
private val _teleported = MutableLiveData(false)
val teleported: LiveData<Boolean> = _teleported
private val _teleported = MutableStateFlow(false)
val teleported: StateFlow<Boolean> = _teleported
private val _locationNames = MutableLiveData<Map<GeohashChannelLevel, String>>(emptyMap())
val locationNames: LiveData<Map<GeohashChannelLevel, String>> = _locationNames
private val _locationNames = MutableStateFlow<Map<GeohashChannelLevel, String>>(emptyMap())
val locationNames: StateFlow<Map<GeohashChannelLevel, String>> = _locationNames
// Add a new LiveData property to indicate when location is being fetched
private val _isLoadingLocation = MutableLiveData(false)
val isLoadingLocation: LiveData<Boolean> = _isLoadingLocation
private val _isLoadingLocation = MutableStateFlow(false)
val isLoadingLocation: StateFlow<Boolean> = _isLoadingLocation
// Add a new LiveData property to track if location services are enabled by user
private val _locationServicesEnabled = MutableLiveData(false)
val locationServicesEnabled: LiveData<Boolean> = _locationServicesEnabled
private val _locationServicesEnabled = MutableStateFlow(false)
val locationServicesEnabled: StateFlow<Boolean> = _locationServicesEnabled
init {
updatePermissionState()
@@ -102,15 +100,15 @@ class LocationChannelManager private constructor(private val context: Context) {
when (getCurrentPermissionStatus()) {
PermissionState.NOT_DETERMINED -> {
Log.d(TAG, "Permission not determined - user needs to grant in app settings")
_permissionState.postValue(PermissionState.NOT_DETERMINED)
_permissionState.value = PermissionState.NOT_DETERMINED
}
PermissionState.DENIED, PermissionState.RESTRICTED -> {
Log.d(TAG, "Permission denied or restricted")
_permissionState.postValue(PermissionState.DENIED)
_permissionState.value = PermissionState.DENIED
}
PermissionState.AUTHORIZED -> {
Log.d(TAG, "Permission authorized - requesting location")
_permissionState.postValue(PermissionState.AUTHORIZED)
_permissionState.value = PermissionState.AUTHORIZED
requestOneShotLocation()
}
}
@@ -180,7 +178,7 @@ class LocationChannelManager private constructor(private val context: Context) {
lastLocation?.let { location ->
when (channel) {
is ChannelID.Mesh -> {
_teleported.postValue(false)
_teleported.value = false
}
is ChannelID.Location -> {
val currentGeohash = Geohash.encode(
@@ -189,7 +187,7 @@ class LocationChannelManager private constructor(private val context: Context) {
precision = channel.channel.level.precision
)
val isTeleportedNow = currentGeohash != channel.channel.geohash
_teleported.postValue(isTeleportedNow)
_teleported.value = isTeleportedNow
Log.d(TAG, "Teleported (immediate recompute): $isTeleportedNow (current: $currentGeohash, selected: ${channel.channel.geohash})")
}
}
@@ -201,7 +199,7 @@ class LocationChannelManager private constructor(private val context: Context) {
*/
fun setTeleported(teleported: Boolean) {
Log.d(TAG, "Setting teleported status: $teleported")
_teleported.postValue(teleported)
_teleported.value = teleported
}
/**
@@ -209,7 +207,7 @@ class LocationChannelManager private constructor(private val context: Context) {
*/
fun enableLocationServices() {
Log.d(TAG, "enableLocationServices() called by user")
_locationServicesEnabled.postValue(true)
_locationServicesEnabled.value = true
saveLocationServicesState(true)
// If we have permission, start location operations
@@ -223,15 +221,15 @@ class LocationChannelManager private constructor(private val context: Context) {
*/
fun disableLocationServices() {
Log.d(TAG, "disableLocationServices() called by user")
_locationServicesEnabled.postValue(false)
_locationServicesEnabled.value = false
saveLocationServicesState(false)
// Stop any ongoing location operations
endLiveRefresh()
// Clear available channels when location is disabled
_availableChannels.postValue(emptyList())
_locationNames.postValue(emptyMap())
_availableChannels.value = emptyList()
_locationNames.value = emptyMap()
// If user had a location channel selected, switch back to mesh
if (_selectedChannel.value is ChannelID.Location) {
@@ -243,7 +241,7 @@ class LocationChannelManager private constructor(private val context: Context) {
* Check if location services are enabled by the user
*/
fun isLocationServicesEnabled(): Boolean {
return _locationServicesEnabled.value ?: false
return _locationServicesEnabled.value
}
// MARK: - Location Operations
@@ -280,13 +278,13 @@ class LocationChannelManager private constructor(private val context: Context) {
if (lastKnownLocation != null) {
Log.d(TAG, "Using last known location: ${lastKnownLocation.latitude}, ${lastKnownLocation.longitude}")
lastLocation = lastKnownLocation
_isLoadingLocation.postValue(false) // Make sure loading state is off
_isLoadingLocation.value = false // Make sure loading state is off
computeChannels(lastKnownLocation)
reverseGeocodeIfNeeded(lastKnownLocation)
} else {
Log.d(TAG, "No last known location available")
// Set loading state to true so UI can show a spinner
_isLoadingLocation.postValue(true)
_isLoadingLocation.value = true
// Request a fresh location only when we don't have a last known location
Log.d(TAG, "Requesting fresh location...")
@@ -294,7 +292,7 @@ class LocationChannelManager private constructor(private val context: Context) {
}
} catch (e: SecurityException) {
Log.e(TAG, "Security exception requesting location: ${e.message}")
_isLoadingLocation.postValue(false) // Turn off loading state on error
_isLoadingLocation.value = false // Turn off loading state on error
updatePermissionState()
}
}
@@ -308,7 +306,7 @@ class LocationChannelManager private constructor(private val context: Context) {
reverseGeocodeIfNeeded(location)
// Update loading state to indicate we have a location now
_isLoadingLocation.postValue(false)
_isLoadingLocation.value = false
// Remove this listener after getting the update
try {
@@ -322,13 +320,13 @@ class LocationChannelManager private constructor(private val context: Context) {
// Request a fresh location update using getCurrentLocation instead of continuous updates
private fun requestFreshLocation() {
if (!hasLocationPermission()) {
_isLoadingLocation.postValue(false) // Turn off loading state if no permission
_isLoadingLocation.value = false // Turn off loading state if no permission
return
}
try {
// Set loading state to true to indicate we're actively trying to get a location
_isLoadingLocation.postValue(true)
_isLoadingLocation.value = true
// Try common providers in order of preference
val providers = listOf(
@@ -358,7 +356,7 @@ class LocationChannelManager private constructor(private val context: Context) {
Log.w(TAG, "Received null location from getCurrentLocation")
}
// Update loading state to indicate we have a location now
_isLoadingLocation.postValue(false)
_isLoadingLocation.value = false
}
)
} else {
@@ -378,14 +376,14 @@ class LocationChannelManager private constructor(private val context: Context) {
// If no provider was available, turn off loading state
if (!providerFound) {
Log.w(TAG, "No location providers available")
_isLoadingLocation.postValue(false)
_isLoadingLocation.value = false
}
} catch (e: SecurityException) {
Log.e(TAG, "Security exception requesting location: ${e.message}")
_isLoadingLocation.postValue(false) // Turn off loading state on error
_isLoadingLocation.value = false // Turn off loading state on error
} catch (e: Exception) {
Log.e(TAG, "Error requesting location: ${e.message}")
_isLoadingLocation.postValue(false) // Turn off loading state on error
_isLoadingLocation.value = false // Turn off loading state on error
}
}
@@ -408,7 +406,7 @@ class LocationChannelManager private constructor(private val context: Context) {
private fun updatePermissionState() {
val newState = getCurrentPermissionStatus()
Log.d(TAG, "Permission state updated to: $newState")
_permissionState.postValue(newState)
_permissionState.value = newState
}
private fun hasLocationPermission(): Boolean {
@@ -433,13 +431,13 @@ class LocationChannelManager private constructor(private val context: Context) {
Log.v(TAG, "Generated ${level.displayName}: $geohash")
}
_availableChannels.postValue(result)
_availableChannels.value = result
// Recompute teleported status based on current location vs selected channel
val selectedChannelValue = _selectedChannel.value
when (selectedChannelValue) {
is ChannelID.Mesh -> {
_teleported.postValue(false)
_teleported.value = false
}
is ChannelID.Location -> {
val currentGeohash = Geohash.encode(
@@ -448,12 +446,9 @@ class LocationChannelManager private constructor(private val context: Context) {
precision = selectedChannelValue.channel.level.precision
)
val isTeleported = currentGeohash != selectedChannelValue.channel.geohash
_teleported.postValue(isTeleported)
_teleported.value = isTeleported
Log.d(TAG, "Teleported status: $isTeleported (current: $currentGeohash, selected: ${selectedChannelValue.channel.geohash})")
}
null -> {
_teleported.postValue(false)
}
}
}
@@ -482,7 +477,7 @@ class LocationChannelManager private constructor(private val context: Context) {
val names = namesByLevel(address)
Log.d(TAG, "Reverse geocoding result: $names")
_locationNames.postValue(names)
_locationNames.value = names
} else {
Log.w(TAG, "No reverse geocoding results")
}
@@ -601,26 +596,26 @@ class LocationChannelManager private constructor(private val context: Context) {
}
if (channel != null) {
_selectedChannel.postValue(channel)
_selectedChannel.value = channel
Log.d(TAG, "Restored persisted channel: ${channel.displayName}")
} else {
Log.d(TAG, "Could not restore persisted channel, defaulting to Mesh")
_selectedChannel.postValue(ChannelID.Mesh)
_selectedChannel.value = ChannelID.Mesh
}
} else {
Log.w(TAG, "Invalid channel data format in persistence")
_selectedChannel.postValue(ChannelID.Mesh)
_selectedChannel.value = ChannelID.Mesh
}
} else {
Log.d(TAG, "No persisted channel found, defaulting to Mesh")
_selectedChannel.postValue(ChannelID.Mesh)
_selectedChannel.value = ChannelID.Mesh
}
} catch (e: JsonSyntaxException) {
Log.e(TAG, "Failed to parse persisted channel data: ${e.message}")
_selectedChannel.postValue(ChannelID.Mesh)
_selectedChannel.value = ChannelID.Mesh
} catch (e: Exception) {
Log.e(TAG, "Failed to load persisted channel: ${e.message}")
_selectedChannel.postValue(ChannelID.Mesh)
_selectedChannel.value = ChannelID.Mesh
}
}
@@ -629,7 +624,7 @@ class LocationChannelManager private constructor(private val context: Context) {
*/
fun clearPersistedChannel() {
dataManager?.clearLastGeohashChannel()
_selectedChannel.postValue(ChannelID.Mesh)
_selectedChannel.value = ChannelID.Mesh
Log.d(TAG, "Cleared persisted channel selection")
}
@@ -653,11 +648,11 @@ class LocationChannelManager private constructor(private val context: Context) {
private fun loadLocationServicesState() {
try {
val enabled = dataManager?.isLocationServicesEnabled() ?: false
_locationServicesEnabled.postValue(enabled)
_locationServicesEnabled.value = enabled
Log.d(TAG, "Loaded location services state: $enabled")
} catch (e: Exception) {
Log.e(TAG, "Failed to load location services state: ${e.message}")
_locationServicesEnabled.postValue(false)
_locationServicesEnabled.value = false
}
}