diff --git a/app/build.gradle b/app/build.gradle index 9c99d98..b8d2777 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,8 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' + id 'org.jetbrains.kotlin.kapt' + id 'com.google.dagger.hilt.android' } android { @@ -9,7 +11,7 @@ android { defaultConfig { applicationId "ru.otus.basicarchitecture" - minSdk 24 + minSdk 26 targetSdk 33 versionCode 1 versionName "1.0" @@ -24,11 +26,14 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' + } + buildFeatures { + viewBinding true } } @@ -38,7 +43,15 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.9.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + + implementation deps.fragment + implementation deps.navigation + implementation deps.viewmodel + implementation deps.livedata + +// implementation deps.dagger +// kapt deps.dagger_compiler + + implementation deps.hilt + kapt deps.hilt_compiler } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1e81fea..b8e093e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,10 +11,17 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.BasicArchitecture" - tools:targetApi="31"> + tools:targetApi="31" + android:name=".di.HiltWizardApp"> + android:exported="true"> + + + + + + \ 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..d6512bc 100644 --- a/app/src/main/java/ru/otus/basicarchitecture/MainActivity.kt +++ b/app/src/main/java/ru/otus/basicarchitecture/MainActivity.kt @@ -1,9 +1,12 @@ package ru.otus.basicarchitecture -import androidx.appcompat.app.AppCompatActivity import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +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/di/HiltCacheModule.kt b/app/src/main/java/ru/otus/basicarchitecture/di/HiltCacheModule.kt new file mode 100644 index 0000000..ba37b70 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/di/HiltCacheModule.kt @@ -0,0 +1,19 @@ +package ru.otus.basicarchitecture.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent +import dagger.hilt.android.scopes.ActivityRetainedScoped +import ru.otus.basicarchitecture.wizardcache.WizardCache +import ru.otus.basicarchitecture.wizardcache.WizardCacheImpl + + +@Module +@InstallIn(ActivityRetainedComponent::class) +interface HiltCacheModule { + + @Binds + @ActivityRetainedScoped + fun bindsCache(impl: WizardCacheImpl): WizardCache +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/di/HiltWizardApp.kt b/app/src/main/java/ru/otus/basicarchitecture/di/HiltWizardApp.kt new file mode 100644 index 0000000..03edb9f --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/di/HiltWizardApp.kt @@ -0,0 +1,7 @@ +package ru.otus.basicarchitecture.di + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class HiltWizardApp : Application() \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/address/AddressDataFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/address/AddressDataFragment.kt new file mode 100644 index 0000000..be48f21 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/address/AddressDataFragment.kt @@ -0,0 +1,104 @@ +package ru.otus.basicarchitecture.ui.address + +import android.os.Bundle +import android.text.InputType +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import dagger.hilt.android.AndroidEntryPoint +import ru.otus.basicarchitecture.R +import ru.otus.basicarchitecture.databinding.UserDataLayoutBinding +import ru.otus.basicarchitecture.util.WizardTextWatcher + +@AndroidEntryPoint +class AddressDataFragment : Fragment() { + + private var _binding: UserDataLayoutBinding? = null + private val binding get() = _binding!! + + private val viewModel: AddressFragViewModel by viewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = UserDataLayoutBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.apply { + nameOrCountryEditText.hint = getString(R.string.country) + surnameOrCityEditText.hint = getString(R.string.city) + ageOrAddressEditText.hint = getString(R.string.address) + ageOrAddressEditText.inputType = InputType.TYPE_CLASS_TEXT + + WizardTextWatcher(nameOrCountryEditText).startListen { setButtonState() } + WizardTextWatcher(surnameOrCityEditText).startListen { setButtonState() } + WizardTextWatcher(ageOrAddressEditText).startListen { setButtonState() } + + nextButton.setOnClickListener { + viewModel.onNextButtonClick(nextButton.isSelected) + } + } + + viewModel.getCurrentData() + + viewModel.state.observe(viewLifecycleOwner) { state -> + handleState(state) + } + viewModel.event.observe(viewLifecycleOwner) { event -> + handleEvent(event) + } + } + + private fun handleState(state: AddressFragState) { + binding.apply { + nameOrCountryEditText.setText(state.country) + surnameOrCityEditText.setText(state.city) + ageOrAddressEditText.setText(state.address) + nextButton.isSelected = state.isButtonEnabled + } + } + + private fun handleEvent(event: AddressFragEvent) { + when (event) { + is AddressFragEvent.Error -> { + showToast(event.message) + } + is AddressFragEvent.Success -> { + viewModel.updateAddressData( + binding.nameOrCountryEditText.text.toString(), + binding.surnameOrCityEditText.text.toString(), + binding.ageOrAddressEditText.text.toString(), + ) + findNavController().navigate(R.id.action_addressDataFragment_to_interestsDataFragment) + } + is AddressFragEvent.Empty -> {} + } + } + + private fun setButtonState() { + binding.apply { + nextButton.isSelected = ageOrAddressEditText.text.isNotEmpty() + && nameOrCountryEditText.text.isNotEmpty() + && surnameOrCityEditText.text.isNotEmpty() + } + } + + private fun showToast(msg: String) { + Toast.makeText(this.context, msg, Toast.LENGTH_SHORT).show() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/address/AddressFragEvent.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/address/AddressFragEvent.kt new file mode 100644 index 0000000..d3642ad --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/address/AddressFragEvent.kt @@ -0,0 +1,7 @@ +package ru.otus.basicarchitecture.ui.address + +sealed interface AddressFragEvent { + object Empty : AddressFragEvent + data class Error(val message: String) : AddressFragEvent + object Success : AddressFragEvent +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/address/AddressFragState.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/address/AddressFragState.kt new file mode 100644 index 0000000..9449bee --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/address/AddressFragState.kt @@ -0,0 +1,8 @@ +package ru.otus.basicarchitecture.ui.address + +data class AddressFragState( + val country: String = "", + val city: String = "", + val address: String = "", + val isButtonEnabled: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/address/AddressFragViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/address/AddressFragViewModel.kt new file mode 100644 index 0000000..2e4d6fb --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/address/AddressFragViewModel.kt @@ -0,0 +1,55 @@ +package ru.otus.basicarchitecture.ui.address + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import ru.otus.basicarchitecture.wizardcache.WizardCache +import javax.inject.Inject + +@HiltViewModel +class AddressFragViewModel @Inject constructor( + private val cache: WizardCache +) : ViewModel() { + + private val _state = MutableLiveData(AddressFragState()) + val state: LiveData get() = _state + + private val _event = MutableLiveData() + val event: LiveData get() = _event + + init { + getCurrentData() + } + + fun getCurrentData() { + cache.getUserData().also { data -> + _state.value = _state.value?.copy( + country = data.country, + city = data.city, + address = data.address, + isButtonEnabled = data.country.isNotEmpty() + && data.city.isNotEmpty() + && data.address.isNotEmpty() + ) + } + } + + fun updateAddressData(country: String, city: String, address: String) { + cache.updateAddress(country, city, address) + } + + fun onNextButtonClick(isButtonEnabled: Boolean) { + if (isButtonEnabled) { + _event.value = AddressFragEvent.Success + onEventHandled() + } else { + _event.value = AddressFragEvent.Error("Enter country, city, address") + onEventHandled() + } + } + + private fun onEventHandled() { + _event.value = AddressFragEvent.Empty + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/interests/InterestsDataFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/interests/InterestsDataFragment.kt new file mode 100644 index 0000000..61cb39b --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/interests/InterestsDataFragment.kt @@ -0,0 +1,80 @@ +package ru.otus.basicarchitecture.ui.interests + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.children +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import com.google.android.material.chip.Chip +import dagger.hilt.android.AndroidEntryPoint +import ru.otus.basicarchitecture.R +import ru.otus.basicarchitecture.databinding.UserInterestsLayoutBinding + +@AndroidEntryPoint +class InterestsDataFragment : Fragment() { + + private var _binding: UserInterestsLayoutBinding? = null + private val binding get() = _binding!! + + private val viewModel: InterestsFragViewModel by viewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = UserInterestsLayoutBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + initChipGroup(viewModel.getListOfInterests()) + + viewModel.state.observe(viewLifecycleOwner) { state -> + handleState(state) + } + + binding.goToSummaryButton.setOnClickListener { + viewModel.onSummaryButtonClick() + findNavController().navigate(R.id.action_interestsDataFragment_to_summaryFragment) + } + } + + private fun handleState(state: InterestsFragState) { + setCheckedInterests(state.checkedInterests) + } + + private fun setCheckedInterests(checkedInterests: List) { + binding.chipGroup.children.forEach { + it as Chip + if (checkedInterests.contains(it.text)) { + it.isChecked = true + } + } + } + private fun initChipGroup(listOfInterests: List) { + listOfInterests.forEach { interest -> + val chip = Chip(this.context) + chip.text = interest + + chip.setOnClickListener { + if (chip.isChecked) { + viewModel.addInterest(chip.text.toString()) + } else { + viewModel.removeInterest(chip.text.toString()) + } + } + binding.chipGroup.addView(chip) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/interests/InterestsFragState.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/interests/InterestsFragState.kt new file mode 100644 index 0000000..bb53096 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/interests/InterestsFragState.kt @@ -0,0 +1,5 @@ +package ru.otus.basicarchitecture.ui.interests + +data class InterestsFragState( + val checkedInterests: List = emptyList() +) \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/interests/InterestsFragViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/interests/InterestsFragViewModel.kt new file mode 100644 index 0000000..3e009e3 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/interests/InterestsFragViewModel.kt @@ -0,0 +1,37 @@ +package ru.otus.basicarchitecture.ui.interests + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import ru.otus.basicarchitecture.wizardcache.WizardCache +import javax.inject.Inject + +@HiltViewModel +class InterestsFragViewModel @Inject constructor( + private val cache: WizardCache +) : ViewModel() { + + private val _state = MutableLiveData(InterestsFragState()) + val state: LiveData get() = _state + + init { + _state.value = _state.value?.copy(checkedInterests = cache.getUserData().checkedInterests) + } + + fun addInterest(interest: String) { + val newCheckedInterests = _state.value?.let { it.checkedInterests + interest } ?: emptyList() + _state.value = _state.value?.copy(checkedInterests = newCheckedInterests) + } + + fun removeInterest(interest: String) { + val newCheckedInterests = _state.value?.let { it.checkedInterests - interest } ?: emptyList() + _state.value = _state.value?.copy(checkedInterests = newCheckedInterests) + } + + fun getListOfInterests(): List = cache.getInterestsList() + + fun onSummaryButtonClick() { + _state.value?.let { cache.updateInterests(it.checkedInterests) } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/main/MainDataFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/main/MainDataFragment.kt new file mode 100644 index 0000000..172a4c3 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/main/MainDataFragment.kt @@ -0,0 +1,126 @@ +package ru.otus.basicarchitecture.ui.main + +import android.os.Bundle +import android.text.InputFilter +import android.text.InputType +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import dagger.hilt.android.AndroidEntryPoint +import ru.otus.basicarchitecture.R +import ru.otus.basicarchitecture.databinding.UserDataLayoutBinding +import ru.otus.basicarchitecture.util.EditTextDateMask +import ru.otus.basicarchitecture.util.WizardTextWatcher + +@AndroidEntryPoint +class MainDataFragment : Fragment() { + + private var _binding: UserDataLayoutBinding? = null + private val binding get() = _binding!! + + private val viewModel: MainFragViewModel by viewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = UserDataLayoutBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.apply { + nameOrCountryEditText.hint = getString(R.string.name) + surnameOrCityEditText.hint = getString(R.string.surname) + ageOrAddressEditText.hint = getString(R.string.age) + + WizardTextWatcher(nameOrCountryEditText).startListen { setButtonState() } + WizardTextWatcher(surnameOrCityEditText).startListen { setButtonState() } + WizardTextWatcher(ageOrAddressEditText).startListen { setButtonState() } + EditTextDateMask(ageOrAddressEditText).startListen() + + ageOrAddressEditText.inputType = InputType.TYPE_CLASS_DATETIME + ageOrAddressEditText.filters = arrayOf(InputFilter.LengthFilter(10)) + + ageOrAddressEditText.setOnFocusChangeListener { _, hasFocus -> + ageOrAddressEditText.hint = if (hasFocus) getString(R.string.dd_mm_yyyy_hint) else getString(R.string.age) + } + + nextButton.setOnClickListener { + viewModel.onNextButtonClick( + nextButton.isSelected, ageOrAddressEditText.text.toString() + ) + } + } + + viewModel.viewState.observe(viewLifecycleOwner) { state -> + handleState(state) + } + viewModel.viewEvent.observe(viewLifecycleOwner) { event -> + handleEvent(event) + } + } + + override fun onResume() { + super.onResume() + viewModel.getCurrentData() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun handleState(state: MainFragState) { + binding.apply { + nameOrCountryEditText.setText(state.name) + surnameOrCityEditText.setText(state.surname) + ageOrAddressEditText.setText(state.dob) + nextButton.isSelected = state.isButtonEnabled + } + } + + private fun handleEvent(event: MainFragEvent) { + when (event) { + is MainFragEvent.Error -> { + showToast(event.message) + viewModel.onEventHandled() + } + + is MainFragEvent.Success -> { + viewModel.updateMainData( + binding.nameOrCountryEditText.text.toString(), + binding.surnameOrCityEditText.text.toString(), + binding.ageOrAddressEditText.text.toString(), + ) + findNavController().navigate(R.id.action_mainDataFragment_to_addressDataFragment) + viewModel.onEventHandled() + } + + is MainFragEvent.Empty -> {} + } + } + + private fun setButtonState() { + binding.apply { + nextButton.isSelected = ageOrAddressEditText.text.isNotEmpty() + && nameOrCountryEditText.text.isNotEmpty() + && surnameOrCityEditText.text.isNotEmpty() + } + } + + private fun showToast(msg: String) { + Toast.makeText( + this.context, + msg, + Toast.LENGTH_SHORT + ).show() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/main/MainFragEvent.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/main/MainFragEvent.kt new file mode 100644 index 0000000..276bed4 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/main/MainFragEvent.kt @@ -0,0 +1,7 @@ +package ru.otus.basicarchitecture.ui.main + +sealed interface MainFragEvent { + object Empty : MainFragEvent + data class Error(val message: String) : MainFragEvent + object Success : MainFragEvent +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/main/MainFragState.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/main/MainFragState.kt new file mode 100644 index 0000000..6fd747c --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/main/MainFragState.kt @@ -0,0 +1,8 @@ +package ru.otus.basicarchitecture.ui.main + +data class MainFragState( + val name: String = "", + val surname: String = "", + val dob: String = "", + val isButtonEnabled: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/main/MainFragViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/main/MainFragViewModel.kt new file mode 100644 index 0000000..00f67b6 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/main/MainFragViewModel.kt @@ -0,0 +1,60 @@ +package ru.otus.basicarchitecture.ui.main + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import ru.otus.basicarchitecture.util.AgeValidator +import ru.otus.basicarchitecture.wizardcache.WizardCache +import javax.inject.Inject + +@HiltViewModel +class MainFragViewModel @Inject constructor( + private val cache: WizardCache +) : ViewModel() { + + private val _viewState = MutableLiveData(MainFragState()) + val viewState: LiveData get() = _viewState + + private val _viewEvent = MutableLiveData() + val viewEvent: LiveData get() = _viewEvent + + + init { + getCurrentData() + } + + fun getCurrentData() { + cache.getUserData().also { data -> + _viewState.value = _viewState.value?.copy( + name = data.name, + surname = data.surname, + dob = data.dateOfBirth, + isButtonEnabled = data.name.isNotEmpty() + && data.surname.isNotEmpty() + && data.dateOfBirth.isNotEmpty() + ) + } + } + + fun updateMainData(name: String, surname: String, dob: String) { + cache.updateMainData(name, surname, dob) + } + + fun onNextButtonClick(isButtonSelected: Boolean, dob: String) { + if (!isButtonSelected) { + _viewEvent.value = MainFragEvent.Error("Enter name, surname, age") + return + } + + if (AgeValidator.isAgeValid(dob)) { + _viewEvent.value = MainFragEvent.Success + } else { + _viewEvent.value = MainFragEvent.Error("Too young to proceed") + } + } + + fun onEventHandled() { + _viewEvent.value = MainFragEvent.Empty + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/summary/SummaryFragState.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/summary/SummaryFragState.kt new file mode 100644 index 0000000..7ce9ef3 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/summary/SummaryFragState.kt @@ -0,0 +1,9 @@ +package ru.otus.basicarchitecture.ui.summary + +data class SummaryFragState( + val name: String ="", + val surname: String = "", + val dob: String = "", + val fullAddress: String = "", + val interests: List = emptyList() +) \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/summary/SummaryFragViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/summary/SummaryFragViewModel.kt new file mode 100644 index 0000000..a545a2a --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/summary/SummaryFragViewModel.kt @@ -0,0 +1,36 @@ +package ru.otus.basicarchitecture.ui.summary + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import ru.otus.basicarchitecture.wizardcache.WizardCache +import javax.inject.Inject + +@HiltViewModel +class SummaryFragViewModel @Inject constructor( + private val cache: WizardCache +) : ViewModel() { + + private val _state = MutableLiveData(SummaryFragState()) + val state: LiveData get() = _state + + init { + getCurrentData() + } + + private fun getCurrentData() { + cache.getUserData().also { data -> + _state.value = _state.value?.copy( + name = data.name, + surname = data.surname, + dob = data.dateOfBirth, + fullAddress = concatenateAddress(data.country, data.city, data.address), + interests = data.checkedInterests + ) + } + } + private fun concatenateAddress(country: String, city: String, address: String): String { + return "$country, $city, $address" + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/summary/SummaryFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/summary/SummaryFragment.kt new file mode 100644 index 0000000..7272739 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/summary/SummaryFragment.kt @@ -0,0 +1,58 @@ +package ru.otus.basicarchitecture.ui.summary + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +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.databinding.UserSummaryLayoutBinding + +@AndroidEntryPoint +class SummaryFragment : Fragment() { + + private var _binding: UserSummaryLayoutBinding? = null + private val binding: UserSummaryLayoutBinding get() = _binding!! + + private val viewModel: SummaryFragViewModel by viewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = UserSummaryLayoutBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.state.observe(viewLifecycleOwner) { state -> + handleState(state) + } + } + + private fun handleState(state: SummaryFragState) { + binding.apply { + namePlaceholder.text = state.name + surnamePlaceholder.text = state.surname + dobPlaceholder.text = state.dob + addressPlaceholder.text = state.fullAddress + + state.interests.forEach { + val chip = Chip(context) + chip.isClickable = false + chip.text = it + interestsChipGroup.addView(chip) + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/util/AgeValidator.kt b/app/src/main/java/ru/otus/basicarchitecture/util/AgeValidator.kt new file mode 100644 index 0000000..6de5d21 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/util/AgeValidator.kt @@ -0,0 +1,21 @@ +package ru.otus.basicarchitecture.util + +import java.time.LocalDate +import java.time.Period +import java.time.format.DateTimeFormatter +import java.time.format.DateTimeParseException + +object AgeValidator { + + private val formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy") + + fun isAgeValid(date: String): Boolean { + val birthdate: LocalDate + try { + birthdate = LocalDate.parse(date, formatter) + } catch (e: DateTimeParseException) { + return false + } + return Period.between(birthdate, LocalDate.now()).years > 17 + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/util/EditTextDateMask.kt b/app/src/main/java/ru/otus/basicarchitecture/util/EditTextDateMask.kt new file mode 100644 index 0000000..be24cf3 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/util/EditTextDateMask.kt @@ -0,0 +1,64 @@ +package ru.otus.basicarchitecture.util + +import android.text.Editable +import android.text.TextWatcher +import android.widget.EditText + +class EditTextDateMask( + private val editText: EditText +) { + + private val textWatcher = object : TextWatcher { + + private val firstDividerPosition = 2 + private val secondDividerPosition = 5 + private val maxTextLength = 10 + + var isEdited = false + val divider = "." + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + if (isEdited) { + isEdited = false + return + } + + var currText = getText() + currText = handleDateDivider(currText, firstDividerPosition, start, before) + currText = handleDateDivider(currText, secondDividerPosition, start, before) + + isEdited = true + editText.setText(currText) + editText.setSelection(editText.text.length) + } + + private fun getText(): String { + return if (editText.text.length >= maxTextLength) + editText.text.toString().substring(0, maxTextLength) + else + editText.text.toString() + } + + private fun handleDateDivider( + currText: String, + dividerPosition: Int, + start: Int, + before: Int + ): String { + if (currText.length == dividerPosition) { + return if (before <= dividerPosition && start < dividerPosition) + currText + divider + else + currText.dropLast(1) + } + return currText + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun afterTextChanged(s: Editable?) {} + } + + fun startListen() { + editText.addTextChangedListener(textWatcher) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/util/WizardTextWatcher.kt b/app/src/main/java/ru/otus/basicarchitecture/util/WizardTextWatcher.kt new file mode 100644 index 0000000..fc89b7e --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/util/WizardTextWatcher.kt @@ -0,0 +1,20 @@ +package ru.otus.basicarchitecture.util + +import android.text.Editable +import android.text.TextWatcher +import android.widget.EditText + +class WizardTextWatcher( + private val editText: EditText +) { + + private fun getTextWatcher(block: () -> Unit) = object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable?) { block() } + } + + fun startListen(block: () -> Unit) { + editText.addTextChangedListener( getTextWatcher(block)) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/wizardcache/WizardCache.kt b/app/src/main/java/ru/otus/basicarchitecture/wizardcache/WizardCache.kt new file mode 100644 index 0000000..59d9046 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/wizardcache/WizardCache.kt @@ -0,0 +1,11 @@ +package ru.otus.basicarchitecture.wizardcache + + +interface WizardCache { + + fun getUserData(): WizardUserData + fun getInterestsList(): List + fun updateMainData(name: String, surname: String, dateOfBirth: String) + fun updateAddress(country: String, city: String,address: String) + fun updateInterests(interests: List) +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/wizardcache/WizardCacheImpl.kt b/app/src/main/java/ru/otus/basicarchitecture/wizardcache/WizardCacheImpl.kt new file mode 100644 index 0000000..32cf949 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/wizardcache/WizardCacheImpl.kt @@ -0,0 +1,32 @@ +package ru.otus.basicarchitecture.wizardcache + +import javax.inject.Inject + +class WizardCacheImpl @Inject constructor() : WizardCache { + + private val interests = listOf("Hiking", "Programming", + "Travel", "Walking", "Cycling", "Theatre", + "Playing double bass", "Movies", "Reading books", + "Running", "Photography", "Bouldering", "Nightclubs", + "Bikepacking trips", "Football", "Boxing", "Self education") + + private var data = WizardUserData() + + override fun getUserData(): WizardUserData { + return data + } + + override fun getInterestsList(): List = interests + + override fun updateMainData(name: String, surname: String, dateOfBirth: String) { + data = data.copy(name = name, surname = surname, dateOfBirth = dateOfBirth) + } + + override fun updateAddress(country: String, city: String, address: String) { + data = data.copy(country =country, city = city, address = address) + } + + override fun updateInterests(interests: List) { + data = data.copy(checkedInterests = interests) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/wizardcache/WizardUserData.kt b/app/src/main/java/ru/otus/basicarchitecture/wizardcache/WizardUserData.kt new file mode 100644 index 0000000..d50dac4 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/wizardcache/WizardUserData.kt @@ -0,0 +1,12 @@ +package ru.otus.basicarchitecture.wizardcache + +data class WizardUserData( + val id: Long = 0, + val name: String = "", + val surname: String= "", + val dateOfBirth: String= "", + val country: String= "", + val city: String= "", + val address: String= "", + val checkedInterests: List = emptyList() +) diff --git a/app/src/main/res/drawable/chip_checked.xml b/app/src/main/res/drawable/chip_checked.xml new file mode 100644 index 0000000..77a1472 --- /dev/null +++ b/app/src/main/res/drawable/chip_checked.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/chip_style.xml b/app/src/main/res/drawable/chip_style.xml new file mode 100644 index 0000000..0be7f4e --- /dev/null +++ b/app/src/main/res/drawable/chip_style.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/chip_unchecked.xml b/app/src/main/res/drawable/chip_unchecked.xml new file mode 100644 index 0000000..dd6181f --- /dev/null +++ b/app/src/main/res/drawable/chip_unchecked.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/data_edit_text_style.xml b/app/src/main/res/drawable/data_edit_text_style.xml new file mode 100644 index 0000000..7c665f9 --- /dev/null +++ b/app/src/main/res/drawable/data_edit_text_style.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/next_button_enabled_style.xml b/app/src/main/res/drawable/next_button_enabled_style.xml new file mode 100644 index 0000000..5b78cf2 --- /dev/null +++ b/app/src/main/res/drawable/next_button_enabled_style.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/next_button_not_enabled_style.xml b/app/src/main/res/drawable/next_button_not_enabled_style.xml new file mode 100644 index 0000000..41689e3 --- /dev/null +++ b/app/src/main/res/drawable/next_button_not_enabled_style.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/next_button_style.xml b/app/src/main/res/drawable/next_button_style.xml new file mode 100644 index 0000000..adccc17 --- /dev/null +++ b/app/src/main/res/drawable/next_button_style.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/font/font_architects_daughter.ttf b/app/src/main/res/font/font_architects_daughter.ttf new file mode 100644 index 0000000..0efbb7a Binary files /dev/null and b/app/src/main/res/font/font_architects_daughter.ttf differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 0b15a20..f961c87 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,9 +1,19 @@ - - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/src/main/res/layout/user_data_layout.xml b/app/src/main/res/layout/user_data_layout.xml new file mode 100644 index 0000000..5a98ca4 --- /dev/null +++ b/app/src/main/res/layout/user_data_layout.xml @@ -0,0 +1,61 @@ + + + + + + + + + +