diff --git a/.idea/misc.xml b/.idea/misc.xml index f23a13b..59a266e 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -5,7 +5,7 @@ - + diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 71ce279..94a25f7 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,6 +2,5 @@ - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 034a26f..4d45170 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,9 +20,11 @@ android { } buildFeatures { - viewBinding = true + viewBinding true + dataBinding true } + buildTypes { release { minifyEnabled false @@ -40,6 +42,7 @@ android { dependencies { implementation 'com.google.android.gms:play-services-location:19.0.1' + def room_version = "2.4.2" implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" @@ -48,7 +51,11 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'com.google.android.material:material:1.5.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.3' - implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.1" + + def lifecycle = "2.4.1" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle" //SwipeRefreshLayout implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" @@ -59,23 +66,28 @@ dependencies { //Coil implementation "io.coil-kt:coil:1.1.1" + def viewBinding = "1.5.3" + implementation "com.github.kirich1409:viewbindingpropertydelegate:$viewBinding" + // To use only without reflection variants of viewBinding + implementation "com.github.kirich1409:viewbindingpropertydelegate-noreflection:$viewBinding" + def navigation = "2.4.1" implementation "androidx.navigation:navigation-fragment-ktx:$navigation" implementation "androidx.navigation:navigation-ui-ktx:$navigation" implementation "androidx.navigation:navigation-runtime-ktx:$navigation" - - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0" + def coroutines = "1.6.0" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" // region Network def retrofit = "2.9.0" - implementation "com.squareup.retrofit2:retrofit:${retrofit}" - implementation "com.squareup.retrofit2:converter-gson:${retrofit}" + implementation "com.squareup.retrofit2:retrofit:$retrofit" + implementation "com.squareup.retrofit2:converter-gson:$retrofit" def okhttp = "4.9.3" - implementation "com.squareup.okhttp3:okhttp:${okhttp}" - debugImplementation "com.squareup.okhttp3:logging-interceptor:${okhttp}" + implementation "com.squareup.okhttp3:okhttp:$okhttp" + debugImplementation "com.squareup.okhttp3:logging-interceptor:$okhttp" // endregion testImplementation 'junit:junit:4.13.2' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f82e4e2..013d722 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ + > = MutableLiveData() + val weather: LiveData> = _weather + + private val _weatherList: MutableLiveData>> = MutableLiveData() + val weatherList: LiveData>> = _weatherList + + private val _cityId: MutableLiveData>> = MutableLiveData() + val cityId: LiveData>> = _cityId + + fun onGetWeather(cityId : Int) { + viewModelScope.launch { + kotlin.runCatching { + getWeatherUseCase(cityId) + }.onSuccess { + _weather.value = Result.success(it) + }.onFailure { + _weather.value = Result.failure(it) + } + } + } + + fun onGetWeatherList(lat: Double, lon: Double, cnt: Int) { + viewModelScope.launch { + kotlin.runCatching { + getWeatherListUseCase(lat, lon, cnt) + }.onSuccess { + _weatherList.value = Result.success(it) + }.onFailure { + _weatherList.value = Result.failure(it) + } + } + } + + fun onGetCityId(cityName: String) { + viewModelScope.launch { + kotlin.runCatching { + getWeatherUseCase(cityName).id + }.onSuccess { + _cityId.value = Event(Result.success(it)) + }.onFailure { + _cityId.value = Event(Result.failure(it)) + } + } + } +} diff --git a/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/activities/MainActivity.kt b/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/activities/MainActivity.kt index ef3f1a7..403f339 100644 --- a/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/activities/MainActivity.kt +++ b/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/activities/MainActivity.kt @@ -3,24 +3,22 @@ package ru.itis.karakurik.androidLab2.presentation.activities import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.navigation.NavController +import by.kirich1409.viewbindingdelegate.viewBinding +import ru.itis.karakurik.androidLab2.R import ru.itis.karakurik.androidLab2.databinding.ActivityMainBinding import ru.itis.karakurik.androidLab2.extentions.findController -class MainActivity : AppCompatActivity() { - private var binding: ActivityMainBinding? = null +class MainActivity : AppCompatActivity(R.layout.activity_main) { + private val binding by viewBinding(ActivityMainBinding::bind) private var controller: NavController? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = ActivityMainBinding.inflate(layoutInflater).also { - setContentView(it.root) - } - controller = binding?.navHostFragmentMain?.id?.let { findController(it) } + controller = binding.navHostFragmentMain.id.let { findController(it) } } override fun onDestroy() { super.onDestroy() - binding = null controller = null } } diff --git a/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/fragments/DetailsFragment.kt b/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/fragments/DetailsFragment.kt index bfb668b..9ee987e 100644 --- a/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/fragments/DetailsFragment.kt +++ b/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/fragments/DetailsFragment.kt @@ -1,75 +1,100 @@ package ru.itis.karakurik.androidLab2.presentation.fragments +import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope -import coil.imageLoader +import androidx.lifecycle.ViewModelProvider import coil.load -import coil.request.CachePolicy -import coil.request.ImageRequest -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import ru.itis.karakurik.androidLab2.R import ru.itis.karakurik.androidLab2.databinding.FragmentDetailsBinding import ru.itis.karakurik.androidLab2.di.DiContainer import ru.itis.karakurik.androidLab2.domain.entity.Weather -import ru.itis.karakurik.androidLab2.domain.usecase.GetWeatherUseCase +import ru.itis.karakurik.androidLab2.presentation.MainViewModel +import ru.itis.karakurik.androidLab2.presentation.convertors.TempColorConverter +import ru.itis.karakurik.androidLab2.presentation.utils.ViewModelFactory +import timber.log.Timber -class DetailsFragment : Fragment(R.layout.fragment_details) { - private var cityId: Int? = null - private var weather: Weather? = null +class DetailsFragment : Fragment() { + private var _binding: FragmentDetailsBinding? = null + private val binding get() = _binding!! - private val getWeatherUseCase: GetWeatherUseCase by lazy { - GetWeatherUseCase(DiContainer.weatherRepository, Dispatchers.Default) + private val viewModel by lazy { + ViewModelProvider( + this, + ViewModelFactory( + DiContainer.getWeatherUseCase, + DiContainer.getWeatherListUseCase + ) + )[MainViewModel::class.java] } - private var binding: FragmentDetailsBinding? = null - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_details, container, false) + savedInstanceState: Bundle?, + ): View { + _binding = FragmentDetailsBinding.inflate(inflater, container, false) + return binding.root } override fun onViewCreated( view: View, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - binding = FragmentDetailsBinding.bind(view) - arguments?.let { - cityId = it.getInt("city_id") + initObservers() + + arguments?.getInt(getString(R.string.city_id))?.let { + viewModel.onGetWeather(it) } + } - lifecycleScope.launch { - weather = cityId?.let { getWeatherUseCase(it) } + private fun initObservers() { + viewModel.weather.observe(viewLifecycleOwner) { result -> + result.fold(onSuccess = { + setWeatherData(it) + }, onFailure = { + Timber.e(it.message.toString()) + }) + } + } - binding?.run { - weather?.let { - tvCity.text = it.name - tvTemp.text = "${it.temp}°C" - tvTempMin.text = "${it.tempMin}°C" - tvTempMax.text = "${it.tempMax}°C" - tvWindDeg.text = it.windDeg.toString() - tvWindSpeed.text = "${it.windSpeed}km/h" - tvHumidity.text = "${it.humidity}%" - tvPressure.text = "${it.pressure}P" - ivAir.context.imageLoader.execute( - ImageRequest.Builder(ivAir.context) - .data(it.iconUrl) - .memoryCachePolicy(CachePolicy.DISABLED) - .placeholder(R.drawable.air) - .target(ivAir) - .build() - ) + @SuppressLint("SetTextI18n") + private fun setWeatherData(weather: Weather) { + with(binding) { + weather.run { + setWeather(weather) + tvCity.text = name + tvTemp.text = "${temp}°C" + tvTempMin.text = "${tempMin}°C" + tvTempMax.text = "${tempMax}°C" + tvWindDeg.text = windDeg.toString() + tvWindSpeed.text = "${windSpeed}km/h" + tvHumidity.text = "${humidity}%" + tvPressure.text = "${pressure}P" + ivAir.load(iconUrl) + + arguments?.getString(R.string.transition_name.toString())?.let { + ivAir.transitionName = it } + + tvTemp.setTextColor( + ContextCompat.getColor( + tvTemp.context, + TempColorConverter.getColor(weather.temp) + ) + ) } } } + + override fun onDestroy() { + super.onDestroy() + _binding = null + } } diff --git a/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/fragments/list/SearchFragment.kt b/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/fragments/list/SearchFragment.kt index bcd0a06..24e22c1 100644 --- a/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/fragments/list/SearchFragment.kt +++ b/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/fragments/list/SearchFragment.kt @@ -1,107 +1,179 @@ package ru.itis.karakurik.androidLab2.presentation.fragments.list -import android.Manifest +import android.Manifest.permission.* import android.content.Intent -import android.content.pm.PackageManager +import android.content.pm.PackageManager.PERMISSION_DENIED import android.os.Bundle import android.provider.Settings import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.widget.SearchView import androidx.core.app.ActivityCompat import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearSmoothScroller +import androidx.recyclerview.widget.RecyclerView import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.LocationServices import com.google.android.material.snackbar.Snackbar -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.launch import ru.itis.karakurik.androidLab2.R import ru.itis.karakurik.androidLab2.databinding.FragmentSearchBinding import ru.itis.karakurik.androidLab2.di.DiContainer -import ru.itis.karakurik.androidLab2.domain.usecase.GetWeatherUseCase -import ru.itis.karakurik.androidLab2.domain.usecase.GetWeathersUseCase +import ru.itis.karakurik.androidLab2.presentation.MainViewModel import ru.itis.karakurik.androidLab2.presentation.fragments.list.recycler.ListRecyclerAdapter +import ru.itis.karakurik.androidLab2.presentation.utils.ViewModelFactory import timber.log.Timber private const val COUNT_OF_CITIES_IN_LIST = 20 private const val DEFAULT_LAT = 55.7887 private const val DEFAULT_LON = 49.1221 -private const val REQUEST_CODE_100 = 100 +private const val TRANSITION_NAME = "transition_name" -class SearchFragment : Fragment(R.layout.fragment_search) { - private var binding: FragmentSearchBinding? = null - private var listRecyclerAdapter: ListRecyclerAdapter? = null +class SearchFragment : Fragment() { + companion object { + private val permissions = arrayOf( + ACCESS_FINE_LOCATION, + ACCESS_COARSE_LOCATION, + ACCESS_LOCATION_EXTRA_COMMANDS + ) + } + + private var _binding: FragmentSearchBinding? = null + private val binding get() = _binding!! + private var listRecyclerAdapter: ListRecyclerAdapter? = null private var userLat: Double = DEFAULT_LAT private var userLon: Double = DEFAULT_LON private var userLocation: FusedLocationProviderClient? = null - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) - - private val getWeatherUseCase: GetWeatherUseCase by lazy { - GetWeatherUseCase(DiContainer.weatherRepository, Dispatchers.Default) + private val viewModel by lazy { + ViewModelProvider( + requireActivity(), + ViewModelFactory( + DiContainer.getWeatherUseCase, + DiContainer.getWeatherListUseCase + ) + )[MainViewModel::class.java] } - private val getWeatherListUseCase: GetWeathersUseCase by lazy { - GetWeathersUseCase(DiContainer.weatherRepository, Dispatchers.Default) + private val requestPermissions = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { it -> + var allPermissionsGranted = true + for (granted in it.values) { + allPermissionsGranted = allPermissionsGranted and granted + } + if (allPermissionsGranted) { + getUserLocation() + } else { + Toast.makeText( + context, + "Разрешения не даны", + Toast.LENGTH_LONG + ).show() + } + } + + private val smoothScroller: RecyclerView.SmoothScroller by lazy { + object : LinearSmoothScroller(context) { + override fun getVerticalSnapPreference(): Int { + return SNAP_TO_START + } + } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ): View { - return inflater.inflate(R.layout.fragment_search, container, false) + _binding = FragmentSearchBinding.inflate(inflater, container, false) + return binding.root } override fun onViewCreated( view: View, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - binding = FragmentSearchBinding.bind(view) + initObservers() getUserLocation() initRecyclerView() - handleSearchQuery() - initSwipeRefreshList() } + private fun initObservers() { + with(viewModel) { + cityId.observe(viewLifecycleOwner) { event -> + event.getContentIfNotHandled()?.let { result -> + result.fold( + onSuccess = { + showDetailsFragment(it) + }, + onFailure = { + Toast.makeText( + context, + "Не удалось найти такой город", + Toast.LENGTH_LONG + ).show() + Timber.d("Do not found city") + } + ) + } + } + + weatherList.observe(viewLifecycleOwner) { result -> + result.fold( + onSuccess = { + listRecyclerAdapter?.submitList(it) + scrollRecyclerViewToPosition(0) + }, + onFailure = { + Timber.e("Error due to get weathers") + Timber.e(it.message.toString()) + Toast.makeText( + context, + "Не удалось найти", + Toast.LENGTH_LONG + ).show() + } + ) + } + } + } + private fun initSwipeRefreshList() { - binding?.swipeRefreshLayout?.let { + binding.swipeRefreshLayout.let { it.setOnRefreshListener { getUserLocation() + updateWeatherList(userLat, userLon, COUNT_OF_CITIES_IN_LIST) it.isRefreshing = false } } } private fun getUserLocation() { - context?.let { + context?.let { context -> if (ActivityCompat.checkSelfPermission( - it, - Manifest.permission.ACCESS_COARSE_LOCATION - ) == PackageManager.PERMISSION_DENIED || + context, + ACCESS_COARSE_LOCATION + ) == PERMISSION_DENIED || ActivityCompat.checkSelfPermission( - it, - Manifest.permission.ACCESS_FINE_LOCATION - ) == PackageManager.PERMISSION_DENIED + context, + ACCESS_FINE_LOCATION + ) == PERMISSION_DENIED || + ActivityCompat.checkSelfPermission( + context, + ACCESS_LOCATION_EXTRA_COMMANDS + ) == PERMISSION_DENIED ) { - val permissions = arrayOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.ACCESS_COARSE_LOCATION - ) Timber.d("Request location permissions") - requestPermissions(permissions, REQUEST_CODE_100) + requestPermissions.launch(permissions) } else { Timber.d("Get user location") userLocation = LocationServices.getFusedLocationProviderClient(requireContext()) @@ -110,14 +182,14 @@ class SearchFragment : Fragment(R.layout.fragment_search) { userLon = location.longitude userLat = location.latitude Timber.d("Location found") - binding?.let { it -> - Snackbar.make(it.root, "Местоположение найдено", Snackbar.LENGTH_LONG) - .show() - } - getWeatherList(userLat, userLon, COUNT_OF_CITIES_IN_LIST) + Toast.makeText( + context, + "Местоположение найдено", + Toast.LENGTH_SHORT + ).show() } else { Timber.d("Location not found") - binding?.let { it -> + binding.let { it -> Snackbar.make( it.root, "Местоположение не найдено\nВключите геолокацию", @@ -137,47 +209,11 @@ class SearchFragment : Fragment(R.layout.fragment_search) { } } - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - when (requestCode) { - REQUEST_CODE_100 -> { - if (grantResults.isNotEmpty() - && grantResults[0] == PackageManager.PERMISSION_GRANTED - && grantResults[1] == PackageManager.PERMISSION_GRANTED - ) { - Timber.d("Permissions granted") - getUserLocation() - } else { - Timber.d("Permissions denied") - binding?.let { - Snackbar.make(it.root, "Разрешения не даны", Snackbar.LENGTH_SHORT) - .show() - } - } - } - } - } - - private fun handleSearchQuery() { - binding?.svSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + binding.svSearch.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { - lifecycleScope.launch { - Timber.d("Pressed query button") - getCityId(query)?.let { - showDetailsFragment(it) - } ?: run { - Toast.makeText( - context, - "Не удалось найти такой город", - Toast.LENGTH_LONG - ).show() - Timber.d("Do not found city") - } - } + Timber.d("Pressed query button") + viewModel.onGetCityId(query) return false } @@ -188,46 +224,39 @@ class SearchFragment : Fragment(R.layout.fragment_search) { }) } - private suspend fun getCityId( - city: String - ) = kotlin.runCatching { - getWeatherUseCase(city).id - }.getOrNull() - - private fun initRecyclerView() { - binding?.rvSearch?.run { + binding.rvSearch.run { listRecyclerAdapter = ListRecyclerAdapter { id -> showDetailsFragment(id) } adapter = listRecyclerAdapter } - - getWeatherList(userLat, userLon, COUNT_OF_CITIES_IN_LIST) + updateWeatherList(userLat, userLon, COUNT_OF_CITIES_IN_LIST) } - private fun getWeatherList(lat: Double, lon: Double, cnt: Int) { + private fun updateWeatherList(lat: Double, lon: Double, cnt: Int) { + viewModel.onGetWeatherList(lat, lon, cnt) Timber.d("Get weathers list") - lifecycleScope.launch { - try { - val weatherList = getWeatherListUseCase(lat, lon, cnt) - listRecyclerAdapter?.submitList(weatherList) - binding?.rvSearch?.layoutManager?.scrollToPosition(0) - } catch (ex: Exception) { - Timber.e("Error due to get weathers") - Timber.e(ex.message.toString()) - Toast.makeText( - context, - "Не удалось найти", - Toast.LENGTH_LONG - ).show() - } - } } - private fun showDetailsFragment(id: Int) = findNavController().navigate( + private fun showDetailsFragment( + id: Int, + ) = findNavController().navigate( SearchFragmentDirections.actionFragmentSearchToFragmentDetails( - id + id, + "Item_$id" ) ) + + private fun scrollRecyclerViewToPosition(position: Int) { + binding.rvSearch.layoutManager?.startSmoothScroll(smoothScroller.apply { + targetPosition = position + }) + } + + override fun onDestroy() { + super.onDestroy() + listRecyclerAdapter = null + _binding = null + } } diff --git a/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/fragments/list/recycler/ListItemViewHolder.kt b/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/fragments/list/recycler/ListItemViewHolder.kt index 503d9e2..6f96bf5 100644 --- a/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/fragments/list/recycler/ListItemViewHolder.kt +++ b/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/fragments/list/recycler/ListItemViewHolder.kt @@ -5,6 +5,7 @@ import android.view.ViewGroup import androidx.annotation.ColorRes import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView +import coil.load import ru.itis.karakurik.androidLab2.databinding.ListItemCityBinding import ru.itis.karakurik.androidLab2.domain.entity.Weather import ru.itis.karakurik.androidLab2.presentation.convertors.TempColorConverter.getColor @@ -15,6 +16,7 @@ class ListItemViewHolder( ) : RecyclerView.ViewHolder(binding.root) { fun bind(weather: Weather) { + with(binding) { tvCityItem.text = weather.name tvTempItem.text = weather.temp.toString() @@ -24,6 +26,10 @@ class ListItemViewHolder( getColor(weather.temp) ) ) + ivIconItem.run { + load(weather.iconUrl) + transitionName = "Item_${weather.id}" + } root.setOnClickListener { onItemClick(weather.id) diff --git a/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/fragments/list/recycler/ListRecyclerAdapter.kt b/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/fragments/list/recycler/ListRecyclerAdapter.kt index fd468f5..97ae06d 100644 --- a/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/fragments/list/recycler/ListRecyclerAdapter.kt +++ b/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/fragments/list/recycler/ListRecyclerAdapter.kt @@ -21,9 +21,4 @@ class ListRecyclerAdapter( ) = holder.bind( getItem(position) ) - - /*override fun submitList(list: MutableList?) { - super.submitList(if (list == null) null else ArrayList(list)) - }*/ - } diff --git a/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/utils/Event.kt b/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/utils/Event.kt new file mode 100644 index 0000000..ac71031 --- /dev/null +++ b/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/utils/Event.kt @@ -0,0 +1,25 @@ +/** + * Used as a wrapper for data that is exposed via a LiveData that represents an event. + */ +open class Event(private val content: T) { + + var hasBeenHandled = false + private set // Allow external read but not write + + /** + * Returns the content and prevents its use again. + */ + fun getContentIfNotHandled(): T? { + return if (hasBeenHandled) { + null + } else { + hasBeenHandled = true + content + } + } + + /** + * Returns the content, even if it's already been handled. + */ + fun peekContent(): T = content +} diff --git a/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/utils/ViewModelFactory.kt b/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/utils/ViewModelFactory.kt new file mode 100644 index 0000000..1ccf37f --- /dev/null +++ b/app/src/main/java/ru/itis/karakurik/androidLab2/presentation/utils/ViewModelFactory.kt @@ -0,0 +1,26 @@ +package ru.itis.karakurik.androidLab2.presentation.utils + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import ru.itis.karakurik.androidLab2.domain.usecase.GetWeatherListUseCase +import ru.itis.karakurik.androidLab2.domain.usecase.GetWeatherUseCase +import ru.itis.karakurik.androidLab2.presentation.MainViewModel +import java.lang.IllegalArgumentException + +class ViewModelFactory( + private val getWeatherUseCase: GetWeatherUseCase, + private val getWeatherListUseCase: GetWeatherListUseCase +) : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + return when { + modelClass.isAssignableFrom(MainViewModel::class.java) -> + MainViewModel( + getWeatherUseCase, + getWeatherListUseCase + ) as? T ?: throw IllegalArgumentException("Unknown ViewModel class") + + else -> throw IllegalArgumentException("Unknown ViewModel class") + } + } +} diff --git a/app/src/main/res/layout/fragment_details.xml b/app/src/main/res/layout/fragment_details.xml index bb2c9c5..49038c8 100644 --- a/app/src/main/res/layout/fragment_details.xml +++ b/app/src/main/res/layout/fragment_details.xml @@ -1,239 +1,248 @@ - - - - - - - - + + + - - + + - - - - - - - + + + + - - - - - - - - - - + + - - - - - + android:gravity="top" + android:textColor="@color/black" + android:textSize="76sp" + tools:text="29.99°C"/> + android:orientation="horizontal"> - - - + + + + + + + + android:layout_weight="1" + android:orientation="vertical"> + + + + + - - - + android:orientation="horizontal"> - - - + + + + + + + + android:layout_weight="1" + android:orientation="vertical"> + + + + + + android:orientation="horizontal"> - - - + + + + + + + + android:layout_weight="1" + android:orientation="vertical"> + + + + + - - + diff --git a/app/src/main/res/layout/list_item_city.xml b/app/src/main/res/layout/list_item_city.xml index 8a2e615..f8a33f0 100644 --- a/app/src/main/res/layout/list_item_city.xml +++ b/app/src/main/res/layout/list_item_city.xml @@ -1,29 +1,37 @@ + app:cardCornerRadius="32dp"> + android:orientation="horizontal" + android:paddingHorizontal="20dp" + android:paddingVertical="10dp"> + + + android:textSize="32sp" + app:layout_constraintHorizontal_chainStyle="spread_inside" + tools:text="Kazan" /> + tools:text="10" + tools:textColor="#00ff00" /> diff --git a/app/src/main/res/navigation/nav_graph_main.xml b/app/src/main/res/navigation/nav_graph_main.xml index 7ad4887..5d4f3b8 100644 --- a/app/src/main/res/navigation/nav_graph_main.xml +++ b/app/src/main/res/navigation/nav_graph_main.xml @@ -13,7 +13,8 @@ + app:destination="@id/fragment_details"> + - + + diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index db0a143..c0e2204 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,6 +1,7 @@