From a875d80d3813faaf8a0229c6e3ba63380271e76e Mon Sep 17 00:00:00 2001 From: Johan von Forstner Date: Sun, 12 Apr 2020 07:55:11 +0200 Subject: [PATCH] migrate to ViewModel --- .../com/johan/evmap/fragment/MapFragment.kt | 132 +++------------- .../com/johan/evmap/viewmodel/MapViewModel.kt | 141 ++++++++++++++++++ .../java/com/johan/evmap/viewmodel/Utils.kt | 9 ++ app/src/main/res/layout/fragment_map.xml | 4 +- 4 files changed, 175 insertions(+), 111 deletions(-) create mode 100644 app/src/main/java/com/johan/evmap/viewmodel/MapViewModel.kt create mode 100644 app/src/main/java/com/johan/evmap/viewmodel/Utils.kt diff --git a/app/src/main/java/com/johan/evmap/fragment/MapFragment.kt b/app/src/main/java/com/johan/evmap/fragment/MapFragment.kt index cba91ed4..69746949 100644 --- a/app/src/main/java/com/johan/evmap/fragment/MapFragment.kt +++ b/app/src/main/java/com/johan/evmap/fragment/MapFragment.kt @@ -12,7 +12,8 @@ import androidx.core.content.ContextCompat import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import androidx.lifecycle.* +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.Observer import androidx.navigation.fragment.findNavController import androidx.navigation.ui.setupWithNavController import androidx.recyclerview.widget.DividerItemDecoration @@ -32,42 +33,25 @@ import com.johan.evmap.REQUEST_LOCATION_PERMISSION import com.johan.evmap.adapter.ConnectorAdapter import com.johan.evmap.adapter.DetailAdapter import com.johan.evmap.adapter.GalleryAdapter -import com.johan.evmap.api.* +import com.johan.evmap.api.ChargeLocation +import com.johan.evmap.api.ChargeLocationCluster +import com.johan.evmap.api.ChargepointListItem +import com.johan.evmap.api.ChargerPhoto import com.johan.evmap.databinding.FragmentMapBinding import com.johan.evmap.ui.ClusterIconGenerator import com.johan.evmap.ui.getBitmapDescriptor +import com.johan.evmap.viewmodel.MapPosition +import com.johan.evmap.viewmodel.MapViewModel +import com.johan.evmap.viewmodel.viewModelFactory import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike import kotlinx.android.synthetic.main.fragment_map.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response -import java.io.IOException - -class MapViewModel : ViewModel() { - val chargepoints: MutableLiveData> by lazy { - MutableLiveData>().apply { - value = emptyList() - } - } - val charger: MutableLiveData by lazy { - MutableLiveData() - } - val availability: MutableLiveData by lazy { - MutableLiveData() - } - val myLocationEnabled: MutableLiveData by lazy { - MutableLiveData() - } -} class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallback { private lateinit var binding: FragmentMapBinding - private val vm: MapViewModel by viewModels() + private val vm: MapViewModel by viewModels(factoryProducer = { + viewModelFactory { MapViewModel(getString(R.string.goingelectric_key)) } + }) private var map: GoogleMap? = null - private lateinit var api: GoingElectricApi private lateinit var fusedLocationClient: FusedLocationProviderClient private lateinit var bottomSheetBehavior: BottomSheetBehaviorGoogleMapsLike private var markers: Map = emptyMap() @@ -83,7 +67,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac binding.vm = vm fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireContext()) - api = GoingElectricApi.create(getString(R.string.goingelectric_key)) setHasOptionsMenu(true) @@ -155,13 +138,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac override fun onChanged(charger: ChargeLocation?) { if (charger != null) { - if (previousCharger == null || - previousCharger!!.id != charger.id - ) { - vm.availability.value = null + if (previousCharger == null || previousCharger!!.id != charger.id) { bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED - loadChargerDetails() } } else { bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN @@ -225,12 +204,14 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac override fun onMapReady(map: GoogleMap) { this.map = map map.setOnCameraIdleListener { - loadChargepoints() + vm.mapPosition.value = MapPosition( + map.projection.visibleRegion.latLngBounds, map.cameraPosition.zoom + ) } map.setOnMarkerClickListener { marker -> when (marker) { in markers -> { - vm.charger.value = markers[marker] + vm.chargerSparse.value = markers[marker] true } in clusterMarkers -> { @@ -242,7 +223,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac } } map.setOnMapClickListener { - vm.charger.value = null + vm.chargerSparse.value = null } val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK @@ -260,6 +241,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac val cameraUpdate = CameraUpdateFactory.newLatLngZoom(LatLng(50.113388, 9.252536), 3.5f) map.moveCamera(cameraUpdate) } + + vm.mapPosition.value = MapPosition( + map.projection.visibleRegion.latLngBounds, map.cameraPosition.zoom + ) } @SuppressLint("MissingPermission") @@ -290,77 +275,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac PackageManager.PERMISSION_GRANTED } - private fun loadChargepoints() { - val map = this.map ?: return - val bounds = map.projection.visibleRegion.latLngBounds - api.getChargepoints( - bounds.southwest.latitude, bounds.southwest.longitude, - bounds.northeast.latitude, bounds.northeast.longitude, - clustering = map.cameraPosition.zoom < 12, zoom = map.cameraPosition.zoom, - clusterDistance = 70 - ).enqueue(object : Callback { - override fun onFailure(call: Call, t: Throwable) { - //TODO: show error message - t.printStackTrace() - } - - override fun onResponse( - call: Call, - response: Response - ) { - if (!response.isSuccessful || response.body()!!.status != "ok") { - //TODO: show error message - return - } - - vm.chargepoints.value = response.body()!!.chargelocations - } - }) - } - - private fun loadChargerDetails() { - val charger = vm.charger.value ?: return - api.getChargepointDetail(charger.id).enqueue(object : Callback { - override fun onFailure(call: Call, t: Throwable) { - //TODO: show error message - t.printStackTrace() - } - - override fun onResponse( - call: Call, - response: Response - ) { - if (!response.isSuccessful || response.body()!!.status != "ok") { - //TODO: show error message - return - } - - vm.charger.value = response.body()!!.chargelocations[0] as ChargeLocation - } - }) - - lifecycleScope.launch { - loadChargerAvailability(charger) - } - } - - private suspend fun loadChargerAvailability(charger: ChargeLocation) { - var availability: ChargeLocationStatus? = null - withContext(Dispatchers.IO) { - for (ad in availabilityDetectors) { - try { - availability = ad.getAvailability(charger) - break - } catch (e: IOException) { - e.printStackTrace() - } catch (e: AvailabilityDetectorException) { - e.printStackTrace() - } - } - } - vm.availability.value = availability - } - private fun updateMap(chargepoints: List) { val map = this.map ?: return markers.keys.forEach { it.remove() } @@ -434,7 +348,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED true } else if (bottomSheetBehavior.state == BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED) { - vm.charger.value = null + vm.chargerSparse.value = null true } else { false diff --git a/app/src/main/java/com/johan/evmap/viewmodel/MapViewModel.kt b/app/src/main/java/com/johan/evmap/viewmodel/MapViewModel.kt new file mode 100644 index 00000000..8dc251b4 --- /dev/null +++ b/app/src/main/java/com/johan/evmap/viewmodel/MapViewModel.kt @@ -0,0 +1,141 @@ +package com.johan.evmap.viewmodel + +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.android.gms.maps.model.LatLngBounds +import com.johan.evmap.api.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.io.IOException + +data class MapPosition(val bounds: LatLngBounds, val zoom: Float) + +class MapViewModel(geApiKey: String) : ViewModel() { + private var api: GoingElectricApi = + GoingElectricApi.create(geApiKey) + + val mapPosition: MutableLiveData by lazy { + MutableLiveData() + } + val chargepoints: MediatorLiveData> by lazy { + MediatorLiveData>() + .apply { + value = emptyList() + addSource(mapPosition) { + mapPosition.value?.let { pos -> loadChargepoints(pos) } + } + } + } + + val chargerSparse: MutableLiveData by lazy { + MutableLiveData() + } + val chargerDetails: MediatorLiveData by lazy { + MediatorLiveData().apply { + addSource(chargerSparse) { charger -> + if (charger != null) { + value = null + loadChargerDetails(charger) + } else { + value = null + } + } + } + } + val charger: MediatorLiveData by lazy { + MediatorLiveData().apply { + addSource(chargerSparse) { value = it } + addSource(chargerDetails) { if (it != null) value = it } + } + } + val availability: MediatorLiveData by lazy { + MediatorLiveData().apply { + addSource(chargerDetails) { charger -> + if (charger != null) { + value = null + viewModelScope.launch { + loadAvailability(charger) + } + } else { + value = null + } + } + } + } + val myLocationEnabled: MutableLiveData by lazy { + MutableLiveData() + } + + private fun loadChargepoints(mapPosition: MapPosition) { + val bounds = mapPosition.bounds + val zoom = mapPosition.zoom + api.getChargepoints( + bounds.southwest.latitude, bounds.southwest.longitude, + bounds.northeast.latitude, bounds.northeast.longitude, + clustering = zoom < 12, zoom = zoom, + clusterDistance = 70 + ).enqueue(object : Callback { + override fun onFailure(call: Call, t: Throwable) { + //TODO: show error message + t.printStackTrace() + } + + override fun onResponse( + call: Call, + response: Response + ) { + if (!response.isSuccessful || response.body()!!.status != "ok") { + //TODO: show error message + return + } + + chargepoints.value = response.body()!!.chargelocations + } + }) + } + + private suspend fun loadAvailability(charger: ChargeLocation) { + var value: ChargeLocationStatus? = null + withContext(Dispatchers.IO) { + for (ad in availabilityDetectors) { + try { + value = ad.getAvailability(charger) + break + } catch (e: IOException) { + e.printStackTrace() + } catch (e: AvailabilityDetectorException) { + e.printStackTrace() + } + } + } + availability.value = value + } + + private fun loadChargerDetails(charger: ChargeLocation) { + api.getChargepointDetail(charger.id).enqueue(object : + Callback { + override fun onFailure(call: Call, t: Throwable) { + //TODO: show error message + t.printStackTrace() + } + + override fun onResponse( + call: Call, + response: Response + ) { + if (!response.isSuccessful || response.body()!!.status != "ok") { + //TODO: show error message + return + } + + chargerDetails.value = response.body()!!.chargelocations[0] as ChargeLocation + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/johan/evmap/viewmodel/Utils.kt b/app/src/main/java/com/johan/evmap/viewmodel/Utils.kt new file mode 100644 index 00000000..5eafa20b --- /dev/null +++ b/app/src/main/java/com/johan/evmap/viewmodel/Utils.kt @@ -0,0 +1,9 @@ +package com.johan.evmap.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider + +inline fun viewModelFactory(crossinline f: () -> VM) = + object : ViewModelProvider.Factory { + override fun create(aClass: Class): T = f() as T + } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_map.xml b/app/src/main/res/layout/fragment_map.xml index e44ab4cc..4356de21 100644 --- a/app/src/main/res/layout/fragment_map.xml +++ b/app/src/main/res/layout/fragment_map.xml @@ -5,11 +5,11 @@ - + + type="com.johan.evmap.viewmodel.MapViewModel" />