diff --git a/app/build.gradle b/app/build.gradle
index 9c99d98..672dd86 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,20 +1,28 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
+ id 'org.jetbrains.kotlin.kapt'
+ id 'com.google.dagger.hilt.android'
+ id("org.jetbrains.kotlin.plugin.serialization")
}
android {
namespace 'ru.otus.basicarchitecture'
- compileSdk 33
+ compileSdk 34
defaultConfig {
applicationId "ru.otus.basicarchitecture"
- minSdk 24
+ minSdk 26
targetSdk 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+
+ Properties properties = new Properties()
+ properties.load(project.rootProject.file("local.properties").newDataInputStream())
+ resValue "string", "dadata_api_key", properties.getProperty("dadata.api.key", "")
+
}
buildTypes {
@@ -30,14 +38,29 @@ android {
kotlinOptions {
jvmTarget = '1.8'
}
+ buildFeatures {
+ viewBinding true
+ }
}
dependencies {
- implementation 'androidx.core:core-ktx:1.8.0'
+ implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+ implementation 'com.google.dagger:hilt-android:2.50'
+ implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
+ implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
+ kapt 'com.google.dagger:hilt-compiler:2.50'
+ implementation("com.squareup.okhttp3:okhttp:4.12.0")
+ implementation("com.squareup.retrofit2:retrofit:2.9.0")
+ implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.1")
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.1")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
+
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
diff --git a/app/src/androidTest/java/ru/otus/basicarchitecture/ExampleInstrumentedTest.kt b/app/src/androidTest/java/ru/otus/basicarchitecture/ExampleInstrumentedTest.kt
index a987f13..0a180ff 100644
--- a/app/src/androidTest/java/ru/otus/basicarchitecture/ExampleInstrumentedTest.kt
+++ b/app/src/androidTest/java/ru/otus/basicarchitecture/ExampleInstrumentedTest.kt
@@ -1,13 +1,11 @@
package ru.otus.basicarchitecture
-import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
-
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
-import org.junit.Assert.*
-
/**
* Instrumented test, which will execute on an Android device.
*
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1e81fea..fc657f6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,9 +1,11 @@
+
+ tools:targetApi="31"
+ android:name=".BasicArchitectureApplication">
+ 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..85a6d42 100644
--- a/app/src/main/java/ru/otus/basicarchitecture/MainActivity.kt
+++ b/app/src/main/java/ru/otus/basicarchitecture/MainActivity.kt
@@ -1,8 +1,10 @@
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)
diff --git a/app/src/main/java/ru/otus/basicarchitecture/WizardCache.kt b/app/src/main/java/ru/otus/basicarchitecture/WizardCache.kt
new file mode 100644
index 0000000..3a85a39
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/WizardCache.kt
@@ -0,0 +1,15 @@
+package ru.otus.basicarchitecture
+
+import dagger.hilt.android.scopes.ActivityRetainedScoped
+import java.time.LocalDate
+import javax.inject.Inject
+
+@ActivityRetainedScoped
+class WizardCache @Inject constructor() {
+ var firstName: String = ""
+ var lastName: String? = ""
+ var birthDate: LocalDate = LocalDate.now().minusYears(18)
+ var address: String = ""
+ var interests: Set = emptySet()
+ var selectedInterests: Set = emptySet()
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/address/AddressFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/address/AddressFragment.kt
new file mode 100644
index 0000000..8ac368e
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/address/AddressFragment.kt
@@ -0,0 +1,80 @@
+package ru.otus.basicarchitecture.address
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.isVisible
+import androidx.core.widget.addTextChangedListener
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.distinctUntilChanged
+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.address.suggestions.SuggestionsAdapter
+import ru.otus.basicarchitecture.databinding.AddressFragmentBinding
+import ru.otus.basicarchitecture.interests.InterestsViewModel
+
+@AndroidEntryPoint
+class AddressFragment : Fragment(R.layout.address_fragment) {
+
+ private lateinit var binding: AddressFragmentBinding
+ private val viewModel: AddressViewModel by viewModels()
+ private val adapter: SuggestionsAdapter = SuggestionsAdapter(
+ onItemClicked = {
+ binding.addressField.setText(it)
+ }
+ )
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = AddressFragmentBinding.inflate(inflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ binding.suggestionsContent.suggestions.adapter = adapter
+ binding.addressField.addTextChangedListener {
+ viewModel.loadSuggestions(input = it?.toString() ?: "")
+ }
+ viewModel.suggestionsGroupState.observe(viewLifecycleOwner) { state ->
+ when (state) {
+ AddressViewModel.SuggestionsGroupState.Content -> {
+ binding.suggestionsLoading.loadingGroup.isVisible = false
+ binding.suggestionsContent.contentGroup.isVisible = true
+ binding.suggestionsError.errorGroup.isVisible = false
+ }
+ AddressViewModel.SuggestionsGroupState.Loading -> {
+ binding.suggestionsLoading.loadingGroup.isVisible = true
+ binding.suggestionsContent.contentGroup.isVisible = false
+ binding.suggestionsError.errorGroup.isVisible = false
+ }
+ AddressViewModel.SuggestionsGroupState.Error -> {
+ binding.suggestionsLoading.loadingGroup.isVisible = false
+ binding.suggestionsContent.contentGroup.isVisible = false
+ binding.suggestionsError.errorGroup.isVisible = true
+ }
+ AddressViewModel.SuggestionsGroupState.NotSet -> {
+ binding.suggestionsLoading.loadingGroup.isVisible = false
+ binding.suggestionsContent.contentGroup.isVisible = false
+ binding.suggestionsError.errorGroup.isVisible = false
+ }
+ }
+ }
+ viewModel.suggestionsState.observe(viewLifecycleOwner) {
+ adapter.submitList(it)
+ }
+ viewModel.fillFieldsFromCache(binding)
+ binding.nextButton.setOnClickListener {
+ viewModel.saveFieldsToCache(binding)
+ findNavController().navigate(R.id.addressNext)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/address/AddressUseCase.kt b/app/src/main/java/ru/otus/basicarchitecture/address/AddressUseCase.kt
new file mode 100644
index 0000000..4dc0ead
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/address/AddressUseCase.kt
@@ -0,0 +1,19 @@
+package ru.otus.basicarchitecture.address
+
+import dagger.hilt.android.scopes.ViewModelScoped
+import ru.otus.basicarchitecture.networkCall
+import ru.otus.basicarchitecture.service.DaDataService
+import ru.otus.basicarchitecture.service.dto.DaDataSuggestionsRequest
+import javax.inject.Inject
+
+@ViewModelScoped
+class AddressUseCase @Inject constructor() {
+
+ @Inject
+ lateinit var daDataService: DaDataService
+
+ suspend fun getSuggestions(input: String) = networkCall {
+ daDataService.getSuggestions(DaDataSuggestionsRequest(query = input))
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/address/AddressViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/address/AddressViewModel.kt
new file mode 100644
index 0000000..e1dea99
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/address/AddressViewModel.kt
@@ -0,0 +1,76 @@
+package ru.otus.basicarchitecture.address
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import ru.otus.basicarchitecture.WizardCache
+import ru.otus.basicarchitecture.address.suggestions.SuggestionsItem
+import ru.otus.basicarchitecture.databinding.AddressFragmentBinding
+import javax.inject.Inject
+
+@HiltViewModel
+class AddressViewModel @Inject constructor(
+ private val useCase: AddressUseCase,
+ private val cache: WizardCache
+) : ViewModel() {
+
+ private val mSuggestionsGroupState =
+ MutableLiveData(SuggestionsGroupState.NotSet)
+ val suggestionsGroupState: LiveData get() = mSuggestionsGroupState
+
+ private val mSuggestionsState = MutableLiveData>()
+ val suggestionsState: LiveData> get() = mSuggestionsState
+
+ private var loadingSuggestionsTask: Job = Job()
+
+ fun loadSuggestions(input: String) {
+ loadingSuggestionsTask.cancel()
+ loadingSuggestionsTask = viewModelScope.launch {
+ mSuggestionsGroupState.value = SuggestionsGroupState.Loading
+ try {
+ withContext(Dispatchers.IO) { useCase.getSuggestions(input) }
+ .takeIf { it.isSuccess }
+ ?.let {
+ mSuggestionsState.value =
+ it.getOrNull()
+ ?.suggestions
+ ?.filter { s -> s.value != input }
+ ?.mapNotNull { s -> s.value?.let { v -> SuggestionsItem(v) } }
+ ?: emptyList()
+ mSuggestionsGroupState.value = SuggestionsGroupState.Content
+ } ?: let {
+ mSuggestionsGroupState.value = SuggestionsGroupState.Error
+ }
+ } catch (t: Throwable) {
+ mSuggestionsGroupState.value = SuggestionsGroupState.Error
+ }
+ }
+ }
+
+ fun fillFieldsFromCache(binding: AddressFragmentBinding) {
+ binding.addressField.setText(cache.address)
+ }
+
+ fun saveFieldsToCache(binding: AddressFragmentBinding) {
+ cache.address = binding.addressField.text?.toString() ?: ""
+ }
+
+ sealed class SuggestionsGroupState {
+
+ data object NotSet: SuggestionsGroupState()
+
+ data object Loading: SuggestionsGroupState()
+
+ data object Content: SuggestionsGroupState()
+
+ data object Error: SuggestionsGroupState()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/address/suggestions/SuggestionsAdapter.kt b/app/src/main/java/ru/otus/basicarchitecture/address/suggestions/SuggestionsAdapter.kt
new file mode 100644
index 0000000..9dc3453
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/address/suggestions/SuggestionsAdapter.kt
@@ -0,0 +1,21 @@
+package ru.otus.basicarchitecture.address.suggestions
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.ListAdapter
+import ru.otus.basicarchitecture.R
+
+class SuggestionsAdapter(
+ private val onItemClicked: (String) -> Unit
+) : ListAdapter(SuggestionsItemCallback()) {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SuggestionsItemViewHolder =
+ SuggestionsItemViewHolder(
+ view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.suggestion_item, parent, false),
+ onItemClicked = onItemClicked)
+
+ override fun onBindViewHolder(holder: SuggestionsItemViewHolder, position: Int) =
+ holder.bind(getItem(position))
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/address/suggestions/SuggestionsItem.kt b/app/src/main/java/ru/otus/basicarchitecture/address/suggestions/SuggestionsItem.kt
new file mode 100644
index 0000000..20912dc
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/address/suggestions/SuggestionsItem.kt
@@ -0,0 +1,5 @@
+package ru.otus.basicarchitecture.address.suggestions
+
+data class SuggestionsItem(
+ val value: String
+)
diff --git a/app/src/main/java/ru/otus/basicarchitecture/address/suggestions/SuggestionsItemCallback.kt b/app/src/main/java/ru/otus/basicarchitecture/address/suggestions/SuggestionsItemCallback.kt
new file mode 100644
index 0000000..7064ffb
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/address/suggestions/SuggestionsItemCallback.kt
@@ -0,0 +1,10 @@
+package ru.otus.basicarchitecture.address.suggestions
+
+import androidx.recyclerview.widget.DiffUtil
+
+class SuggestionsItemCallback : DiffUtil.ItemCallback() {
+
+ override fun areItemsTheSame(p0: SuggestionsItem, p1: SuggestionsItem): Boolean = false
+
+ override fun areContentsTheSame(p0: SuggestionsItem, p1: SuggestionsItem): Boolean = false
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/address/suggestions/SuggestionsItemViewHolder.kt b/app/src/main/java/ru/otus/basicarchitecture/address/suggestions/SuggestionsItemViewHolder.kt
new file mode 100644
index 0000000..d2b48c4
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/address/suggestions/SuggestionsItemViewHolder.kt
@@ -0,0 +1,22 @@
+package ru.otus.basicarchitecture.address.suggestions
+
+import android.view.View
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import ru.otus.basicarchitecture.R
+
+class SuggestionsItemViewHolder(
+ view: View,
+ private val onItemClicked: (String) -> Unit = {}
+) : RecyclerView.ViewHolder(view) {
+
+ private val textView: TextView by lazy { itemView.findViewById(R.id.value) }
+
+ fun bind(item: SuggestionsItem) {
+ textView.text = item.value
+ itemView.setOnClickListener {
+ onItemClicked(item.value)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/application.kt b/app/src/main/java/ru/otus/basicarchitecture/application.kt
new file mode 100644
index 0000000..6b6cf64
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/application.kt
@@ -0,0 +1,33 @@
+package ru.otus.basicarchitecture
+
+import android.app.Application
+import android.content.Context
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.HiltAndroidApp
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import ru.otus.basicarchitecture.service.DaDataService
+import ru.otus.basicarchitecture.service.InterestsService
+import ru.otus.basicarchitecture.service.impl.DaDataServiceImpl
+import ru.otus.basicarchitecture.service.impl.InterestsServiceStubImpl
+import javax.inject.Singleton
+
+@HiltAndroidApp
+class BasicArchitectureApplication : Application()
+
+@Module
+@InstallIn(SingletonComponent::class)
+class Module {
+
+ @Provides
+ @Singleton
+ fun interestsService(): InterestsService = InterestsServiceStubImpl()
+
+ @Provides
+ @Singleton
+ fun daDataService(@ApplicationContext context: Context): DaDataService =
+ DaDataServiceImpl.create(context.getString(R.string.dadata_api_key))
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/helpers.kt b/app/src/main/java/ru/otus/basicarchitecture/helpers.kt
new file mode 100644
index 0000000..77ce7fa
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/helpers.kt
@@ -0,0 +1,20 @@
+package ru.otus.basicarchitecture
+
+import android.text.Editable
+import retrofit2.Response
+import java.io.IOException
+import java.time.LocalDate
+import java.time.format.DateTimeFormatter
+
+fun LocalDate.toBrithDateString(): String =
+ format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))
+
+fun String.toBirthDate() : LocalDate =
+ LocalDate.parse(this, DateTimeFormatter.ofPattern("dd.MM.yyyy"))
+
+fun Editable.toBirthDate() : LocalDate = toString().toBirthDate()
+
+suspend inline fun networkCall(crossinline block: suspend () -> Response): Result =
+ runCatching {
+ block().body() ?: throw IOException("Network error")
+ }
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/interests/InterestsFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/interests/InterestsFragment.kt
new file mode 100644
index 0000000..b935135
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/interests/InterestsFragment.kt
@@ -0,0 +1,86 @@
+package ru.otus.basicarchitecture.interests
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.forEach
+import androidx.core.view.isVisible
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.distinctUntilChanged
+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.InterestsFragmentBinding
+
+@AndroidEntryPoint
+class InterestsFragment : Fragment(R.layout.interests_fragment_content) {
+
+ private lateinit var binding: InterestsFragmentBinding
+ private val viewModel: InterestsViewModel by viewModels()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = InterestsFragmentBinding.inflate(inflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ viewModel.interestsGroupState.observe(viewLifecycleOwner) { state ->
+ when (state) {
+ is InterestsViewModel.InterestsGroupState.Content -> {
+ binding.interestsLoading.loadingGroup.isVisible = false
+ binding.interestsContent.contentGroup.isVisible = true
+ binding.interestsError.errorGroup.isVisible = false
+ }
+ InterestsViewModel.InterestsGroupState.Loading -> {
+ binding.interestsLoading.loadingGroup.isVisible = true
+ binding.interestsContent.contentGroup.isVisible = false
+ binding.interestsError.errorGroup.isVisible = false
+ }
+ InterestsViewModel.InterestsGroupState.Error -> {
+ binding.interestsLoading.loadingGroup.isVisible = false
+ binding.interestsContent.contentGroup.isVisible = false
+ binding.interestsError.errorGroup.isVisible = true
+ }
+ InterestsViewModel.InterestsGroupState.NotSet -> {
+ binding.interestsLoading.loadingGroup.isVisible = false
+ binding.interestsContent.contentGroup.isVisible = false
+ binding.interestsError.errorGroup.isVisible = false
+ }
+ }
+ }
+ viewModel.interestsState.distinctUntilChanged().observe(viewLifecycleOwner) { interests ->
+ interests.forEach { tag ->
+ Chip(binding.interestsContent.tags.context).apply {
+ text = tag
+ isClickable = true
+ isCheckable = true
+ setOnCheckedChangeListener { chip, state ->
+ viewModel.processInterestClick(chip.text.toString(), state)
+ }
+ binding.interestsContent.tags.addView(this)
+ }
+ }
+ }
+ viewModel.selectedInterestsState.distinctUntilChanged().observe(viewLifecycleOwner) { selectedInterests ->
+ binding.interestsContent.tags.forEach { tag ->
+ (tag as? Chip)
+ ?.takeIf { (it.isChecked xor selectedInterests.contains(it.text)) }
+ ?.apply { toggle() }
+ }
+ }
+ viewModel.fillInterestsFromCache()
+ binding.interestsContent.nextButton.setOnClickListener {
+ viewModel.saveInterestsToCache()
+ findNavController().navigate(R.id.interestsNext)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/interests/InterestsUseCase.kt b/app/src/main/java/ru/otus/basicarchitecture/interests/InterestsUseCase.kt
new file mode 100644
index 0000000..6f763a7
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/interests/InterestsUseCase.kt
@@ -0,0 +1,15 @@
+package ru.otus.basicarchitecture.interests
+
+import dagger.hilt.android.scopes.ViewModelScoped
+import ru.otus.basicarchitecture.service.InterestsService
+import javax.inject.Inject
+
+@ViewModelScoped
+class InterestsUseCase @Inject constructor() {
+
+ @Inject
+ lateinit var interestsService: InterestsService
+
+ suspend fun getAvailableInterests(): Set = interestsService.getAvailableInterests()
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/interests/InterestsViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/interests/InterestsViewModel.kt
new file mode 100644
index 0000000..4179447
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/interests/InterestsViewModel.kt
@@ -0,0 +1,77 @@
+package ru.otus.basicarchitecture.interests
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.google.android.material.chip.Chip
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import ru.otus.basicarchitecture.WizardCache
+import javax.inject.Inject
+
+@HiltViewModel
+class InterestsViewModel @Inject constructor(
+ private val useCase: InterestsUseCase,
+ private val cache: WizardCache
+) : ViewModel() {
+
+ private val mInterestsGroupState = MutableLiveData(InterestsGroupState.NotSet)
+ val interestsGroupState: LiveData get() = mInterestsGroupState
+
+ private val mInterestsState = MutableLiveData>()
+ val interestsState: LiveData> get() = mInterestsState
+
+ private val mSelectedInterestsState = MutableLiveData>()
+ val selectedInterestsState: LiveData> get() = mSelectedInterestsState
+
+ private fun loadAvailableInterests() =
+ viewModelScope.launch {
+ mInterestsGroupState.value = InterestsGroupState.Loading
+ try {
+ cache.interests = withContext(Dispatchers.IO) {
+ useCase.getAvailableInterests()
+ }
+ mInterestsState.value = cache.interests
+ mInterestsGroupState.value = InterestsGroupState.Content
+ } catch (t: Throwable) {
+ mInterestsGroupState.value = InterestsGroupState.Error
+ }
+ }
+
+ fun fillInterestsFromCache() {
+ cache.interests
+ .takeIf { it.isEmpty() }
+ ?.let { loadAvailableInterests() }
+ ?: let {
+ mInterestsState.value = cache.interests
+ mInterestsGroupState.value = InterestsGroupState.Content
+ }
+ mSelectedInterestsState.value = cache.selectedInterests
+ }
+
+ fun saveInterestsToCache() {
+ cache.selectedInterests = mSelectedInterestsState.value ?: emptySet()
+ }
+
+ fun processInterestClick(name: String, state: Boolean) =
+ if (state) {
+ mSelectedInterestsState.value = (mSelectedInterestsState.value ?: emptySet()) + name
+ } else {
+ mSelectedInterestsState.value = (mSelectedInterestsState.value ?: emptySet()) - name
+ }
+
+ sealed class InterestsGroupState {
+
+ data object NotSet: InterestsGroupState()
+
+ data object Loading: InterestsGroupState()
+
+ data object Content: InterestsGroupState()
+
+ data object Error: InterestsGroupState()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/personal/PersonalDataFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/personal/PersonalDataFragment.kt
new file mode 100644
index 0000000..7b79bf4
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/personal/PersonalDataFragment.kt
@@ -0,0 +1,45 @@
+package ru.otus.basicarchitecture.personal
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.widget.addTextChangedListener
+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.PersonalDataFragmentBinding
+
+@AndroidEntryPoint
+class PersonalDataFragment : Fragment(R.layout.personal_data_fragment) {
+
+ private lateinit var binding: PersonalDataFragmentBinding
+ private val viewModel: PersonalDataViewModel by viewModels()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = PersonalDataFragmentBinding.inflate(inflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ viewModel.nextButtonState.observe(viewLifecycleOwner) {
+ binding.nextButton.isEnabled = it
+ }
+ binding.dateField.addTextChangedListener {
+ viewModel.updateNextButtonStatus(it?.toString())
+ }
+ viewModel.fillFieldsFromCache(binding)
+ binding.nextButton.setOnClickListener {
+ viewModel.saveFieldsToCache(binding)
+ findNavController().navigate(R.id.personalDataNext)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/personal/PersonalDataUseCase.kt b/app/src/main/java/ru/otus/basicarchitecture/personal/PersonalDataUseCase.kt
new file mode 100644
index 0000000..fade996
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/personal/PersonalDataUseCase.kt
@@ -0,0 +1,13 @@
+package ru.otus.basicarchitecture.personal
+
+import dagger.hilt.android.scopes.ViewModelScoped
+import java.time.LocalDate
+import javax.inject.Inject
+
+@ViewModelScoped
+class PersonalDataUseCase @Inject constructor() {
+
+ fun isBirthDateValid(date: LocalDate) =
+ !date.isAfter(LocalDate.now().minusYears(18))
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/personal/PersonalDataViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/personal/PersonalDataViewModel.kt
new file mode 100644
index 0000000..8f05600
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/personal/PersonalDataViewModel.kt
@@ -0,0 +1,50 @@
+package ru.otus.basicarchitecture.personal
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import ru.otus.basicarchitecture.WizardCache
+import ru.otus.basicarchitecture.databinding.PersonalDataFragmentBinding
+import ru.otus.basicarchitecture.toBirthDate
+import ru.otus.basicarchitecture.toBrithDateString
+import java.time.format.DateTimeParseException
+import javax.inject.Inject
+
+@HiltViewModel
+class PersonalDataViewModel @Inject constructor(
+ private val useCase: PersonalDataUseCase,
+ private val cache: WizardCache
+) : ViewModel() {
+
+ private val mNextButtonState = MutableLiveData(false)
+ val nextButtonState: LiveData get() = mNextButtonState
+
+ fun updateNextButtonStatus(date: String?) {
+ try {
+ mNextButtonState.value =
+ date?.let { useCase.isBirthDateValid(it.toBirthDate())
+ } ?: false
+ } catch (e: DateTimeParseException) {
+ mNextButtonState.value = false
+ }
+ }
+
+ fun fillFieldsFromCache(binding: PersonalDataFragmentBinding) {
+ binding.firstNameField.setText(cache.firstName)
+ binding.lastNameField.setText(cache.lastName)
+ binding.dateField.setText(
+ cache.birthDate.toBrithDateString())
+ }
+
+ fun saveFieldsToCache(binding: PersonalDataFragmentBinding) {
+ cache.firstName = binding.firstNameField.text?.toString() ?: ""
+ cache.lastName = binding.lastNameField.text?.toString() ?: ""
+ try {
+ binding.dateField.text?.apply {
+ cache.birthDate = toBirthDate()
+ }
+ } catch (_: DateTimeParseException) {}
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/service/DaDataService.kt b/app/src/main/java/ru/otus/basicarchitecture/service/DaDataService.kt
new file mode 100644
index 0000000..048eee0
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/service/DaDataService.kt
@@ -0,0 +1,11 @@
+package ru.otus.basicarchitecture.service
+
+import retrofit2.Response
+import ru.otus.basicarchitecture.service.dto.DaDataSuggestionsRequest
+import ru.otus.basicarchitecture.service.dto.DaDataSuggestionsResponse
+
+interface DaDataService {
+
+ suspend fun getSuggestions(body: DaDataSuggestionsRequest): Response
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/service/InterestsService.kt b/app/src/main/java/ru/otus/basicarchitecture/service/InterestsService.kt
new file mode 100644
index 0000000..ebda767
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/service/InterestsService.kt
@@ -0,0 +1,7 @@
+package ru.otus.basicarchitecture.service
+
+interface InterestsService {
+
+ suspend fun getAvailableInterests(): Set
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/service/dto/DaDataSuggestionsRequest.kt b/app/src/main/java/ru/otus/basicarchitecture/service/dto/DaDataSuggestionsRequest.kt
new file mode 100644
index 0000000..838793a
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/service/dto/DaDataSuggestionsRequest.kt
@@ -0,0 +1,8 @@
+package ru.otus.basicarchitecture.service.dto
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class DaDataSuggestionsRequest(
+ val query: String
+)
diff --git a/app/src/main/java/ru/otus/basicarchitecture/service/dto/DaDataSuggestionsResponse.kt b/app/src/main/java/ru/otus/basicarchitecture/service/dto/DaDataSuggestionsResponse.kt
new file mode 100644
index 0000000..6776cd5
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/service/dto/DaDataSuggestionsResponse.kt
@@ -0,0 +1,8 @@
+package ru.otus.basicarchitecture.service.dto
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class DaDataSuggestionsResponse(
+ val suggestions: List?
+)
diff --git a/app/src/main/java/ru/otus/basicarchitecture/service/dto/Suggestion.kt b/app/src/main/java/ru/otus/basicarchitecture/service/dto/Suggestion.kt
new file mode 100644
index 0000000..a2b873b
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/service/dto/Suggestion.kt
@@ -0,0 +1,8 @@
+package ru.otus.basicarchitecture.service.dto
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class Suggestion(
+ val value: String?
+)
diff --git a/app/src/main/java/ru/otus/basicarchitecture/service/impl/DaDataServiceImpl.kt b/app/src/main/java/ru/otus/basicarchitecture/service/impl/DaDataServiceImpl.kt
new file mode 100644
index 0000000..89eb41f
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/service/impl/DaDataServiceImpl.kt
@@ -0,0 +1,62 @@
+package ru.otus.basicarchitecture.service.impl
+
+import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
+import kotlinx.serialization.json.Json
+import okhttp3.Interceptor
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.OkHttpClient
+import retrofit2.Call
+import retrofit2.Response
+import retrofit2.Retrofit
+import retrofit2.http.Body
+import retrofit2.http.Header
+import retrofit2.http.Headers
+import retrofit2.http.POST
+import ru.otus.basicarchitecture.R
+import ru.otus.basicarchitecture.service.DaDataService
+import ru.otus.basicarchitecture.service.dto.DaDataSuggestionsRequest
+import ru.otus.basicarchitecture.service.dto.DaDataSuggestionsResponse
+import java.util.concurrent.TimeUnit
+
+interface DaDataServiceImpl : DaDataService {
+
+ @POST("suggestions/api/4_1/rs/suggest/address")
+ override suspend fun getSuggestions(
+ @Body body: DaDataSuggestionsRequest
+ ): Response
+
+ companion object {
+
+ fun create(apiKey: String): DaDataService {
+ val okHttp = OkHttpClient.Builder()
+ .connectTimeout(60, TimeUnit.SECONDS)
+ .readTimeout(60, TimeUnit.SECONDS)
+ .writeTimeout(60, TimeUnit.SECONDS)
+ .addInterceptor(Interceptor { chain ->
+ chain.proceed(
+ chain.request().newBuilder()
+ .header("Content-Type", "application/json")
+ .header("Accept", "application/json")
+ .header(
+ "Authorization",
+ "Token $apiKey")
+ .build())
+ })
+ .build()
+
+ val json = Json {
+ coerceInputValues = true
+ ignoreUnknownKeys = true
+ }
+
+ val retrofit = Retrofit.Builder()
+ .client(okHttp)
+ .baseUrl("http://suggestions.dadata.ru/")
+ .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
+ .build()
+
+ return retrofit.create(DaDataServiceImpl::class.java)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/service/impl/InterestsServiceStubImpl.kt b/app/src/main/java/ru/otus/basicarchitecture/service/impl/InterestsServiceStubImpl.kt
new file mode 100644
index 0000000..2d38b38
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/service/impl/InterestsServiceStubImpl.kt
@@ -0,0 +1,10 @@
+package ru.otus.basicarchitecture.service.impl
+
+import ru.otus.basicarchitecture.service.InterestsService
+
+class InterestsServiceStubImpl : InterestsService {
+
+ override suspend fun getAvailableInterests(): Set = setOf(
+ "Cooking", "Hiking", "Programming", "Travelling", "Sleeping"
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/summary/SummaryFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/summary/SummaryFragment.kt
new file mode 100644
index 0000000..56ba174
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/summary/SummaryFragment.kt
@@ -0,0 +1,45 @@
+package ru.otus.basicarchitecture.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.R
+import ru.otus.basicarchitecture.databinding.SummaryFragmentBinding
+
+@AndroidEntryPoint
+class SummaryFragment : Fragment(R.layout.summary_fragment) {
+
+ private lateinit var binding: SummaryFragmentBinding
+ private val viewModel: SummaryViewModel by viewModels()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = SummaryFragmentBinding.inflate(inflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ viewModel.selectedInterestsState.observe(viewLifecycleOwner) { selectedInterests ->
+ binding.interestsTitle.visibility =
+ if (selectedInterests.isEmpty()) View.GONE else View.VISIBLE
+ selectedInterests.forEach { tag ->
+ Chip(binding.interests.context).apply {
+ text = tag
+ isClickable = false
+ binding.interests.addView(this)
+ }
+ }
+ }
+ viewModel.fillFieldsFromCache(binding)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/summary/SummaryUseCase.kt b/app/src/main/java/ru/otus/basicarchitecture/summary/SummaryUseCase.kt
new file mode 100644
index 0000000..bdca13f
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/summary/SummaryUseCase.kt
@@ -0,0 +1,8 @@
+package ru.otus.basicarchitecture.summary
+
+import dagger.hilt.android.scopes.ViewModelScoped
+import javax.inject.Inject
+
+@ViewModelScoped
+class SummaryUseCase @Inject constructor() {
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/summary/SummaryViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/summary/SummaryViewModel.kt
new file mode 100644
index 0000000..2934acc
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/summary/SummaryViewModel.kt
@@ -0,0 +1,29 @@
+package ru.otus.basicarchitecture.summary
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import ru.otus.basicarchitecture.WizardCache
+import ru.otus.basicarchitecture.databinding.SummaryFragmentBinding
+import ru.otus.basicarchitecture.toBrithDateString
+import javax.inject.Inject
+
+@HiltViewModel
+class SummaryViewModel @Inject constructor(
+ private val useCase: SummaryUseCase,
+ private val cache: WizardCache
+) : ViewModel() {
+
+ private val mSelectedInterestsState = MutableLiveData>()
+ val selectedInterestsState: LiveData> get() = mSelectedInterestsState
+
+ fun fillFieldsFromCache(binding: SummaryFragmentBinding) {
+ binding.firstName.text = cache.firstName
+ binding.lastName.text = cache.lastName
+ binding.birthDate.text = cache.birthDate.toBrithDateString()
+ binding.address.text = cache.address
+ mSelectedInterestsState.value = cache.selectedInterests
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 0b15a20..bbbca8d 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -6,4 +6,12 @@
android:layout_height="match_parent"
tools:context=".MainActivity">
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/address_fragment.xml b/app/src/main/res/layout/address_fragment.xml
new file mode 100644
index 0000000..02d407a
--- /dev/null
+++ b/app/src/main/res/layout/address_fragment.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/address_suggestions_content.xml b/app/src/main/res/layout/address_suggestions_content.xml
new file mode 100644
index 0000000..e55ecea
--- /dev/null
+++ b/app/src/main/res/layout/address_suggestions_content.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/error.xml b/app/src/main/res/layout/error.xml
new file mode 100644
index 0000000..f173f68
--- /dev/null
+++ b/app/src/main/res/layout/error.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/interests_fragment.xml b/app/src/main/res/layout/interests_fragment.xml
new file mode 100644
index 0000000..435c31b
--- /dev/null
+++ b/app/src/main/res/layout/interests_fragment.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/interests_fragment_content.xml b/app/src/main/res/layout/interests_fragment_content.xml
new file mode 100644
index 0000000..1f0e03c
--- /dev/null
+++ b/app/src/main/res/layout/interests_fragment_content.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/loading.xml b/app/src/main/res/layout/loading.xml
new file mode 100644
index 0000000..5ccbd57
--- /dev/null
+++ b/app/src/main/res/layout/loading.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/personal_data_fragment.xml b/app/src/main/res/layout/personal_data_fragment.xml
new file mode 100644
index 0000000..d6dd06f
--- /dev/null
+++ b/app/src/main/res/layout/personal_data_fragment.xml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/suggestion_item.xml b/app/src/main/res/layout/suggestion_item.xml
new file mode 100644
index 0000000..bf3d012
--- /dev/null
+++ b/app/src/main/res/layout/suggestion_item.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/summary_fragment.xml b/app/src/main/res/layout/summary_fragment.xml
new file mode 100644
index 0000000..74e5d22
--- /dev/null
+++ b/app/src/main/res/layout/summary_fragment.xml
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/navigation/navigation.xml b/app/src/main/res/navigation/navigation.xml
new file mode 100644
index 0000000..1564516
--- /dev/null
+++ b/app/src/main/res/navigation/navigation.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
index bbaa36f..b3d6383 100644
--- a/app/src/main/res/values-night/themes.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -1,4 +1,4 @@
-
+