From 5128868b83932b0c5b8f05180c2ac8ff5419892c Mon Sep 17 00:00:00 2001 From: Huawei Date: Thu, 19 Jun 2025 18:54:57 +0300 Subject: [PATCH 1/4] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=201=20=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BD=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 31 ++- app/src/main/AndroidManifest.xml | 1 + .../otus/basicarchitecture/AddressFragment.kt | 178 +++++++++++++++++ .../basicarchitecture/AddressViewModel.kt | 74 +++++++ .../java/ru/otus/basicarchitecture/App.kt | 29 +++ .../basicarchitecture/InterestsFragment.kt | 103 ++++++++++ .../basicarchitecture/InterestsViewModel.kt | 46 +++++ .../ru/otus/basicarchitecture/MainActivity.kt | 4 + .../ru/otus/basicarchitecture/NameFragment.kt | 184 ++++++++++++++++++ .../otus/basicarchitecture/NameViewModel.kt | 113 +++++++++++ .../otus/basicarchitecture/ResultFragment.kt | 78 ++++++++ .../otus/basicarchitecture/ResultViewModel.kt | 43 ++++ .../java/ru/otus/basicarchitecture/User.kt | 14 ++ app/src/main/res/drawable/border_black.xml | 7 + .../main/res/drawable/border_with_blue.xml | 7 + app/src/main/res/layout/activity_main.xml | 7 +- app/src/main/res/layout/fragment_address.xml | 95 +++++++++ .../main/res/layout/fragment_interests.xml | 31 +++ app/src/main/res/layout/fragment_name.xml | 96 +++++++++ app/src/main/res/layout/fragment_result.xml | 105 ++++++++++ app/src/main/res/values/colors.xml | 4 + app/src/main/res/values/strings.xml | 13 ++ build.gradle | 5 +- settings.gradle | 4 + 24 files changed, 1267 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/ru/otus/basicarchitecture/AddressFragment.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/AddressViewModel.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/App.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/InterestsFragment.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/InterestsViewModel.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/NameFragment.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/NameViewModel.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/ResultFragment.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/ResultViewModel.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/User.kt create mode 100644 app/src/main/res/drawable/border_black.xml create mode 100644 app/src/main/res/drawable/border_with_blue.xml create mode 100644 app/src/main/res/layout/fragment_address.xml create mode 100644 app/src/main/res/layout/fragment_interests.xml create mode 100644 app/src/main/res/layout/fragment_name.xml create mode 100644 app/src/main/res/layout/fragment_result.xml diff --git a/app/build.gradle b/app/build.gradle index e515992..a096e68 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,10 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' + id 'com.google.dagger.hilt.android' + id 'com.google.devtools.ksp' +// id("dagger.hilt.android.plugin") +// id 'kotlin-kapt' } android { @@ -30,14 +34,35 @@ android { kotlinOptions { jvmTarget = '17' } + buildFeatures{ + viewBinding = true + } + ksp { + arg("dagger.hilt.disableModulesHaveInstallInCheck", "true") + } } dependencies { - implementation 'androidx.core:core-ktx:1.15.0' - implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'androidx.core:core-ktx:1.16.0' + implementation 'androidx.appcompat:appcompat:1.7.1' implementation 'com.google.android.material:material:1.12.0' - implementation 'androidx.constraintlayout:constraintlayout:2.2.0' + implementation 'androidx.constraintlayout:constraintlayout:2.2.1' + + implementation 'androidx.activity:activity-ktx:1.10.1' + implementation 'androidx.fragment:fragment-ktx:1.8.8' + + implementation "com.google.dagger:hilt-android:2.56.2" + implementation 'androidx.navigation:navigation-fragment-ktx:2.8.6' + ksp "com.google.dagger:hilt-compiler:2.56.2" + + implementation("com.google.android.material:material:1.12.0") + +// implementation 'com.google.dagger:hilt-android:2.56.2' +// ksp 'com.google.dagger:hilt-compiler:2.56.2' +// implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0' +// ksp 'androidx.hilt:hilt-compiler:1.0.0' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.2.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ea17fa5..523ba83 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> (false) + val canContinue: LiveData + get() = _canContinue + + private var _errorEmptyCity = MutableLiveData() + val errorEmptyCity: LiveData + get() = _errorEmptyCity + + private var _errorEmptyCountry = MutableLiveData() + val errorEmptyCountry: LiveData + get() = _errorEmptyCountry + + private var _errorEmptyAddress = MutableLiveData() + val errorEmptyAddress: LiveData + get() = _errorEmptyAddress + + fun validateData() { + var successful = true + successful = checkEmptyFields() + if (successful == false){ + _canContinue.value = false + return + } + _canContinue.value = true + } + + fun setCountry(country: String) { + wizardCache.userAddress.country = country + } + + fun setCity(city: String) { + wizardCache.userAddress.city = city + } + + fun setAddress(address: String) { + wizardCache.userAddress.address = address + } + + private fun checkEmptyFields(): Boolean{ + var successful = true + if (wizardCache.userAddress.country == ""){ + _errorEmptyCountry.value = true + successful = false + } else{ + _errorEmptyCountry.value = false + } + if (wizardCache.userAddress.city == ""){ + _errorEmptyCity.value = true + successful = false + } else { + _errorEmptyCity.value = false + } + if (wizardCache.userAddress.address == ""){ + _errorEmptyAddress.value = true + successful = false + } else{ + _errorEmptyAddress.value = false + } + return successful + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/App.kt b/app/src/main/java/ru/otus/basicarchitecture/App.kt new file mode 100644 index 0000000..bad6873 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/App.kt @@ -0,0 +1,29 @@ +package ru.otus.basicarchitecture + +import android.app.Application +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.HiltAndroidApp +import dagger.hilt.components.SingletonComponent +import javax.inject.Inject +import javax.inject.Singleton + +@HiltAndroidApp +class WizardApp: Application() { +} + +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + @Provides + @Singleton + fun userInfo() = WizardCache() +} + +class WizardCache @Inject constructor(){ + var userName: UserName = UserName("", "", "") + var userAddress: UserAddress = UserAddress("", "", "") + var interests: List = listOf() +} + diff --git a/app/src/main/java/ru/otus/basicarchitecture/InterestsFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/InterestsFragment.kt new file mode 100644 index 0000000..c67397d --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/InterestsFragment.kt @@ -0,0 +1,103 @@ +package ru.otus.basicarchitecture + +import android.content.res.ColorStateList +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.content.ContextCompat +import androidx.fragment.app.viewModels +import com.google.android.material.chip.Chip +import dagger.hilt.android.AndroidEntryPoint +import ru.otus.basicarchitecture.databinding.FragmentInterestsBinding +import kotlin.getValue + +@AndroidEntryPoint +class InterestsFragment : Fragment() { + + private var _binding: FragmentInterestsBinding? = null + private val binding: FragmentInterestsBinding + get() = _binding ?: throw RuntimeException("FragmentInterestsBinding == null") + + private val viewModel: InterestsViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.loadListOfInterests() + observeViewModel() + binding.buttonNext.setOnClickListener { + viewModel.setInterests(getSelectedInterests()) + viewModel.checkInterests() + } + } + + private fun observeViewModel(){ + viewModel.listOfInterests.observe(viewLifecycleOwner) { + val selectedColor = ContextCompat.getColor(requireContext(), R.color.blue_light) + val defaultColor = ContextCompat.getColor(requireContext(), R.color.gray_light) + setupChips(it, selectedColor, defaultColor) + } + viewModel.canContinue.observe(viewLifecycleOwner) { + if (it){ + requireActivity().supportFragmentManager.beginTransaction() + .replace(R.id.mainContainer, ResultFragment.newInstance()) + .commit() + } else { + Toast.makeText(requireContext(), "It is necessary to mark at least one tag", Toast.LENGTH_SHORT).show() + } + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + _binding = FragmentInterestsBinding.inflate(inflater, container, false) + return binding.root + } + + private fun setupChips(interests: List, selectedColor: Int, defaultColor: Int) { + interests.forEach { interest -> + val chip = Chip(requireContext()).apply { + text = interest + isCheckable = true + isCheckedIconVisible = false + chipBackgroundColor = ColorStateList.valueOf(defaultColor) + + setOnCheckedChangeListener { _, isChecked -> + chipBackgroundColor = ColorStateList.valueOf( + if (isChecked) selectedColor else defaultColor + ) + } + } + + binding.chipGroupInterests.addView(chip) + } + } + + fun getSelectedInterests(): List { + val selected = mutableListOf() + for (i in 0 until binding.chipGroupInterests.childCount) { + val child = binding.chipGroupInterests.getChildAt(i) + if (child is Chip && child.isChecked) { + selected.add(child.text.toString()) + } + } + return selected + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + companion object { + fun newInstance() = InterestsFragment() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/InterestsViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/InterestsViewModel.kt new file mode 100644 index 0000000..55e66a4 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/InterestsViewModel.kt @@ -0,0 +1,46 @@ +package ru.otus.basicarchitecture + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class InterestsViewModel @Inject constructor( + private val wizardCache: WizardCache +): ViewModel() { + + private var _canContinue = MutableLiveData() + val canContinue: LiveData + get() = _canContinue + + private var _listOfInterests = MutableLiveData>() + val listOfInterests: LiveData> + get() = _listOfInterests + + private val interests = listOf( + "Reading", "Music", "Movies", "Travel", "Cooking", + "Sports", "Technology", "Photography", "Art", "Fashion", + "Fitness", "Gaming", "Science", "History", "Nature", + "Animals", "Books", "Programming", "Design", "Dance", + "Food", "Cars", "Space", "Education", "Politics", + "Health", "Business", "Finance", "Writing", "Comedy" + ) + + fun checkInterests(){ + if (wizardCache.interests.size <= 0){ + _canContinue.value = false + } else { + _canContinue.value = true + } + } + + fun setInterests(interests: List){ + wizardCache.interests = interests + } + + fun loadListOfInterests(){ + _listOfInterests.value = interests + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/MainActivity.kt b/app/src/main/java/ru/otus/basicarchitecture/MainActivity.kt index 623aba9..c58fa84 100644 --- a/app/src/main/java/ru/otus/basicarchitecture/MainActivity.kt +++ b/app/src/main/java/ru/otus/basicarchitecture/MainActivity.kt @@ -2,8 +2,12 @@ package ru.otus.basicarchitecture import androidx.appcompat.app.AppCompatActivity import android.os.Bundle +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) diff --git a/app/src/main/java/ru/otus/basicarchitecture/NameFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/NameFragment.kt new file mode 100644 index 0000000..960f73d --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/NameFragment.kt @@ -0,0 +1,184 @@ +package ru.otus.basicarchitecture + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import dagger.hilt.android.AndroidEntryPoint +import ru.otus.basicarchitecture.databinding.FragmentNameBinding + +@AndroidEntryPoint +class NameFragment : Fragment() { + + private var _binding: FragmentNameBinding? = null + private val binding: FragmentNameBinding + get() = _binding ?: throw RuntimeException("FragmentNameBinding == null") + + private val viewModel: NameViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + observeViewModel() + addTextChangedListeners() + binding.buttonNext.setOnClickListener { + viewModel.validateData() + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentNameBinding.inflate(inflater, container, false) + return binding.root + + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + fun observeViewModel(){ + viewModel.errorEmptyName.observe(viewLifecycleOwner) { + with(binding) { + if (it){ + editTextName.error = String.format(resources.getString(R.string.empty_field), + textInputName.hint.toString() + ) + } else { + editTextName.error = null + } + } + } + viewModel.errorEmptySurname.observe(viewLifecycleOwner) { + with(binding) { + if (it){ + editTextSurname.error = String.format(resources.getString(R.string.empty_field), + textInputSurname.hint.toString() + ) + } else { + editTextSurname.error = null + } + } + } + viewModel.errorEmptyBirthday.observe(viewLifecycleOwner) { + with(binding) { + if (it){ + editTextBirthday.error = String.format(resources.getString(R.string.empty_field), + textInputBirthday.hint.toString() + ) + } else { + editTextBirthday.error = null + } + } + } + viewModel.errorAge.observe(viewLifecycleOwner) { + with(binding) { + if (it){ + editTextBirthday.error = resources.getString(R.string.you_are_under_18) + } else { + editTextBirthday.error = null + } + } + + } + viewModel.errorBirthday.observe(viewLifecycleOwner) { + with(binding) { + if (it){ + editTextBirthday.error = resources.getString(R.string.incorrectly_birthday) + } else { + editTextBirthday.error = null + } + } + + } + viewModel.canContinue.observe(viewLifecycleOwner) { + if (it){ + requireActivity().supportFragmentManager.beginTransaction() + .replace(R.id.mainContainer, AddressFragment.newInstance()) + .commit() + } + } + } + + private fun addTextChangedListeners(){ + with(binding){ + editTextName.addTextChangedListener(object : TextWatcher{ + override fun beforeTextChanged( + s: CharSequence?, + start: Int, + count: Int, + after: Int + ) { + } + + override fun onTextChanged( + s: CharSequence?, + start: Int, + before: Int, + count: Int + ) { + viewModel.setName(editTextName.text.toString()) + } + + override fun afterTextChanged(s: Editable?) { + } + }) + editTextSurname.addTextChangedListener(object : TextWatcher{ + override fun beforeTextChanged( + s: CharSequence?, + start: Int, + count: Int, + after: Int + ) { + } + + override fun onTextChanged( + s: CharSequence?, + start: Int, + before: Int, + count: Int + ) { + viewModel.setSurname(editTextSurname.text.toString()) + } + + override fun afterTextChanged(s: Editable?) { + } + }) + editTextBirthday.addTextChangedListener(object : TextWatcher{ + override fun beforeTextChanged( + s: CharSequence?, + start: Int, + count: Int, + after: Int + ) { + } + + override fun onTextChanged( + s: CharSequence?, + start: Int, + before: Int, + count: Int + ) { + viewModel.setBirthday(editTextBirthday.text.toString()) + } + + override fun afterTextChanged(s: Editable?) { + } + }) + } + } + + companion object { + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/NameViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/NameViewModel.kt new file mode 100644 index 0000000..61f495f --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/NameViewModel.kt @@ -0,0 +1,113 @@ +package ru.otus.basicarchitecture + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale +import javax.inject.Inject + +@HiltViewModel +class NameViewModel @Inject constructor( + private val wizardCache: WizardCache +): ViewModel() { + + private var _canContinue = MutableLiveData(false) + val canContinue: LiveData + get() = _canContinue + + private var _errorAge = MutableLiveData() + val errorAge: LiveData + get() = _errorAge + + private var _errorEmptyName = MutableLiveData() + val errorEmptyName: LiveData + get() = _errorEmptyName + + private var _errorEmptySurname = MutableLiveData() + val errorEmptySurname: LiveData + get() = _errorEmptySurname + + private var _errorEmptyBirthday = MutableLiveData() + val errorEmptyBirthday: LiveData + get() = _errorEmptyBirthday + + private var _errorBirthday = MutableLiveData() + val errorBirthday: LiveData + get() = _errorBirthday + + fun validateData() { + var successful = true + successful = checkEmptyFields() + if (successful == false){ + _canContinue.value = false + return + } + successful = checkAge() + if (successful == false){ + _canContinue.value = false + return + } + _canContinue.value = true + } + + fun setName(name: String) { + wizardCache.userName.name = name + } + + fun setSurname(surname: String) { + wizardCache.userName.surname = surname + } + + fun setBirthday(birthday: String) { + wizardCache.userName.birthday = birthday + } + + private fun checkAge(): Boolean { + var successful = true + val sdf = SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()) + try { + val birthDate = sdf.parse(wizardCache.userName.birthday) + _errorBirthday.value = false + val today = Calendar.getInstance().time + val diff = today.time - birthDate.time + val years = (diff / (1000L * 60 * 60 *24 * 365)).toInt() + if (years < 18) { + _errorAge.value = true + successful = false + } else { + _errorAge.value = false + } + } catch (e: Exception) { + _errorBirthday.value = true + successful = false + } + return successful + } + + + private fun checkEmptyFields(): Boolean{ + var successful = true + if (wizardCache.userName.name == ""){ + _errorEmptyName.value = true + successful = false + } else{ + _errorEmptyName.value = false + } + if (wizardCache.userName.surname == ""){ + _errorEmptySurname.value = true + successful = false + } else { + _errorEmptySurname.value = false + } + if (wizardCache.userName.birthday == ""){ + _errorEmptyBirthday.value = true + successful = false + } else{ + _errorEmptyBirthday.value = false + } + return successful + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ResultFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/ResultFragment.kt new file mode 100644 index 0000000..63c4cd5 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ResultFragment.kt @@ -0,0 +1,78 @@ +package ru.otus.basicarchitecture + +import android.content.res.ColorStateList +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.fragment.app.viewModels +import com.google.android.material.chip.Chip +import dagger.hilt.android.AndroidEntryPoint +import ru.otus.basicarchitecture.databinding.FragmentResultBinding +import kotlin.getValue + +@AndroidEntryPoint +class ResultFragment : Fragment() { + + private var _binding: FragmentResultBinding? = null + private val binding: FragmentResultBinding + get() = _binding ?: throw RuntimeException("FragmentResultBinding == null") + + private val viewModel: ResultViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + observeViewModel() + viewModel.loadUserInfo() + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + _binding = FragmentResultBinding.inflate(inflater, container, false) + return binding.root + } + + private fun observeViewModel(){ + viewModel.userName.observe(viewLifecycleOwner) { + with(binding){ + textViewName.text = it.name + textViewSurname.text = it.surname + textViewBirthday.text = it.birthday + } + } + viewModel.userAddress.observe(viewLifecycleOwner) { + with(binding){ + textViewAddress.text = String.format( + resources.getString(R.string.address_mask), + it.country, + it.city, + it.address + ) + } + } + viewModel.interests.observe(viewLifecycleOwner) { + val defaultColor = ContextCompat.getColor(requireContext(), R.color.gray_light) + with(binding){ + it.forEach { interest -> + val chip = Chip(requireContext()).apply { + text = interest + chipBackgroundColor = ColorStateList.valueOf(defaultColor) + } + binding.chipGroupInterests.addView(chip) + } + } + } + } + + companion object { + fun newInstance() = ResultFragment() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ResultViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/ResultViewModel.kt new file mode 100644 index 0000000..1d29e2b --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ResultViewModel.kt @@ -0,0 +1,43 @@ +package ru.otus.basicarchitecture + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class ResultViewModel @Inject constructor( + private val wizardCache: WizardCache +): ViewModel() { + + private var _userName = MutableLiveData() + val userName: LiveData + get() = _userName + + private var _userAddress = MutableLiveData() + val userAddress: LiveData + get() = _userAddress + + private var _interests = MutableLiveData>() + val interests: LiveData> + get() = _interests + + fun loadUserInfo(){ + loadUserName() + loadUserAddress() + loadInterests() + } + + private fun loadUserName(){ + _userName.value = wizardCache.userName + } + + private fun loadUserAddress(){ + _userAddress.value = wizardCache.userAddress + } + + private fun loadInterests(){ + _interests.value = wizardCache.interests + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/User.kt b/app/src/main/java/ru/otus/basicarchitecture/User.kt new file mode 100644 index 0000000..7a71587 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/User.kt @@ -0,0 +1,14 @@ +package ru.otus.basicarchitecture + + +data class UserName( + var name: String, + var surname: String, + var birthday: String +) + +data class UserAddress( + var country: String, + var city: String, + var address: String +) \ No newline at end of file diff --git a/app/src/main/res/drawable/border_black.xml b/app/src/main/res/drawable/border_black.xml new file mode 100644 index 0000000..e9ea1f2 --- /dev/null +++ b/app/src/main/res/drawable/border_black.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/border_with_blue.xml b/app/src/main/res/drawable/border_with_blue.xml new file mode 100644 index 0000000..029a1c6 --- /dev/null +++ b/app/src/main/res/drawable/border_with_blue.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 0b15a20..998ce2a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,9 +1,14 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_address.xml b/app/src/main/res/layout/fragment_address.xml new file mode 100644 index 0000000..b09fe12 --- /dev/null +++ b/app/src/main/res/layout/fragment_address.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_interests.xml b/app/src/main/res/layout/fragment_interests.xml new file mode 100644 index 0000000..db76393 --- /dev/null +++ b/app/src/main/res/layout/fragment_interests.xml @@ -0,0 +1,31 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_name.xml b/app/src/main/res/layout/fragment_name.xml new file mode 100644 index 0000000..efc79ec --- /dev/null +++ b/app/src/main/res/layout/fragment_name.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml new file mode 100644 index 0000000..2f84324 --- /dev/null +++ b/app/src/main/res/layout/fragment_result.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f8c6127..63e117f 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -7,4 +7,8 @@ #FF018786 #FF000000 #FFFFFFFF + #0F6DD1 + #aa0F6DD1 + #383838 + #DEDEDE \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f26b6d3..ed753c7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,16 @@ BasicArchitecture + %s, %s, %s + Сountry + City + Address + Next + Name + Surname + Birthday + Date of birth + Interests + The birthday is entered incorrectly + You are under 18 + The field \"%s\" is empty \ No newline at end of file diff --git a/build.gradle b/build.gradle index 7b166ff..2f2e3a8 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,9 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. + plugins { id 'com.android.application' version '8.7.3' apply false + id 'org.jetbrains.kotlin.android' version '2.0.0' apply false id 'com.android.library' version '8.7.3' apply false - id 'org.jetbrains.kotlin.android' version '2.0.21' apply false + id 'com.google.dagger.hilt.android' version '2.56.2' apply false + id'com.google.devtools.ksp' version '2.0.0-1.0.23' apply false } \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 9717fd6..7a66128 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,12 +4,16 @@ pluginManagement { mavenCentral() gradlePluginPortal() } + plugins { + id("androidx.hilt.lifecycle-viewmodel") version "1.0.0" apply false + } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() + gradlePluginPortal() } } rootProject.name = "BasicArchitecture" From 58c59ff972ae0ae4257e5f369cb48db421af1c26 Mon Sep 17 00:00:00 2001 From: Huawei Date: Sun, 22 Jun 2025 19:54:11 +0300 Subject: [PATCH 2/4] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=202=20=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BD=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 18 +- app/src/main/AndroidManifest.xml | 6 +- .../otus/basicarchitecture/AddressFragment.kt | 178 ------------------ .../basicarchitecture/AddressViewModel.kt | 74 -------- .../java/ru/otus/basicarchitecture/App.kt | 29 --- .../data/AddressApiService.kt | 19 ++ .../data/AddressRepositoryImpl.kt | 31 +++ .../data/dto/AddressDataDto.kt | 10 + .../data/dto/AddressRequestDto.kt | 5 + .../data/dto/AddressResponseDto.kt | 5 + .../data/dto/AddressSuggestionDto.kt | 7 + .../data/mapper/AddressMapper.kt | 17 ++ .../ru/otus/basicarchitecture/di/AppModule.kt | 42 +++++ .../domain/AddressRepository.kt | 5 + .../domain/AddressSuggestUseCase.kt | 8 + .../basicarchitecture/{ => domain}/User.kt | 7 +- .../presentation/AddressFragment.kt | 155 +++++++++++++++ .../presentation/AddressViewModel.kt | 76 ++++++++ .../{ => presentation}/InterestsFragment.kt | 8 +- .../{ => presentation}/InterestsViewModel.kt | 2 +- .../{ => presentation}/MainActivity.kt | 5 +- .../{ => presentation}/NameFragment.kt | 15 +- .../{ => presentation}/NameViewModel.kt | 2 +- .../{ => presentation}/ResultFragment.kt | 22 ++- .../{ => presentation}/ResultViewModel.kt | 4 +- .../presentation/WizardApp.kt | 8 + .../presentation/WizardCache.kt | 11 ++ app/src/main/res/layout/activity_main.xml | 4 +- app/src/main/res/layout/fragment_address.xml | 75 ++------ .../main/res/layout/fragment_interests.xml | 2 +- app/src/main/res/layout/fragment_name.xml | 2 +- app/src/main/res/layout/fragment_result.xml | 2 +- app/src/main/res/values/strings.xml | 2 + build.gradle | 5 + 34 files changed, 480 insertions(+), 381 deletions(-) delete mode 100644 app/src/main/java/ru/otus/basicarchitecture/AddressFragment.kt delete mode 100644 app/src/main/java/ru/otus/basicarchitecture/AddressViewModel.kt delete mode 100644 app/src/main/java/ru/otus/basicarchitecture/App.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/data/AddressApiService.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/data/AddressRepositoryImpl.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/data/dto/AddressDataDto.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/data/dto/AddressRequestDto.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/data/dto/AddressResponseDto.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/data/dto/AddressSuggestionDto.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/data/mapper/AddressMapper.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/di/AppModule.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/domain/AddressRepository.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/domain/AddressSuggestUseCase.kt rename app/src/main/java/ru/otus/basicarchitecture/{ => domain}/User.kt (55%) create mode 100644 app/src/main/java/ru/otus/basicarchitecture/presentation/AddressFragment.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/presentation/AddressViewModel.kt rename app/src/main/java/ru/otus/basicarchitecture/{ => presentation}/InterestsFragment.kt (97%) rename app/src/main/java/ru/otus/basicarchitecture/{ => presentation}/InterestsViewModel.kt (96%) rename app/src/main/java/ru/otus/basicarchitecture/{ => presentation}/MainActivity.kt (80%) rename app/src/main/java/ru/otus/basicarchitecture/{ => presentation}/NameFragment.kt (97%) rename app/src/main/java/ru/otus/basicarchitecture/{ => presentation}/NameViewModel.kt (98%) rename app/src/main/java/ru/otus/basicarchitecture/{ => presentation}/ResultFragment.kt (84%) rename app/src/main/java/ru/otus/basicarchitecture/{ => presentation}/ResultViewModel.kt (87%) create mode 100644 app/src/main/java/ru/otus/basicarchitecture/presentation/WizardApp.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/presentation/WizardCache.kt diff --git a/app/build.gradle b/app/build.gradle index a096e68..1d17bdc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,8 +3,7 @@ plugins { id 'org.jetbrains.kotlin.android' id 'com.google.dagger.hilt.android' id 'com.google.devtools.ksp' -// id("dagger.hilt.android.plugin") -// id 'kotlin-kapt' + id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' } android { @@ -36,6 +35,7 @@ android { } buildFeatures{ viewBinding = true + buildConfig = true } ksp { arg("dagger.hilt.disableModulesHaveInstallInCheck", "true") @@ -58,10 +58,16 @@ dependencies { implementation("com.google.android.material:material:1.12.0") -// implementation 'com.google.dagger:hilt-android:2.56.2' -// ksp 'com.google.dagger:hilt-compiler:2.56.2' -// implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0' -// ksp 'androidx.hilt:hilt-compiler:1.0.0' + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.2") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7") + + implementation("com.google.code.gson:gson:2.8.5") + + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.2.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 523ba83..92ed7f8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,8 +2,10 @@ + + diff --git a/app/src/main/java/ru/otus/basicarchitecture/AddressFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/AddressFragment.kt deleted file mode 100644 index cfa4997..0000000 --- a/app/src/main/java/ru/otus/basicarchitecture/AddressFragment.kt +++ /dev/null @@ -1,178 +0,0 @@ -package ru.otus.basicarchitecture - -import android.os.Bundle -import android.text.Editable -import android.text.TextWatcher -import androidx.fragment.app.Fragment -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.viewModels -import dagger.hilt.android.AndroidEntryPoint -import ru.otus.basicarchitecture.databinding.FragmentAddressBinding -import kotlin.getValue - -@AndroidEntryPoint -class AddressFragment : Fragment() { - - private var _binding: FragmentAddressBinding? = null - private val binding: FragmentAddressBinding - get() = _binding ?: throw RuntimeException("FragmentAddressBinding == null") - - private val viewModel: AddressViewModel by viewModels() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) -// arguments?.let { -// userName = it.getParcelable(EXTRA_USER_NAME, UserName::class.java) as UserName -// } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - observeViewModel() - addTextChangedListeners() - binding.buttonNext.setOnClickListener { - viewModel.validateData() - } - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - _binding = FragmentAddressBinding.inflate(inflater, container, false) - return binding.root - } - - fun observeViewModel(){ - viewModel.errorEmptyCountry.observe(viewLifecycleOwner) { - with(binding) { - if (it){ - editTextCountry.error = String.format( - resources.getString(R.string.empty_field), - textInputCountry.hint.toString() - ) - } else { - editTextCountry.error = null - } - } - - } - viewModel.errorEmptyCity.observe(viewLifecycleOwner) { - with(binding) { - if (it){ - editTextCity.error = String.format(resources.getString(R.string.empty_field), - textInputCity.hint.toString() - ) - } else { - editTextCity.error = null - } - } - - } - viewModel.errorEmptyAddress.observe(viewLifecycleOwner) { - with(binding) { - if (it){ - editTextAddress.error = String.format(resources.getString(R.string.empty_field), - textInputAddress.hint.toString() - ) - } else { - editTextAddress.error = null - } - } - } - viewModel.canContinue.observe(viewLifecycleOwner) { - if (it){ - requireActivity().supportFragmentManager.beginTransaction() - .replace(R.id.mainContainer, InterestsFragment.newInstance()) - .commit() - } - } - } - - private fun addTextChangedListeners(){ - with(binding){ - editTextCountry.addTextChangedListener(object : TextWatcher{ - override fun beforeTextChanged( - s: CharSequence?, - start: Int, - count: Int, - after: Int - ) { - } - - override fun onTextChanged( - s: CharSequence?, - start: Int, - before: Int, - count: Int - ) { - viewModel.setCountry(editTextCountry.text.toString()) - } - - override fun afterTextChanged(s: Editable?) { - } - }) - editTextCity.addTextChangedListener(object : TextWatcher{ - override fun beforeTextChanged( - s: CharSequence?, - start: Int, - count: Int, - after: Int - ) { - } - - override fun onTextChanged( - s: CharSequence?, - start: Int, - before: Int, - count: Int - ) { - viewModel.setCity(editTextCity.text.toString()) - } - - override fun afterTextChanged(s: Editable?) { - } - }) - editTextAddress.addTextChangedListener(object : TextWatcher{ - override fun beforeTextChanged( - s: CharSequence?, - start: Int, - count: Int, - after: Int - ) { - } - - override fun onTextChanged( - s: CharSequence?, - start: Int, - before: Int, - count: Int - ) { - viewModel.setAddress(editTextAddress.text.toString()) - } - - override fun afterTextChanged(s: Editable?) { - } - }) - } - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - companion object { - - private const val EXTRA_USER_NAME = "user_name" - - fun newInstance() = AddressFragment() -// AddressFragment().apply { -// arguments = Bundle().apply { -// putParcelable(EXTRA_USER_NAME, userName) -// } -// } - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/AddressViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/AddressViewModel.kt deleted file mode 100644 index e53b33b..0000000 --- a/app/src/main/java/ru/otus/basicarchitecture/AddressViewModel.kt +++ /dev/null @@ -1,74 +0,0 @@ -package ru.otus.basicarchitecture - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject - -@HiltViewModel -class AddressViewModel @Inject constructor( - private val wizardCache: WizardCache -): ViewModel() { - - private var _canContinue = MutableLiveData(false) - val canContinue: LiveData - get() = _canContinue - - private var _errorEmptyCity = MutableLiveData() - val errorEmptyCity: LiveData - get() = _errorEmptyCity - - private var _errorEmptyCountry = MutableLiveData() - val errorEmptyCountry: LiveData - get() = _errorEmptyCountry - - private var _errorEmptyAddress = MutableLiveData() - val errorEmptyAddress: LiveData - get() = _errorEmptyAddress - - fun validateData() { - var successful = true - successful = checkEmptyFields() - if (successful == false){ - _canContinue.value = false - return - } - _canContinue.value = true - } - - fun setCountry(country: String) { - wizardCache.userAddress.country = country - } - - fun setCity(city: String) { - wizardCache.userAddress.city = city - } - - fun setAddress(address: String) { - wizardCache.userAddress.address = address - } - - private fun checkEmptyFields(): Boolean{ - var successful = true - if (wizardCache.userAddress.country == ""){ - _errorEmptyCountry.value = true - successful = false - } else{ - _errorEmptyCountry.value = false - } - if (wizardCache.userAddress.city == ""){ - _errorEmptyCity.value = true - successful = false - } else { - _errorEmptyCity.value = false - } - if (wizardCache.userAddress.address == ""){ - _errorEmptyAddress.value = true - successful = false - } else{ - _errorEmptyAddress.value = false - } - return successful - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/App.kt b/app/src/main/java/ru/otus/basicarchitecture/App.kt deleted file mode 100644 index bad6873..0000000 --- a/app/src/main/java/ru/otus/basicarchitecture/App.kt +++ /dev/null @@ -1,29 +0,0 @@ -package ru.otus.basicarchitecture - -import android.app.Application -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.HiltAndroidApp -import dagger.hilt.components.SingletonComponent -import javax.inject.Inject -import javax.inject.Singleton - -@HiltAndroidApp -class WizardApp: Application() { -} - -@Module -@InstallIn(SingletonComponent::class) -object AppModule { - @Provides - @Singleton - fun userInfo() = WizardCache() -} - -class WizardCache @Inject constructor(){ - var userName: UserName = UserName("", "", "") - var userAddress: UserAddress = UserAddress("", "", "") - var interests: List = listOf() -} - diff --git a/app/src/main/java/ru/otus/basicarchitecture/data/AddressApiService.kt b/app/src/main/java/ru/otus/basicarchitecture/data/AddressApiService.kt new file mode 100644 index 0000000..529f913 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/data/AddressApiService.kt @@ -0,0 +1,19 @@ +package ru.otus.basicarchitecture.data + +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.Header +import retrofit2.http.Headers +import retrofit2.http.POST +import ru.otus.basicarchitecture.data.dto.AddressRequestDto +import ru.otus.basicarchitecture.data.dto.AddressResponseDto + +interface AddressApiService { + + @Headers("Content-Type: application/json") + @POST("suggest/address") + suspend fun suggestAddress( + @Header("Authorization") token: String, + @Body request: AddressRequestDto, + ): Response +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/data/AddressRepositoryImpl.kt b/app/src/main/java/ru/otus/basicarchitecture/data/AddressRepositoryImpl.kt new file mode 100644 index 0000000..43aa2b1 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/data/AddressRepositoryImpl.kt @@ -0,0 +1,31 @@ +package ru.otus.basicarchitecture.data + +import android.util.Log +import ru.otus.basicarchitecture.BuildConfig +import ru.otus.basicarchitecture.data.mapper.AddressMapper +import ru.otus.basicarchitecture.data.dto.AddressRequestDto +import ru.otus.basicarchitecture.domain.AddressRepository +import ru.otus.basicarchitecture.domain.UserAddress +import javax.inject.Inject + +class AddressRepositoryImpl @Inject constructor( + private val addressApiService: AddressApiService +) : AddressRepository { + + override suspend fun suggestAddress(query: String): List { + val token = "Token ${BuildConfig.dadata_api_key}" + val response = addressApiService.suggestAddress(token, AddressRequestDto(query)) + if (!response.isSuccessful){ + val errorResponse = response.errorBody()?.string() + Log.e("API Error", "Error response: $errorResponse") + } + val listAddressDataDto = response.body()?.suggestions?.map { suggestion -> + suggestion.data.copy(fullAddress = suggestion.unrestricted_value) + } + val mapper = AddressMapper() + val listUserAddress = listAddressDataDto?.map { + mapper.mapDtoToEntity(it) + } + return listUserAddress ?: listOf() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/data/dto/AddressDataDto.kt b/app/src/main/java/ru/otus/basicarchitecture/data/dto/AddressDataDto.kt new file mode 100644 index 0000000..f963dac --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/data/dto/AddressDataDto.kt @@ -0,0 +1,10 @@ +package ru.otus.basicarchitecture.data.dto + +data class AddressDataDto( + val country: String?, + val city: String?, + val street: String?, + val house: String?, + val block: String?, + val fullAddress: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/data/dto/AddressRequestDto.kt b/app/src/main/java/ru/otus/basicarchitecture/data/dto/AddressRequestDto.kt new file mode 100644 index 0000000..880bdd0 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/data/dto/AddressRequestDto.kt @@ -0,0 +1,5 @@ +package ru.otus.basicarchitecture.data.dto + +data class AddressRequestDto( + val query: String +) \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/data/dto/AddressResponseDto.kt b/app/src/main/java/ru/otus/basicarchitecture/data/dto/AddressResponseDto.kt new file mode 100644 index 0000000..fbda98f --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/data/dto/AddressResponseDto.kt @@ -0,0 +1,5 @@ +package ru.otus.basicarchitecture.data.dto + +data class AddressResponseDto( + val suggestions: List +) \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/data/dto/AddressSuggestionDto.kt b/app/src/main/java/ru/otus/basicarchitecture/data/dto/AddressSuggestionDto.kt new file mode 100644 index 0000000..aac4795 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/data/dto/AddressSuggestionDto.kt @@ -0,0 +1,7 @@ +package ru.otus.basicarchitecture.data.dto + +data class AddressSuggestionDto( + val value: String, + val unrestricted_value: String, + val data: AddressDataDto +) \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/data/mapper/AddressMapper.kt b/app/src/main/java/ru/otus/basicarchitecture/data/mapper/AddressMapper.kt new file mode 100644 index 0000000..c2914d8 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/data/mapper/AddressMapper.kt @@ -0,0 +1,17 @@ +package ru.otus.basicarchitecture.data.mapper + +import ru.otus.basicarchitecture.data.dto.AddressDataDto +import ru.otus.basicarchitecture.domain.UserAddress + +class AddressMapper { + + fun mapDtoToEntity(dto: AddressDataDto) = UserAddress( + fullAddress = dto.fullAddress ?: "", + country = dto.country ?: "", + city = dto.city ?: "", + street = dto.street ?: "", + house = dto.house ?: "", + block = dto.block ?: "" + ) + +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/di/AppModule.kt b/app/src/main/java/ru/otus/basicarchitecture/di/AppModule.kt new file mode 100644 index 0000000..47b9a90 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/di/AppModule.kt @@ -0,0 +1,42 @@ +package ru.otus.basicarchitecture.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import ru.otus.basicarchitecture.data.AddressApiService +import ru.otus.basicarchitecture.data.AddressRepositoryImpl +import ru.otus.basicarchitecture.domain.AddressRepository +import ru.otus.basicarchitecture.presentation.WizardCache +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + @Provides + @Singleton + fun userInfo() = WizardCache() + + @Provides + @Singleton + fun provideRetrofit(): Retrofit { + return Retrofit.Builder() + .baseUrl("https://suggestions.dadata.ru/suggestions/api/4_1/rs/") + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + + @Provides + @Singleton + fun provideDaDataService(retrofit: Retrofit): AddressApiService { + return retrofit.create(AddressApiService::class.java) + } + + @Provides + @Singleton + fun provideAddressRepository(impl: AddressRepositoryImpl): AddressRepository { + return impl + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/domain/AddressRepository.kt b/app/src/main/java/ru/otus/basicarchitecture/domain/AddressRepository.kt new file mode 100644 index 0000000..941b05e --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/domain/AddressRepository.kt @@ -0,0 +1,5 @@ +package ru.otus.basicarchitecture.domain + +interface AddressRepository { + suspend fun suggestAddress(query: String): List +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/domain/AddressSuggestUseCase.kt b/app/src/main/java/ru/otus/basicarchitecture/domain/AddressSuggestUseCase.kt new file mode 100644 index 0000000..55f8a7a --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/domain/AddressSuggestUseCase.kt @@ -0,0 +1,8 @@ +package ru.otus.basicarchitecture.domain + +class AddressSuggestUseCase(private val addressRepository: AddressRepository) { + + suspend operator fun invoke(query: String): List { + return addressRepository.suggestAddress(query) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/User.kt b/app/src/main/java/ru/otus/basicarchitecture/domain/User.kt similarity index 55% rename from app/src/main/java/ru/otus/basicarchitecture/User.kt rename to app/src/main/java/ru/otus/basicarchitecture/domain/User.kt index 7a71587..3f7ecaa 100644 --- a/app/src/main/java/ru/otus/basicarchitecture/User.kt +++ b/app/src/main/java/ru/otus/basicarchitecture/domain/User.kt @@ -1,4 +1,4 @@ -package ru.otus.basicarchitecture +package ru.otus.basicarchitecture.domain data class UserName( @@ -10,5 +10,8 @@ data class UserName( data class UserAddress( var country: String, var city: String, - var address: String + var street: String, + var house: String, + var block: String, + var fullAddress: String ) \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/presentation/AddressFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/presentation/AddressFragment.kt new file mode 100644 index 0000000..046f414 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/presentation/AddressFragment.kt @@ -0,0 +1,155 @@ +package ru.otus.basicarchitecture.presentation + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import dagger.hilt.android.AndroidEntryPoint +import ru.otus.basicarchitecture.R +import ru.otus.basicarchitecture.databinding.FragmentAddressBinding +import ru.otus.basicarchitecture.domain.UserAddress + +@AndroidEntryPoint +class AddressFragment : Fragment() { + + private var _binding: FragmentAddressBinding? = null + private val binding: FragmentAddressBinding + get() = _binding ?: throw RuntimeException("FragmentAddressBinding == null") + + private val viewModel: AddressViewModel by viewModels() + + private val adapter by lazy { + ArrayAdapter( + requireContext(), + android.R.layout.simple_dropdown_item_1line, + mutableListOf() + ) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.addressInput.setAdapter(adapter) + observeViewModel() + addTextChangedListeners() + binding.buttonNext.setOnClickListener { + viewModel.validateData() + } + binding.addressInput.setOnItemClickListener { _, _, position, _ -> + val selectedItem = binding.addressInput.adapter.getItem(position) as? UserAddress + ?: return@setOnItemClickListener + selectedItem.let { + val address = listOf( + it.country, + it.city, + it.street, + it.house, + it.block + ) + .filter { !it.isBlank() } + .joinToString(", ") + binding.addressInput.setText(address) + } + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + _binding = FragmentAddressBinding.inflate(inflater, container, false) + return binding.root + } + + fun observeViewModel(){ + viewModel.errorNetwork.observe(viewLifecycleOwner) { + if (it) { + Toast.makeText( + requireContext(), + getString(R.string.error_network), + Toast.LENGTH_SHORT + ).show() + } + } + viewModel.errorEmptyAddress.observe(viewLifecycleOwner) { + with(binding) { + if (it){ + addressInput.error = String.format(resources.getString(R.string.empty_field), + addressInput.hint.toString() + ) + } else { + addressInput.error = null + } + } + } + viewModel.listUserAddress.observe(viewLifecycleOwner) { listUserAddress -> + adapter.clear() + adapter.addAll(listUserAddress.map { + listOf( + it.country, + it.city, + it.street, + it.house, + it.block + ) + .filter { !it.isBlank() } + .joinToString(", ") + }) + } + viewModel.canContinue.observe(viewLifecycleOwner) { + if (it){ + requireActivity().supportFragmentManager.beginTransaction() + .replace(R.id.mainContainer, InterestsFragment.newInstance()) + .commit() + } + } + } + + private fun addTextChangedListeners(){ + with(binding){ + addressInput.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged( + s: CharSequence?, + start: Int, + count: Int, + after: Int + ) { + } + + override fun onTextChanged( + s: CharSequence?, + start: Int, + before: Int, + count: Int + ) { + val address = addressInput.text.toString() + viewModel.setAddress(address) + viewModel.searchAddress(address) + } + + override fun afterTextChanged(s: Editable?) { + } + }) + } + } + + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + companion object { + + fun newInstance() = AddressFragment() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/presentation/AddressViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/presentation/AddressViewModel.kt new file mode 100644 index 0000000..da6cf1c --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/presentation/AddressViewModel.kt @@ -0,0 +1,76 @@ +package ru.otus.basicarchitecture.presentation + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import ru.otus.basicarchitecture.domain.AddressRepository +import ru.otus.basicarchitecture.domain.AddressSuggestUseCase +import ru.otus.basicarchitecture.domain.UserAddress +import javax.inject.Inject + +@HiltViewModel +class AddressViewModel @Inject constructor( + private val wizardCache: WizardCache, + private val addressRepository: AddressRepository +): ViewModel() { + + private val addressSuggestUseCase = AddressSuggestUseCase(addressRepository) + + private var _canContinue = MutableLiveData(false) + val canContinue: LiveData + get() = _canContinue + + private var _listUserAddress = MutableLiveData>() + val listUserAddress: LiveData> + get() = _listUserAddress + + private var _errorNetwork = MutableLiveData() + val errorNetwork: LiveData + get() = _errorNetwork + + private var _errorEmptyAddress = MutableLiveData() + val errorEmptyAddress: LiveData + get() = _errorEmptyAddress + + fun validateData() { + var successful = true + successful = checkEmptyFields() + if (successful == false){ + _canContinue.value = false + return + } + _canContinue.value = true + } + + fun setAddress(fullAddress: String) { + wizardCache.userAddress.fullAddress = fullAddress + } + + fun searchAddress(query: String) { + viewModelScope.launch { + try { + val result = addressSuggestUseCase.invoke(query) + _listUserAddress.postValue(result) + Log.d("AddressViewModel", result.toString()) + } catch (e: Exception) { + _errorNetwork.postValue(true) + } + } + } + + + private fun checkEmptyFields(): Boolean{ + var successful = true + if (wizardCache.userAddress.fullAddress == ""){ + _errorEmptyAddress.value = true + successful = false + } else{ + _errorEmptyAddress.value = false + } + return successful + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/InterestsFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/presentation/InterestsFragment.kt similarity index 97% rename from app/src/main/java/ru/otus/basicarchitecture/InterestsFragment.kt rename to app/src/main/java/ru/otus/basicarchitecture/presentation/InterestsFragment.kt index c67397d..d96ecd5 100644 --- a/app/src/main/java/ru/otus/basicarchitecture/InterestsFragment.kt +++ b/app/src/main/java/ru/otus/basicarchitecture/presentation/InterestsFragment.kt @@ -1,18 +1,18 @@ -package ru.otus.basicarchitecture +package ru.otus.basicarchitecture.presentation import android.content.res.ColorStateList import android.os.Bundle -import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import com.google.android.material.chip.Chip import dagger.hilt.android.AndroidEntryPoint +import ru.otus.basicarchitecture.R import ru.otus.basicarchitecture.databinding.FragmentInterestsBinding -import kotlin.getValue @AndroidEntryPoint class InterestsFragment : Fragment() { @@ -46,7 +46,7 @@ class InterestsFragment : Fragment() { viewModel.canContinue.observe(viewLifecycleOwner) { if (it){ requireActivity().supportFragmentManager.beginTransaction() - .replace(R.id.mainContainer, ResultFragment.newInstance()) + .replace(R.id.mainContainer, ResultFragment.Companion.newInstance()) .commit() } else { Toast.makeText(requireContext(), "It is necessary to mark at least one tag", Toast.LENGTH_SHORT).show() diff --git a/app/src/main/java/ru/otus/basicarchitecture/InterestsViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/presentation/InterestsViewModel.kt similarity index 96% rename from app/src/main/java/ru/otus/basicarchitecture/InterestsViewModel.kt rename to app/src/main/java/ru/otus/basicarchitecture/presentation/InterestsViewModel.kt index 55e66a4..e4ea7ef 100644 --- a/app/src/main/java/ru/otus/basicarchitecture/InterestsViewModel.kt +++ b/app/src/main/java/ru/otus/basicarchitecture/presentation/InterestsViewModel.kt @@ -1,4 +1,4 @@ -package ru.otus.basicarchitecture +package ru.otus.basicarchitecture.presentation import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData diff --git a/app/src/main/java/ru/otus/basicarchitecture/MainActivity.kt b/app/src/main/java/ru/otus/basicarchitecture/presentation/MainActivity.kt similarity index 80% rename from app/src/main/java/ru/otus/basicarchitecture/MainActivity.kt rename to app/src/main/java/ru/otus/basicarchitecture/presentation/MainActivity.kt index c58fa84..0589c00 100644 --- a/app/src/main/java/ru/otus/basicarchitecture/MainActivity.kt +++ b/app/src/main/java/ru/otus/basicarchitecture/presentation/MainActivity.kt @@ -1,8 +1,9 @@ -package ru.otus.basicarchitecture +package ru.otus.basicarchitecture.presentation -import androidx.appcompat.app.AppCompatActivity import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity import dagger.hilt.android.AndroidEntryPoint +import ru.otus.basicarchitecture.R @AndroidEntryPoint class MainActivity : AppCompatActivity() { diff --git a/app/src/main/java/ru/otus/basicarchitecture/NameFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/presentation/NameFragment.kt similarity index 97% rename from app/src/main/java/ru/otus/basicarchitecture/NameFragment.kt rename to app/src/main/java/ru/otus/basicarchitecture/presentation/NameFragment.kt index 960f73d..294609a 100644 --- a/app/src/main/java/ru/otus/basicarchitecture/NameFragment.kt +++ b/app/src/main/java/ru/otus/basicarchitecture/presentation/NameFragment.kt @@ -1,19 +1,20 @@ -package ru.otus.basicarchitecture +package ru.otus.basicarchitecture.presentation import android.os.Bundle import android.text.Editable import android.text.TextWatcher -import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import dagger.hilt.android.AndroidEntryPoint +import ru.otus.basicarchitecture.R import ru.otus.basicarchitecture.databinding.FragmentNameBinding @AndroidEntryPoint class NameFragment : Fragment() { - + private var _binding: FragmentNameBinding? = null private val binding: FragmentNameBinding get() = _binding ?: throw RuntimeException("FragmentNameBinding == null") @@ -105,7 +106,7 @@ class NameFragment : Fragment() { viewModel.canContinue.observe(viewLifecycleOwner) { if (it){ requireActivity().supportFragmentManager.beginTransaction() - .replace(R.id.mainContainer, AddressFragment.newInstance()) + .replace(R.id.mainContainer, AddressFragment.Companion.newInstance()) .commit() } } @@ -113,7 +114,7 @@ class NameFragment : Fragment() { private fun addTextChangedListeners(){ with(binding){ - editTextName.addTextChangedListener(object : TextWatcher{ + editTextName.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged( s: CharSequence?, start: Int, @@ -134,7 +135,7 @@ class NameFragment : Fragment() { override fun afterTextChanged(s: Editable?) { } }) - editTextSurname.addTextChangedListener(object : TextWatcher{ + editTextSurname.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged( s: CharSequence?, start: Int, @@ -155,7 +156,7 @@ class NameFragment : Fragment() { override fun afterTextChanged(s: Editable?) { } }) - editTextBirthday.addTextChangedListener(object : TextWatcher{ + editTextBirthday.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged( s: CharSequence?, start: Int, diff --git a/app/src/main/java/ru/otus/basicarchitecture/NameViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/presentation/NameViewModel.kt similarity index 98% rename from app/src/main/java/ru/otus/basicarchitecture/NameViewModel.kt rename to app/src/main/java/ru/otus/basicarchitecture/presentation/NameViewModel.kt index 61f495f..8b18b44 100644 --- a/app/src/main/java/ru/otus/basicarchitecture/NameViewModel.kt +++ b/app/src/main/java/ru/otus/basicarchitecture/presentation/NameViewModel.kt @@ -1,4 +1,4 @@ -package ru.otus.basicarchitecture +package ru.otus.basicarchitecture.presentation import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData diff --git a/app/src/main/java/ru/otus/basicarchitecture/ResultFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/presentation/ResultFragment.kt similarity index 84% rename from app/src/main/java/ru/otus/basicarchitecture/ResultFragment.kt rename to app/src/main/java/ru/otus/basicarchitecture/presentation/ResultFragment.kt index 63c4cd5..944706f 100644 --- a/app/src/main/java/ru/otus/basicarchitecture/ResultFragment.kt +++ b/app/src/main/java/ru/otus/basicarchitecture/presentation/ResultFragment.kt @@ -1,17 +1,18 @@ -package ru.otus.basicarchitecture +package ru.otus.basicarchitecture.presentation import android.content.res.ColorStateList import android.os.Bundle -import androidx.fragment.app.Fragment +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import com.google.android.material.chip.Chip import dagger.hilt.android.AndroidEntryPoint +import ru.otus.basicarchitecture.R import ru.otus.basicarchitecture.databinding.FragmentResultBinding -import kotlin.getValue @AndroidEntryPoint class ResultFragment : Fragment() { @@ -46,16 +47,13 @@ class ResultFragment : Fragment() { textViewName.text = it.name textViewSurname.text = it.surname textViewBirthday.text = it.birthday + Log.d("ResultFragment", it.toString()) } } viewModel.userAddress.observe(viewLifecycleOwner) { with(binding){ - textViewAddress.text = String.format( - resources.getString(R.string.address_mask), - it.country, - it.city, - it.address - ) + textViewAddress.text = it.fullAddress + Log.d("ResultFragment", it.toString()) } } viewModel.interests.observe(viewLifecycleOwner) { @@ -63,6 +61,7 @@ class ResultFragment : Fragment() { with(binding){ it.forEach { interest -> val chip = Chip(requireContext()).apply { + Log.d("ResultFragment", interest) text = interest chipBackgroundColor = ColorStateList.valueOf(defaultColor) } @@ -72,6 +71,11 @@ class ResultFragment : Fragment() { } } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + companion object { fun newInstance() = ResultFragment() } diff --git a/app/src/main/java/ru/otus/basicarchitecture/ResultViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/presentation/ResultViewModel.kt similarity index 87% rename from app/src/main/java/ru/otus/basicarchitecture/ResultViewModel.kt rename to app/src/main/java/ru/otus/basicarchitecture/presentation/ResultViewModel.kt index 1d29e2b..ef245e3 100644 --- a/app/src/main/java/ru/otus/basicarchitecture/ResultViewModel.kt +++ b/app/src/main/java/ru/otus/basicarchitecture/presentation/ResultViewModel.kt @@ -1,9 +1,11 @@ -package ru.otus.basicarchitecture +package ru.otus.basicarchitecture.presentation import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import ru.otus.basicarchitecture.domain.UserAddress +import ru.otus.basicarchitecture.domain.UserName import javax.inject.Inject @HiltViewModel diff --git a/app/src/main/java/ru/otus/basicarchitecture/presentation/WizardApp.kt b/app/src/main/java/ru/otus/basicarchitecture/presentation/WizardApp.kt new file mode 100644 index 0000000..33b4439 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/presentation/WizardApp.kt @@ -0,0 +1,8 @@ +package ru.otus.basicarchitecture.presentation + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class WizardApp: Application() { +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/presentation/WizardCache.kt b/app/src/main/java/ru/otus/basicarchitecture/presentation/WizardCache.kt new file mode 100644 index 0000000..0e4b354 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/presentation/WizardCache.kt @@ -0,0 +1,11 @@ +package ru.otus.basicarchitecture.presentation + +import ru.otus.basicarchitecture.domain.UserAddress +import ru.otus.basicarchitecture.domain.UserName +import javax.inject.Inject + +class WizardCache @Inject constructor(){ + var userName: UserName = UserName("", "", "") + var userAddress: UserAddress = UserAddress("", "", "", "", "", "") + var interests: List = listOf() +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 998ce2a..200146a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -3,12 +3,12 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".MainActivity"> + tools:context=".presentation.MainActivity"> + android:name="ru.otus.basicarchitecture.presentation.NameFragment"/> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_address.xml b/app/src/main/res/layout/fragment_address.xml index b09fe12..69ab52c 100644 --- a/app/src/main/res/layout/fragment_address.xml +++ b/app/src/main/res/layout/fragment_address.xml @@ -4,79 +4,34 @@ android:layout_width="match_parent" android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto" - tools:context=".AddressFragment"> + tools:context=".presentation.AddressFragment"> - - - - - - - + app:layout_constraintTop_toTopOf="parent"> - - + android:background="@drawable/border_black" + android:maxLines="1" + android:ellipsize="end"/> - - - - - - + tools:context=".presentation.InterestsFragment"> + tools:context=".presentation.NameFragment"> + tools:context=".presentation.ResultFragment"> The birthday is entered incorrectly You are under 18 The field \"%s\" is empty + Country, city, address + An error occurred while interacting with the network \ No newline at end of file diff --git a/build.gradle b/build.gradle index 2f2e3a8..633ffe2 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,10 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + dependencies { + classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1") + } +} plugins { id 'com.android.application' version '8.7.3' apply false id 'org.jetbrains.kotlin.android' version '2.0.0' apply false From fcb36bceea28b055476f6bf0e7323939b026d75c Mon Sep 17 00:00:00 2001 From: Huawei Date: Thu, 10 Jul 2025 13:54:57 +0300 Subject: [PATCH 3/4] =?UTF-8?q?=D0=92=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/ru/otus/basicarchitecture/di/AppModule.kt | 7 +++++++ .../basicarchitecture/presentation/AddressViewModel.kt | 4 +--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/ru/otus/basicarchitecture/di/AppModule.kt b/app/src/main/java/ru/otus/basicarchitecture/di/AppModule.kt index 47b9a90..ec6145c 100644 --- a/app/src/main/java/ru/otus/basicarchitecture/di/AppModule.kt +++ b/app/src/main/java/ru/otus/basicarchitecture/di/AppModule.kt @@ -9,6 +9,7 @@ import retrofit2.converter.gson.GsonConverterFactory import ru.otus.basicarchitecture.data.AddressApiService import ru.otus.basicarchitecture.data.AddressRepositoryImpl import ru.otus.basicarchitecture.domain.AddressRepository +import ru.otus.basicarchitecture.domain.AddressSuggestUseCase import ru.otus.basicarchitecture.presentation.WizardCache import javax.inject.Singleton @@ -39,4 +40,10 @@ object AppModule { fun provideAddressRepository(impl: AddressRepositoryImpl): AddressRepository { return impl } + + @Provides + @Singleton + fun provideAddressSuggestUseCase(repository: AddressRepository): AddressSuggestUseCase { + return AddressSuggestUseCase(repository) + } } \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/presentation/AddressViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/presentation/AddressViewModel.kt index da6cf1c..6815ed1 100644 --- a/app/src/main/java/ru/otus/basicarchitecture/presentation/AddressViewModel.kt +++ b/app/src/main/java/ru/otus/basicarchitecture/presentation/AddressViewModel.kt @@ -15,11 +15,9 @@ import javax.inject.Inject @HiltViewModel class AddressViewModel @Inject constructor( private val wizardCache: WizardCache, - private val addressRepository: AddressRepository + private val addressSuggestUseCase: AddressSuggestUseCase ): ViewModel() { - private val addressSuggestUseCase = AddressSuggestUseCase(addressRepository) - private var _canContinue = MutableLiveData(false) val canContinue: LiveData get() = _canContinue From 6f8dca0b98e1cf20d2ea3517077c17bc17093904 Mon Sep 17 00:00:00 2001 From: Huawei Date: Fri, 11 Jul 2025 21:22:37 +0300 Subject: [PATCH 4/4] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=203=20=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BD=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 10 ++ .../presentation/AddressViewModelTest.kt | 96 +++++++++++++++++ .../presentation/NameViewModelTest.kt | 101 ++++++++++++++++++ 3 files changed, 207 insertions(+) create mode 100644 app/src/test/java/ru/otus/basicarchitecture/presentation/AddressViewModelTest.kt create mode 100644 app/src/test/java/ru/otus/basicarchitecture/presentation/NameViewModelTest.kt diff --git a/app/build.gradle b/app/build.gradle index 1d17bdc..9e285d7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -68,6 +68,16 @@ dependencies { implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-gson:2.9.0") + implementation 'com.squareup.okhttp3:okhttp:4.12.0' + + testImplementation("junit:junit:4.13.2") + testImplementation 'androidx.arch.core:core-testing:2.2.0' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0' + testImplementation 'org.mockito:mockito-core:5.12.0' + testImplementation 'org.mockito.kotlin:mockito-kotlin:5.2.1' + testImplementation 'org.mockito:mockito-inline:5.2.0' + testImplementation 'net.bytebuddy:byte-buddy:1.14.15' + androidTestImplementation 'org.mockito:mockito-android:5.12.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.2.1' diff --git a/app/src/test/java/ru/otus/basicarchitecture/presentation/AddressViewModelTest.kt b/app/src/test/java/ru/otus/basicarchitecture/presentation/AddressViewModelTest.kt new file mode 100644 index 0000000..4c4d916 --- /dev/null +++ b/app/src/test/java/ru/otus/basicarchitecture/presentation/AddressViewModelTest.kt @@ -0,0 +1,96 @@ +package ru.otus.basicarchitecture.presentation + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import ru.otus.basicarchitecture.domain.AddressSuggestUseCase +import ru.otus.basicarchitecture.domain.UserAddress + + +class AddressViewModelTest { + + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + private val testDispatcher = StandardTestDispatcher() + + private val wizardCache: WizardCache = mock() + private val addressSuggestUseCase: AddressSuggestUseCase = mock() + + private val viewModel: AddressViewModel = AddressViewModel(wizardCache, addressSuggestUseCase) + + @Before + fun before(){ + Dispatchers.setMain(testDispatcher) + println("Начинается тест") + } + + @After + fun after(){ + Dispatchers.resetMain() + println("Тест закончился") + } + + @Test + fun `empty full address returns error`() { + runTest { + whenever(wizardCache.userAddress).thenReturn(UserAddress("", "", "", "", "", "")) + viewModel.validateData() + val actual = viewModel.errorEmptyAddress.value ?: throw RuntimeException("errorEmptyAddress == null") + assertTrue(actual) + } + } + + @Test + fun `valid input returns success`() { + runTest { + whenever(wizardCache.userAddress).thenReturn(UserAddress("", "", "", "", "", "Россия, Москва")) + viewModel.validateData() + val actual = viewModel.errorEmptyAddress.value ?: throw RuntimeException("errorEmptyAddress == null") + assertFalse(actual) + } + } + @Test + fun `network success`() { + runTest { + whenever(addressSuggestUseCase.invoke(any())).thenReturn(getAddress()) + launch { + viewModel.searchAddress("query") + } + advanceUntilIdle() + val actual = viewModel.listUserAddress.value + assertNotNull(actual) + } + } + + @Test + fun `network error`() { + runTest { + whenever(addressSuggestUseCase.invoke(any())).thenThrow(RuntimeException("Network error")) + launch { + viewModel.searchAddress("query") + } + advanceUntilIdle() + val actual = viewModel.errorNetwork.value ?: throw RuntimeException("errorNetwork == null") + assertTrue(actual) + } + } + + private fun getAddress(): List { + return listOf(UserAddress("Россия", "Москва", "Тверская", "3", "", "Россия, Москва, Тверская, 3")) + } +} \ No newline at end of file diff --git a/app/src/test/java/ru/otus/basicarchitecture/presentation/NameViewModelTest.kt b/app/src/test/java/ru/otus/basicarchitecture/presentation/NameViewModelTest.kt new file mode 100644 index 0000000..233fa83 --- /dev/null +++ b/app/src/test/java/ru/otus/basicarchitecture/presentation/NameViewModelTest.kt @@ -0,0 +1,101 @@ +package ru.otus.basicarchitecture.presentation + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito.mock +import org.mockito.kotlin.whenever +import ru.otus.basicarchitecture.domain.UserName + +class NameViewModelTest { + + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + private val wizardCache: WizardCache = mock() + private val viewModel = NameViewModel(wizardCache) + + @Before + fun before(){ + println("Начинается тест") + } + + @After + fun after(){ + println("Тест закончился") + } + + @Test + fun `empty name returns error`() { + runTest { + whenever(wizardCache.userName).thenReturn(UserName("", "Иванов", "19.09.1999")) + viewModel.validateData() + val actual = viewModel.errorEmptyName.value ?: throw RuntimeException("errorEmptyName == null") + assertTrue(actual) + } + } + + @Test + fun `empty surname returns error`() { + runTest { + whenever(wizardCache.userName).thenReturn(UserName("Иван", "", "19.09.1999")) + viewModel.validateData() + val actual = viewModel.errorEmptySurname.value ?: throw RuntimeException("errorEmptySurname == null") + assertTrue(actual) + } + } + + @Test + fun `empty birthday returns error`() { + runTest { + whenever(wizardCache.userName).thenReturn(UserName("Иван", "Иванов", "")) + viewModel.validateData() + val actual = viewModel.errorEmptyBirthday.value ?: throw RuntimeException("errorEmptyBirthday == null") + assertTrue(actual) + } + } + + @Test + fun `young age returns error`() { + runTest { + whenever(wizardCache.userName).thenReturn(UserName("Иван", "Иванов", "19.09.2009")) + viewModel.validateData() + val actual = viewModel.errorAge.value ?: throw RuntimeException("errorAge == null") + assertTrue(actual) + } + } + + @Test + fun `incorrect birthday entry returns error`() { + runTest { + whenever(wizardCache.userName).thenReturn(UserName("Иван", "Иванов", "19.09/2009")) + viewModel.validateData() + val actual = viewModel.errorBirthday.value ?: throw RuntimeException("errorBirthday == null") + assertTrue(actual) + } + } + + @Test + fun `valid input returns success`() { + runTest { + whenever(wizardCache.userName).thenReturn(UserName("Иван", "Иванов", "19.09.1999")) + viewModel.validateData() + val actualList = listOf( + viewModel.errorEmptyName.value, + viewModel.errorEmptySurname.value, + viewModel.errorEmptyBirthday.value, + viewModel.errorBirthday.value, + viewModel.errorAge.value, + ) + if (actualList.contains(null)) { + throw RuntimeException("actual == null") + } + val actual = actualList.contains(true) + assertFalse(actual) + } + } +} \ No newline at end of file