From f63604fd314b5106cd02c5c0f1f61cfa0fc378b0 Mon Sep 17 00:00:00 2001 From: AndreiRookie Date: Tue, 22 Aug 2023 12:52:44 +0400 Subject: [PATCH 1/8] raw implementation; one VM; no DI --- app/build.gradle | 15 +- app/src/main/AndroidManifest.xml | 8 +- .../basicarchitecture/dto/WizardUserData.kt | 12 + .../ui/AddressDataFragment.kt | 132 +++++++++++ .../ui/InterestsDataFragment.kt | 139 ++++++++++++ .../basicarchitecture/ui/MainDataFragment.kt | 206 ++++++++++++++++++ .../basicarchitecture/ui/SummaryFragment.kt | 88 ++++++++ .../basicarchitecture/util/AgeValidator.kt | 21 ++ .../basicarchitecture/viewmodel/Action.kt | 7 + .../otus/basicarchitecture/viewmodel/Event.kt | 9 + .../otus/basicarchitecture/viewmodel/State.kt | 9 + .../viewmodel/WizardStateHolder.kt | 6 + .../viewmodel/WizardViewModel.kt | 102 +++++++++ .../wizardcache/WizardCache.kt | 14 ++ .../wizardcache/WizardCacheImpl.kt | 53 +++++ app/src/main/res/drawable/chip_checked.xml | 6 + app/src/main/res/drawable/chip_style.xml | 6 + app/src/main/res/drawable/chip_unchecked.xml | 6 + .../res/drawable/data_edit_text_style.xml | 8 + .../drawable/next_button_enabled_style.xml | 7 + .../next_button_not_enabled_style.xml | 7 + .../main/res/drawable/next_button_style.xml | 5 + .../res/font/font_architects_daughter.ttf | Bin 0 -> 43356 bytes app/src/main/res/layout/activity_main.xml | 14 +- app/src/main/res/layout/user_data_layout.xml | 61 ++++++ .../main/res/layout/user_interests_layout.xml | 85 ++++++++ .../main/res/layout/user_summary_layout.xml | 89 ++++++++ app/src/main/res/navigation/nav_graph.xml | 43 ++++ app/src/main/res/values/attrs.xml | 6 + app/src/main/res/values/colors.xml | 5 + app/src/main/res/values/dimens.xml | 4 + app/src/main/res/values/strings.xml | 11 + app/src/main/res/values/styles.xml | 75 +++++++ app/src/main/res/values/themes.xml | 4 +- build.gradle | 20 ++ 35 files changed, 1275 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/ru/otus/basicarchitecture/dto/WizardUserData.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/ui/AddressDataFragment.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/ui/InterestsDataFragment.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/ui/MainDataFragment.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/ui/SummaryFragment.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/util/AgeValidator.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/viewmodel/Action.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/viewmodel/Event.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/viewmodel/State.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/viewmodel/WizardStateHolder.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/viewmodel/WizardViewModel.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/wizardcache/WizardCache.kt create mode 100644 app/src/main/java/ru/otus/basicarchitecture/wizardcache/WizardCacheImpl.kt create mode 100644 app/src/main/res/drawable/chip_checked.xml create mode 100644 app/src/main/res/drawable/chip_style.xml create mode 100644 app/src/main/res/drawable/chip_unchecked.xml create mode 100644 app/src/main/res/drawable/data_edit_text_style.xml create mode 100644 app/src/main/res/drawable/next_button_enabled_style.xml create mode 100644 app/src/main/res/drawable/next_button_not_enabled_style.xml create mode 100644 app/src/main/res/drawable/next_button_style.xml create mode 100644 app/src/main/res/font/font_architects_daughter.ttf create mode 100644 app/src/main/res/layout/user_data_layout.xml create mode 100644 app/src/main/res/layout/user_interests_layout.xml create mode 100644 app/src/main/res/layout/user_summary_layout.xml create mode 100644 app/src/main/res/navigation/nav_graph.xml create mode 100644 app/src/main/res/values/attrs.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/styles.xml diff --git a/app/build.gradle b/app/build.gradle index 9c99d98..121351c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,7 +9,7 @@ android { defaultConfig { applicationId "ru.otus.basicarchitecture" - minSdk 24 + minSdk 26 targetSdk 33 versionCode 1 versionName "1.0" @@ -30,6 +30,9 @@ android { kotlinOptions { jvmTarget = '1.8' } + buildFeatures { + viewBinding true + } } dependencies { @@ -38,7 +41,11 @@ 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 + implementation deps.dagger_compiler } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1e81fea..aba7ca0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,7 +14,13 @@ tools:targetApi="31"> + android:exported="true"> + + + + + + \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/dto/WizardUserData.kt b/app/src/main/java/ru/otus/basicarchitecture/dto/WizardUserData.kt new file mode 100644 index 0000000..4ad685a --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/dto/WizardUserData.kt @@ -0,0 +1,12 @@ +package ru.otus.basicarchitecture.dto + +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 interests: Set = emptySet() +) diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/AddressDataFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/AddressDataFragment.kt new file mode 100644 index 0000000..cc0760b --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/AddressDataFragment.kt @@ -0,0 +1,132 @@ +package ru.otus.basicarchitecture.ui + +import android.os.Bundle +import android.text.Editable +import android.text.InputType +import android.text.TextWatcher +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.activityViewModels +import androidx.navigation.fragment.findNavController +import ru.otus.basicarchitecture.R +import ru.otus.basicarchitecture.databinding.UserDataLayoutBinding +import ru.otus.basicarchitecture.viewmodel.Action +import ru.otus.basicarchitecture.viewmodel.WizardViewModel +import ru.otus.basicarchitecture.viewmodel.Event +import ru.otus.basicarchitecture.viewmodel.State +import ru.otus.basicarchitecture.viewmodel.WizardStateHolder + +class AddressDataFragment : Fragment() { + + private var _binding: UserDataLayoutBinding? = null + private val binding get() = _binding!! + + private val viewModel by activityViewModels { WizardViewModel.getFactory(requireContext()) } + + + 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 + + nameOrCountryEditText.addTextChangedListener(textWatcher) + surnameOrCityEditText.addTextChangedListener(textWatcher) + ageOrAddressEditText.addTextChangedListener(textWatcher) + + nextButton.setOnClickListener { button -> + viewModel.sendAction(Action.AddressFragOnButtonClick(button.isSelected)) + } + } + setNextButtonState() + + viewModel.wizardState.observe(viewLifecycleOwner) { data -> + binding.apply { + nameOrCountryEditText.setText(data.country) + surnameOrCityEditText.setText(data.city) + ageOrAddressEditText.setText(data.address) + } + } + + viewModel.state.observe(viewLifecycleOwner) { stateHolder -> +// handleStateHolder(stateHolder) + stateHolder.event?.let { handleEvent(stateHolder.event) } + } + + } + + private fun handleStateHolder(stateHolder: WizardStateHolder) { + handleState(stateHolder.state) + stateHolder.event?.let { handleEvent(stateHolder.event) } + } + + private fun handleState(state: State) { + when (state) { + is State.Init -> {} + is State.Data -> { + val data = state.userData + binding.apply { + nameOrCountryEditText.setText(data.country) + surnameOrCityEditText.setText(data.city) + ageOrAddressEditText.setText(data.address) + } + } + } + } + + private fun handleEvent(event: Event) { + when (event) { + is Event.Error -> { + showToast(event.message) + } + is Event.OpenAddressFragment -> {} + is Event.OpenSummaryFragment -> {} + is Event.OpenInterestsFragment -> { + viewModel.updateAddress( + binding.nameOrCountryEditText.text.toString(), + binding.surnameOrCityEditText.text.toString(), + binding.ageOrAddressEditText.text.toString(), + ) + findNavController().navigate(R.id.action_addressDataFragment_to_interestsDataFragment) + } + } + } + + private fun setNextButtonState() { + binding.apply { + nextButton.isSelected = ageOrAddressEditText.text.isNotEmpty() + && nameOrCountryEditText.text.isNotEmpty() + && surnameOrCityEditText.text.isNotEmpty() + } + } + + private val textWatcher = 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?) { setNextButtonState() } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + 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/InterestsDataFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/InterestsDataFragment.kt new file mode 100644 index 0000000..9535f58 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/InterestsDataFragment.kt @@ -0,0 +1,139 @@ +package ru.otus.basicarchitecture.ui + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import com.google.android.material.chip.Chip +import ru.otus.basicarchitecture.R +import ru.otus.basicarchitecture.databinding.UserInterestsLayoutBinding +import ru.otus.basicarchitecture.viewmodel.Action +import ru.otus.basicarchitecture.viewmodel.Event +import ru.otus.basicarchitecture.viewmodel.State +import ru.otus.basicarchitecture.viewmodel.WizardViewModel +import ru.otus.basicarchitecture.viewmodel.WizardStateHolder + +class InterestsDataFragment : Fragment() { + + private var _binding: UserInterestsLayoutBinding? = null + private val binding get() = _binding!! + + private val viewModel by activityViewModels { WizardViewModel.getFactory(requireContext()) } + + private val checkedInterests = mutableSetOf() + + + 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) + + Log.e("AAA", "checkedInterests" + checkedInterests) + +// viewModel.wizardState.observe(viewLifecycleOwner) { data -> +// if (data.interests.isNotEmpty()) { +// checkedInterests += data.interests +// } +// } + + initChipGroup() + +// binding.chipGroup.setOnCheckedStateChangeListener { group, checkedIds -> +// checkedIds.forEach { +// val chip = group.findViewById(it) +// checkedInterests += chip.text.toString() +// } +// } + + viewModel.state.observe(viewLifecycleOwner) { stateHolder -> +// handleStateHolder(stateHolder) + stateHolder.event?.let { handleEvent(stateHolder.event) } + } + + binding.goToSummaryButton.setOnClickListener { + viewModel.sendAction(Action.InterestsFragOnButtonClick) + } + + checkState() + } + + private fun checkState() { + viewModel.wizardState.observe(viewLifecycleOwner) { data -> + if (data.interests.isNotEmpty()) { + checkedInterests += data.interests + } + } + } + + private fun initChipGroup() { + INTERESTS.forEach { interest -> + val chip = Chip(this.context) + chip.text = interest + + if (checkedInterests.contains(interest)) { + chip.isChecked = true + } + + chip.setOnClickListener { + if (chip.isChecked) { + checkedInterests += chip.text.toString() + } else { + checkedInterests -= chip.text.toString() + } + } + binding.chipGroup.addView(chip) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun handleStateHolder(stateHolder: WizardStateHolder) { + handleState(stateHolder.state) + stateHolder.event?.let { handleEvent(stateHolder.event) } + } + + private fun handleState(state: State) { + when (state) { + is State.Init -> {} + is State.Data -> { + val data = state.userData + if (data.interests.isNotEmpty()) { + checkedInterests += data.interests + } + } + } + } + + private fun handleEvent(event: Event) { + when (event) { + is Event.Error -> {} + is Event.OpenAddressFragment -> {} + is Event.OpenSummaryFragment -> { + viewModel.updateInterests(checkedInterests) + findNavController().navigate(R.id.action_interestsDataFragment_to_summaryFragment) + } + is Event.OpenInterestsFragment -> {} + } + } + + companion object { + private val INTERESTS = listOf( + "Hiking", "Programming", "Travel", "Reading books", "Playing double bass", + "Movies", "Cycling", "Theatre", "Running", "Photography", "Bouldering", "Nightclubs" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/MainDataFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/MainDataFragment.kt new file mode 100644 index 0000000..adc2b81 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/MainDataFragment.kt @@ -0,0 +1,206 @@ +package ru.otus.basicarchitecture.ui + +import android.os.Bundle +import android.text.Editable +import android.text.InputFilter +import android.text.InputType +import android.text.TextWatcher +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.activityViewModels +import androidx.navigation.fragment.findNavController +import ru.otus.basicarchitecture.R +import ru.otus.basicarchitecture.databinding.UserDataLayoutBinding +import ru.otus.basicarchitecture.viewmodel.Action +import ru.otus.basicarchitecture.viewmodel.WizardViewModel +import ru.otus.basicarchitecture.viewmodel.Event +import ru.otus.basicarchitecture.viewmodel.State +import ru.otus.basicarchitecture.viewmodel.WizardStateHolder + +class MainDataFragment : Fragment() { + + private var _binding: UserDataLayoutBinding? = null + private val binding get() = _binding!! + + private val viewModel by activityViewModels { WizardViewModel.getFactory(requireContext()) } + + 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) + + nameOrCountryEditText.addTextChangedListener(textWatcher) + surnameOrCityEditText.addTextChangedListener(textWatcher) + ageOrAddressEditText.addTextChangedListener(ageFieldTextWatcher) + + 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) + } + } + + setNextButtonState() + + nextButton.setOnClickListener { button -> + viewModel.sendAction( + Action.MainFragOnButtonClick( + button.isSelected, + ageOrAddressEditText.text.toString() + ) + ) + } + } + viewModel.wizardState.observe(viewLifecycleOwner) { user -> + binding.apply { + nameOrCountryEditText.setText(user.name) + surnameOrCityEditText.setText(user.surname) + ageOrAddressEditText.setText(user.dateOfBirth) + } + } + + viewModel.state.observe(viewLifecycleOwner) { stateHolder -> +// handleStateHolder(stateHolder + stateHolder.event?.let { handleEvent(stateHolder.event) } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun handleStateHolder(stateHolder: WizardStateHolder) { + handleState(stateHolder.state) + stateHolder.event?.let { handleEvent(stateHolder.event) } + } + + private fun handleState(state: State) { + when (state) { + is State.Init -> {} + is State.Data -> { + val data = state.userData + binding.apply { + nameOrCountryEditText.setText(data.name) + surnameOrCityEditText.setText(data.surname) + ageOrAddressEditText.setText(data.dateOfBirth) + } + } + } + } + + private fun handleEvent(event: Event) { + when (event) { + is Event.Error -> { + showToast(event.message) + } + + is Event.OpenAddressFragment -> { + viewModel.updateMainData( + binding.nameOrCountryEditText.text.toString(), + binding.surnameOrCityEditText.text.toString(), + binding.ageOrAddressEditText.text.toString(), + ) + findNavController().navigate(R.id.action_mainDataFragment_to_addressDataFragment) + } + + is Event.OpenInterestsFragment -> {} + is Event.OpenSummaryFragment -> {} + } + } + + private fun setNextButtonState() { + 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() + } + + private val textWatcher = 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?) { + setNextButtonState() + } + } + + private val ageFieldTextWatcher = object : TextWatcher { + val firstDividerPosition = 2 + val secondDividerPosition = 5 + 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 + binding.ageOrAddressEditText.setText(currText) + binding.ageOrAddressEditText.setSelection(binding.ageOrAddressEditText.text.length) + } + + private fun getText(): String { + return if (binding.ageOrAddressEditText.text.length >= maxTextLength) + binding.ageOrAddressEditText.text.toString().substring(0, maxTextLength) + else + binding.ageOrAddressEditText.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?) { + setNextButtonState() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/SummaryFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/SummaryFragment.kt new file mode 100644 index 0000000..69534ce --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/SummaryFragment.kt @@ -0,0 +1,88 @@ +package ru.otus.basicarchitecture.ui + +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.activityViewModels +import com.google.android.material.chip.Chip +import ru.otus.basicarchitecture.databinding.UserSummaryLayoutBinding +import ru.otus.basicarchitecture.viewmodel.State +import ru.otus.basicarchitecture.viewmodel.WizardViewModel + +class SummaryFragment : Fragment() { + + private var _binding: UserSummaryLayoutBinding? = null + private val binding: UserSummaryLayoutBinding get() = _binding!! + + private val viewModel by activityViewModels { WizardViewModel.getFactory(requireContext()) } + + 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.wizardState.observe(viewLifecycleOwner) { data -> + binding.apply { + namePlaceholder.text = data.name + surnamePlaceholder.text = data.surname + dobPlaceholder.text = data.dateOfBirth + addressPlaceholder.text = concatenateAddress( + data.country, data.city, data.address) + + + data.interests.forEach { + val chip = Chip(context) + chip.isClickable = false + chip.text = it + interestsChipGroup.addView(chip) + } + } + } + +// viewModel.state.observe(viewLifecycleOwner) { stateHolder -> +// handleState(stateHolder.state) +// } + } + + private fun handleState(state: State) { + when (state) { + is State.Init -> {} + is State.Data -> { + val data = state.userData + binding.apply { + namePlaceholder.text = data.name + surnamePlaceholder.text = data.surname + dobPlaceholder.text = data.dateOfBirth + addressPlaceholder.text = concatenateAddress( + data.country, data.city, data.address) + + + data.interests.forEach { + val chip = Chip(context) + chip.isClickable = false + chip.text = it + interestsChipGroup.addView(chip) + } + } + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + 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/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/viewmodel/Action.kt b/app/src/main/java/ru/otus/basicarchitecture/viewmodel/Action.kt new file mode 100644 index 0000000..a07b87f --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/viewmodel/Action.kt @@ -0,0 +1,7 @@ +package ru.otus.basicarchitecture.viewmodel + +sealed interface Action { + data class MainFragOnButtonClick(val isButtonSelected: Boolean, val dob: String) : Action + data class AddressFragOnButtonClick(val isButtonSelected: Boolean) : Action + object InterestsFragOnButtonClick : Action +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/viewmodel/Event.kt b/app/src/main/java/ru/otus/basicarchitecture/viewmodel/Event.kt new file mode 100644 index 0000000..79bfcd4 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/viewmodel/Event.kt @@ -0,0 +1,9 @@ +package ru.otus.basicarchitecture.viewmodel + + +sealed interface Event { + object OpenAddressFragment : Event + object OpenInterestsFragment : Event + object OpenSummaryFragment : Event + data class Error(val message: String) : Event +} diff --git a/app/src/main/java/ru/otus/basicarchitecture/viewmodel/State.kt b/app/src/main/java/ru/otus/basicarchitecture/viewmodel/State.kt new file mode 100644 index 0000000..55c831c --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/viewmodel/State.kt @@ -0,0 +1,9 @@ +package ru.otus.basicarchitecture.viewmodel + +import ru.otus.basicarchitecture.dto.WizardUserData + +sealed interface State { + + data class Data(val userData: WizardUserData) : State + object Init : State +} diff --git a/app/src/main/java/ru/otus/basicarchitecture/viewmodel/WizardStateHolder.kt b/app/src/main/java/ru/otus/basicarchitecture/viewmodel/WizardStateHolder.kt new file mode 100644 index 0000000..59197a2 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/viewmodel/WizardStateHolder.kt @@ -0,0 +1,6 @@ +package ru.otus.basicarchitecture.viewmodel + +data class WizardStateHolder( + val state: State, + val event: Event? = null +) \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/viewmodel/WizardViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/viewmodel/WizardViewModel.kt new file mode 100644 index 0000000..d29deeb --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/viewmodel/WizardViewModel.kt @@ -0,0 +1,102 @@ +package ru.otus.basicarchitecture.viewmodel + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import ru.otus.basicarchitecture.dto.WizardUserData +import ru.otus.basicarchitecture.util.AgeValidator +import ru.otus.basicarchitecture.wizardcache.WizardCache +import ru.otus.basicarchitecture.wizardcache.WizardCacheImpl + +class WizardViewModel( + private val cache: WizardCache +) : ViewModel() { + + private val _state = MutableLiveData(WizardStateHolder(state = State.Init)) + val state: LiveData get() = _state + + val wizardState: LiveData = cache.getData() + + + fun sendAction(action: Action) { + when (action) { + is Action.MainFragOnButtonClick -> { + openAddressFragment(action.isButtonSelected, action.dob) + onEventHandled() + } + is Action.AddressFragOnButtonClick -> { + openInterestsFragment(action.isButtonSelected) + onEventHandled() + } + is Action.InterestsFragOnButtonClick -> { + openSummaryFragment() + onEventHandled() + } + } + } + +// private fun getUserData() { +// val currData = cache.getUserData() +// _state.value = _state.value?.copy(state = State.Data(currData)) +// } + + + fun updateMainData(name: String, surname: String, dob: String) { + cache.updateMainData(name, surname, dob) + +// getUserData() + } + + fun updateAddress(country: String, city: String, address: String) { + cache.updateAddress(country, city, address) + +// getUserData() + } + + fun updateInterests(interests: Set) { + cache.updateInterests(interests) + +// getUserData() + } + + private fun openAddressFragment(isButtonSelected: Boolean, dob: String) { + if (!isButtonSelected) { + _state.value = _state.value?.copy(event = Event.Error("Enter name, surname, age")) + return + } + + if (AgeValidator.isAgeValid(dob)) { + _state.value = _state.value?.copy(event = Event.OpenAddressFragment) + } else { + _state.value = _state.value?.copy(event = Event.Error("Too young to proceed")) + } + } + + private fun openInterestsFragment(isButtonSelected: Boolean) { + if (isButtonSelected) { + _state.value = _state.value?.copy(event = Event.OpenInterestsFragment) + } else { + _state.value = _state.value?.copy(event = Event.Error("Enter country, city, address")) + } + } + + private fun openSummaryFragment() { + _state.value = _state.value?.copy(event = Event.OpenSummaryFragment) + } + + private fun onEventHandled() { +// if (_state.value?.event == event) { + _state.value = _state.value?.copy(event = null) +// } + } + + companion object{ + fun getFactory(context: Context) = object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return WizardViewModel(WizardCacheImpl()) as T + } + } + } +} \ 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..671ae0c --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/wizardcache/WizardCache.kt @@ -0,0 +1,14 @@ +package ru.otus.basicarchitecture.wizardcache + +import androidx.lifecycle.LiveData +import ru.otus.basicarchitecture.dto.WizardUserData + +interface WizardCache { + +// fun getUserData(): WizardUserData + + fun getData(): LiveData + fun updateMainData(name: String, surname: String, dateOfBirth: String) + fun updateAddress(country: String, city: String,address: String) + fun updateInterests(interests: Set) +} \ 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..6378daa --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/wizardcache/WizardCacheImpl.kt @@ -0,0 +1,53 @@ +package ru.otus.basicarchitecture.wizardcache + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import ru.otus.basicarchitecture.dto.WizardUserData + +class WizardCacheImpl : WizardCache { + +// private var userData = WizardUserData() + + private var _data = WizardUserData() + private val data = MutableLiveData(_data) + +// override fun getUserData(): WizardUserData { +// Log.e("AAA", "CURRENT CACHE: " + _data.toString()) +// return userData +// } + + override fun getData(): LiveData { + Log.e("AAA", "CURRENT CACHE: " + _data.toString()) + return data + } + + override fun updateMainData(name: String, surname: String, dateOfBirth: String) { + +// userData = userData.copy(name = name, surname = surname, dateOfBirth = dateOfBirth) + + + _data = _data.copy(name = name, surname = surname, dateOfBirth = dateOfBirth) + data.value = _data + Log.e("AAA", "CACHE: " + _data.toString()) + } + + override fun updateAddress(country: String, city: String, address: String) { + +// userData = userData.copy(country =country, city = city, address = address) + + _data = _data.copy(country =country, city = city, address = address) + data.value = _data + Log.e("AAA", "CACHE: " + _data.toString()) + } + + override fun updateInterests(interests: Set) { + + +// userData = userData.copy(interests = interests) + + _data = _data.copy(interests = interests) + data.value = _data + Log.e("AAA", "CACHE: " + _data.toString()) + } +} \ No newline at end of file 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 0000000000000000000000000000000000000000..0efbb7ae5f18e60baa55e4d1398332ec27573e4a GIT binary patch literal 43356 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 literal 0 HcmV?d00001 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 @@ + + + + + + + + + +