Skip to content

Commit

Permalink
Code review [#25] P4, P5, P6, P7, P8 - Kotlin Channels, removed unnec…
Browse files Browse the repository at this point in the history
…essary comments and shackbars, Ui states now in ViewModels
  • Loading branch information
E-D-W-I-N committed May 15, 2021
1 parent 2fddb12 commit 3a0893a
Show file tree
Hide file tree
Showing 13 changed files with 180 additions and 146 deletions.
3 changes: 3 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ dependencies {
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"

// Lifecycle
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01"

// Fragment
implementation "androidx.fragment:fragment-ktx:1.3.3"

Expand Down
4 changes: 1 addition & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@
android:name="preloaded_fonts"
android:resource="@array/preloaded_fonts" />

<activity
android:name=".presentation.MainActivity"
android:screenOrientation="portrait">
<activity android:name=".presentation.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.edwin.weatherapp.extentions
package com.edwin.weatherapp.extensions

import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.Marker
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.edwin.weatherapp.extentions
package com.edwin.weatherapp.extensions

import android.view.View
import androidx.cardview.widget.CardView
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.edwin.weatherapp.extentions
package com.edwin.weatherapp.extensions

import android.content.Context
import android.widget.ImageView
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.edwin.weatherapp.extentions
package com.edwin.weatherapp.extensions

import android.content.Context
import android.graphics.Bitmap
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.edwin.weatherapp.extentions
package com.edwin.weatherapp.extensions

import android.view.View
import androidx.annotation.StringRes
Expand Down

This file was deleted.

132 changes: 61 additions & 71 deletions app/src/main/java/com/edwin/weatherapp/presentation/map/MapFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,20 @@ import androidx.activity.result.contract.ActivityResultContracts.RequestPermissi
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.edwin.domain.exception.MapException
import com.edwin.weatherapp.R
import com.edwin.weatherapp.databinding.MapFragmentBinding
import com.edwin.weatherapp.extentions.*
import com.edwin.weatherapp.extensions.*
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.OnMapReadyCallback
import com.google.android.gms.maps.model.LatLng
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.koin.androidx.viewmodel.ext.android.viewModel

class MapFragment : Fragment(R.layout.map_fragment), OnMapReadyCallback {
Expand All @@ -39,24 +44,62 @@ class MapFragment : Fragment(R.layout.map_fragment), OnMapReadyCallback {
RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
//Если разрешение выдано показываем Snackbar и выполняем действие
showSnackbar(getString(R.string.permission_granted))
moveCameraToCurrentLocation()
} else {
showSnackbar(getString(R.string.permission_denied))
viewModel.getFusedLocation()
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = MapFragmentBinding.bind(view)
setupObservers()
binding.apply {
mapView.onCreate(savedInstanceState)
mapView.getMapAsync(this@MapFragment)
}
setHasOptionsMenu(true)
}

private fun setupObservers() {
val uiStateFlow = viewModel.uiState.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
uiStateFlow.onEach { state ->
when (state) {
is MapViewModel.MapUiState.Loading -> binding.progressBar.visibility = View.VISIBLE
is MapViewModel.MapUiState.CurrentLocationLoaded -> {
binding.progressBar.visibility = View.INVISIBLE
val latLng = LatLng(state.location.latitude, state.location.longitude)
moveCameraToCurrentLocation(latLng)
}
is MapViewModel.MapUiState.AddressLoaded -> {
binding.progressBar.visibility = View.INVISIBLE
setupShowWeatherWindow(state.address)
}
is MapViewModel.MapUiState.Error -> binding.progressBar.visibility = View.INVISIBLE
else -> Unit
}
}.launchIn(viewLifecycleOwner.lifecycleScope)

val eventsFlow = viewModel.eventsFlow.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
eventsFlow.onEach { event ->
when (event) {
is MapViewModel.ActionState.ShowError -> {
when (event.throwable) {
is MapException.GeocoderFailed -> showSnackbar(
getString(R.string.check_connection_error_text)
)
is MapException.CityNotFound -> showSnackbar(
getString(R.string.no_city_error_text)
)
is MapException.NoLastLocation -> {
showSnackbar(getString(R.string.current_location_error_text)) {
action(R.string.action_retry) { viewModel.getFusedLocation() }
}
}
}
}
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
}

override fun onMapReady(googleMap: GoogleMap) {
map = googleMap
checkPermissions()
Expand All @@ -65,47 +108,30 @@ class MapFragment : Fragment(R.layout.map_fragment), OnMapReadyCallback {
map.addMarker {
position(latLng)
}
// Если текущий зум камеры выше стандартного, то не уменьшаем его
val cameraPosition = if (googleMap.cameraPosition.zoom > ON_CLICK_MAP_ZOOM) {
CameraUpdateFactory.newLatLng(latLng)
} else {
CameraUpdateFactory.newLatLngZoom(latLng, ON_CLICK_MAP_ZOOM)
}
map.animateCamera(cameraPosition)
// Делаем progressBar видимым и обновляем Address во ViewModel
binding.progressBar.visibility = View.VISIBLE
viewModel.getAddress(latLng.latitude, latLng.longitude)
}
viewModel.address.observe(viewLifecycleOwner, { result ->
// При успешном получении Address убираем progressBar и показываем окно
result.onSuccess { address ->
binding.progressBar.visibility = View.INVISIBLE
setupShowWeatherWindow(address)
}
// При неудачном получении Address убираем progressBar и обрабатываем ошибки
result.onFailure { exception ->
binding.progressBar.visibility = View.INVISIBLE
when (exception) {
is MapException.GeocoderFailed -> showSnackbar(
getString(R.string.check_connection_error_text)
)
is MapException.CityNotFound -> showSnackbar(
getString(R.string.no_city_error_text)
)
}
}
})
}

private fun moveCameraToCurrentLocation(latLng: LatLng) {
val cameraPosition = CameraUpdateFactory.newLatLngZoom(latLng, ON_START_MAP_ZOOM)
map.animateCamera(cameraPosition)
map.addMarker {
position(latLng)
title(getString(R.string.marker_title))
icon(requireContext(), R.drawable.ic_my_location)
}
}

private fun setupShowWeatherWindow(address: Address) = with(binding) {
cityName.text = address.locality
cityLatlng.text = getString(R.string.cityLatlng, address.latitude, address.longitude)
// По клику на кнопку закрытия окна скрываем его с анимацией
closeWindowButton.setOnClickListener {
showWeatherWindow.animateOut()
}
/* По клику на кнопку "Show weather" открываем WeatherDetailsFragment.
Через SafeArgs передаем в него название города и его координаты */
closeWindowButton.setOnClickListener { showWeatherWindow.animateOut() }
showWeatherButton.setOnClickListener {
val action = MapFragmentDirections.actionMapFragmentToWeatherDetailsFragment(
address.locality, address.latitude.toFloat(), address.longitude.toFloat()
Expand All @@ -121,50 +147,15 @@ class MapFragment : Fragment(R.layout.map_fragment), OnMapReadyCallback {
requireContext(),
LOCATION_PERMISSION
) == PackageManager.PERMISSION_GRANTED -> {
moveCameraToCurrentLocation()
viewModel.getFusedLocation()
}
// Если пользователь отказался давать разрешение, то пытаемся убедить его :)
shouldShowRequestPermissionRationale(LOCATION_PERMISSION) -> {
showDialog()
}
else -> requestPermissionLauncher.launch(LOCATION_PERMISSION)
}
}

private fun moveCameraToCurrentLocation() {
binding.progressBar.visibility = View.VISIBLE
// Обновляем значение LiveData во ViewModel
viewModel.getFusedLocation()
viewModel.fusedLocation.observe(viewLifecycleOwner, { result ->
// Если локация пользователя получена успешно, то приближаем камеру и ставим маркер
result.onSuccess { location ->
binding.progressBar.visibility = View.INVISIBLE
val latLng = LatLng(location.latitude, location.longitude)
val cameraPosition = CameraUpdateFactory.newLatLngZoom(latLng, ON_START_MAP_ZOOM)
map.animateCamera(cameraPosition)
map.addMarker {
position(latLng)
title(getString(R.string.marker_title))
icon(requireContext(), R.drawable.ic_my_location)
}
}
// Если при получении локации возникли исключения, то обрабатываем их
result.onFailure { exception ->
binding.progressBar.visibility = View.INVISIBLE
when (exception) {
is MapException.NoLastLocation -> {
showSnackbar(getString(R.string.current_location_error_text)) {
action(R.string.action_retry) {
binding.progressBar.visibility = View.VISIBLE
viewModel.getFusedLocation()
}
}
}
}
}
})
}

private fun showDialog() {
AlertDialog.Builder(requireContext(), R.style.AlertDialogTheme)
.setMessage(getString(R.string.permission_dialog_message))
Expand All @@ -183,7 +174,6 @@ class MapFragment : Fragment(R.layout.map_fragment), OnMapReadyCallback {

override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
// При клике на иконку поиска в меню показываем Snackbar
R.id.action_search -> {
showSnackbar(getString(R.string.search_toast_text))
true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,58 @@ package com.edwin.weatherapp.presentation.map

import android.location.Address
import android.location.Location
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.edwin.domain.usecase.GetAddressFromGeocoderUseCase
import com.edwin.domain.usecase.GetFusedLocationUseCase
import com.edwin.weatherapp.extentions.asLiveData
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch

class MapViewModel(
private val getFusedLocationUseCase: GetFusedLocationUseCase,
private val getAddressFromGeocoderUseCase: GetAddressFromGeocoderUseCase
) : ViewModel() {

private val _fusedLocation = MutableLiveData<Result<Location>>()
val fusedLocation: LiveData<Result<Location>> = _fusedLocation.asLiveData()
private val _uiState = MutableStateFlow<MapUiState>(MapUiState.Default)
val uiState: StateFlow<MapUiState> = _uiState.asStateFlow()

private val _address = MutableLiveData<Result<Address>>()
val address: LiveData<Result<Address>> = _address.asLiveData()
private val eventChannel = Channel<ActionState>(Channel.BUFFERED)
val eventsFlow = eventChannel.receiveAsFlow()

fun getFusedLocation() = viewModelScope.launch {
_fusedLocation.value = getFusedLocationUseCase.invoke().single()
_uiState.value = MapUiState.Loading
val location = getFusedLocationUseCase.invoke().single()
location.fold(
onSuccess = { _uiState.value = MapUiState.CurrentLocationLoaded(it) },
onFailure = {
_uiState.value = MapUiState.Error
eventChannel.send(ActionState.ShowError(it))
}
)
}

fun getAddress(latitude: Double, longitude: Double) = viewModelScope.launch {
_address.value = getAddressFromGeocoderUseCase.invoke(latitude, longitude).single()
_uiState.value = MapUiState.Loading
val address = getAddressFromGeocoderUseCase.invoke(latitude, longitude).single()
address.fold(
onSuccess = { _uiState.value = MapUiState.AddressLoaded(it) },
onFailure = {
_uiState.value = MapUiState.Error
eventChannel.send(ActionState.ShowError(it))
}
)
}

sealed class MapUiState {
object Loading : MapUiState()
object Error : MapUiState()
object Default : MapUiState()
data class CurrentLocationLoaded(val location: Location) : MapUiState()
data class AddressLoaded(val address: Address) : MapUiState()
}

sealed class ActionState {
data class ShowError(val throwable: Throwable) : ActionState()
}
}
Loading

0 comments on commit 3a0893a

Please sign in to comment.