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 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/user_interests_layout.xml b/app/src/main/res/layout/user_interests_layout.xml
new file mode 100644
index 0000000..7acb7a5
--- /dev/null
+++ b/app/src/main/res/layout/user_interests_layout.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/user_summary_layout.xml b/app/src/main/res/layout/user_summary_layout.xml
new file mode 100644
index 0000000..d867282
--- /dev/null
+++ b/app/src/main/res/layout/user_summary_layout.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml
new file mode 100644
index 0000000..86b7f08
--- /dev/null
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..907ed6c
--- /dev/null
+++ b/app/src/main/res/values/attrs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ 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..30d5d2b 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -7,4 +7,9 @@
#FF018786
#FF000000
#FFFFFFFF
+
+ #356ACF
+ #518AF4
+ #8E8E8E
+
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..11da0a8
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,4 @@
+
+
+ 8dp
+
\ 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..51cb29b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,14 @@
BasicArchitecture
+ Next
+ Summary
+ Name
+ Surname
+ Age
+ Country
+ City
+ Address
+ Date of Birth
+ Interests
+ dd.mm.yyyy
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..2e9451d
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 0ab4563..0a1bca2 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,6 +1,6 @@
-
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index c84cccf..729b326 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,6 +1,31 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ ext.versions = [
+ 'fragments': '1.5.7',
+ 'navigation': '2.6.0',
+ 'lifecycle': '2.6.0',
+ 'dagger': '2.45',
+ 'hilt': '2.44'
+
+ ]
+ ext.deps = [
+ 'fragment': "androidx.fragment:fragment-ktx:${versions.fragments}",
+ 'navigation': "androidx.navigation:navigation-fragment-ktx:${versions.navigation}",
+
+ 'viewmodel': "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.lifecycle}",
+ 'livedata': "androidx.lifecycle:lifecycle-livedata-ktx:${versions.lifecycle}",
+
+ 'dagger': "com.google.dagger:dagger:${versions.dagger}",
+ 'dagger_compiler': "com.google.dagger:dagger-compiler:${versions.dagger}",
+
+ 'hilt': "com.google.dagger:hilt-android:${versions.hilt}",
+ 'hilt_compiler': "com.google.dagger:hilt-compiler:${versions.hilt}"
+ ]
+}
+
plugins {
id 'com.android.application' version '8.0.0' apply false
id 'com.android.library' version '8.0.0' apply false
- id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
+ id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
+ id 'com.google.dagger.hilt.android' version '2.44' apply false
}
\ No newline at end of file