diff --git a/app/build.gradle b/app/build.gradle
index 9c99d98..845a887 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,16 +1,32 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
+ id 'kotlin-kapt'
+ id 'dagger.hilt.android.plugin'
}
+
+
+kotlin {
+ jvmToolchain {
+ (System.getenv("JAVA_HOME") ?: "/path/to/jdk11") as File
+ }
+}
+
+Properties properties = new Properties()
+properties.load(project.rootProject.file('local.properties').newDataInputStream())
+
+
+def dDataTokin = properties.getProperty('D_DATA_TOKIN', '')
+
android {
namespace 'ru.otus.basicarchitecture'
- compileSdk 33
+ compileSdk 34
defaultConfig {
applicationId "ru.otus.basicarchitecture"
- minSdk 24
- targetSdk 33
+ minSdk 28
+ targetSdk 34
versionCode 1
versionName "1.0"
@@ -19,9 +35,14 @@ android {
buildTypes {
release {
+ buildConfigField 'String', 'D_DATA_TOKIN', "\"${dDataTokin}\""
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
+ debug {
+ buildConfigField 'String', 'D_DATA_TOKIN', "\"${dDataTokin}\""
+ }
+
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -30,15 +51,40 @@ android {
kotlinOptions {
jvmTarget = '1.8'
}
+
+ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
+ kotlinOptions.jvmTarget = '1.8'
+ }
+
+ buildFeatures {
+ viewBinding true
+ buildConfig = true
+ }
+
}
dependencies {
-
- implementation 'androidx.core:core-ktx:1.8.0'
+ implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+ implementation 'androidx.navigation:navigation-fragment-ktx:2.7.0'
+ testImplementation 'org.mockito:mockito-inline:3.8.0'
+ testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+ implementation 'com.google.dagger:hilt-android:2.47'
+ kapt 'com.google.dagger:hilt-compiler:2.47'
+ kapt("org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.5.0")
+ implementation 'com.google.android.flexbox:flexbox:3.0.0'
+ implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
+ implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
+ implementation 'androidx.databinding:databinding-runtime:8.1.0'
+ implementation "androidx.navigation:navigation-fragment-ktx:2.7.0"
+ implementation "androidx.navigation:navigation-ui-ktx:2.7.0"
+ implementation 'com.squareup.retrofit2:retrofit:2.9.0'
+ implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1e81fea..e974a3d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,9 +1,11 @@
+
+ android:exported="true">
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/CustomArrayAdapter.kt b/app/src/main/java/ru/otus/basicarchitecture/CustomArrayAdapter.kt
new file mode 100644
index 0000000..36b760a
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/CustomArrayAdapter.kt
@@ -0,0 +1,55 @@
+package ru.otus.basicarchitecture
+
+import android.content.Context
+import android.widget.ArrayAdapter
+import android.widget.Filter
+
+class CustomArrayAdapter(
+ context: Context,
+ private val items: List
+) : ArrayAdapter(context, android.R.layout.simple_dropdown_item_1line, items) {
+
+ override fun getFilter(): Filter {
+ return object : Filter() {
+ override fun performFiltering(constraint: CharSequence?): FilterResults {
+ val results = FilterResults()
+
+ if (constraint.isNullOrEmpty()) {
+ results.values = items
+ results.count = items.size
+ } else {
+ val searchText = constraint.toString().lowercase().cleanForSearch()
+ val matchedItems = items.filter {
+ it.lowercase().cleanForSearch().contains(searchText)
+ }
+ results.values = matchedItems
+ results.count = matchedItems.size
+ }
+
+ return results
+ }
+
+ private fun String.cleanForSearch(): String {
+ return this.replace("г ", "")
+ .replace(", ", " ")
+ .replace(" пер", "")
+ .replace(" ул", "")
+ .replace(" д", "")
+ .replace(" б-р", "")
+ .replace(" р-н", "")
+ }
+
+ override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
+ if (results != null && results.count > 0) {
+ notifyDataSetChanged()
+ } else {
+ notifyDataSetInvalidated()
+ }
+ }
+
+ override fun convertResultToString(resultValue: Any?): CharSequence {
+ return resultValue as? String ?: ""
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/ru/otus/basicarchitecture/DaDataRequest.kt b/app/src/main/java/ru/otus/basicarchitecture/DaDataRequest.kt
new file mode 100644
index 0000000..3b49bb1
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/DaDataRequest.kt
@@ -0,0 +1,5 @@
+package ru.otus.basicarchitecture
+
+data class DaDataRequest(
+ val query: String,
+)
diff --git a/app/src/main/java/ru/otus/basicarchitecture/DaDataService.kt b/app/src/main/java/ru/otus/basicarchitecture/DaDataService.kt
new file mode 100644
index 0000000..a0398b8
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/DaDataService.kt
@@ -0,0 +1,31 @@
+package ru.otus.basicarchitecture
+
+import okhttp3.MediaType
+import okhttp3.ResponseBody
+import retrofit2.Response
+import retrofit2.http.Body
+import retrofit2.http.POST
+
+
+interface DaDataService {
+ @POST("suggestions/api/4_1/rs/suggest/address")
+ suspend fun getSuggestions(@Body request: DaDataRequest): Response
+
+}
+
+class FakeDaDataService : DaDataService {
+ var shouldReturnError = false
+
+ override suspend fun getSuggestions(request: DaDataRequest): Response {
+ return if (shouldReturnError) {
+ Response.error(400, "Bad Request".toResponseBody())
+ } else {
+ val fakeSuggestions = listOf(Suggestion("1","2",Data("1","2","3","4","5")))
+ val fakeResponse = SuggestionsResponse(fakeSuggestions)
+ Response.success(fakeResponse)
+ }
+ }
+
+ fun String.toResponseBody(): ResponseBody =
+ ResponseBody.create(MediaType.parse("text/plain"), this)
+}
diff --git a/app/src/main/java/ru/otus/basicarchitecture/MainActivity.kt b/app/src/main/java/ru/otus/basicarchitecture/MainActivity.kt
index 623aba9..2876204 100644
--- a/app/src/main/java/ru/otus/basicarchitecture/MainActivity.kt
+++ b/app/src/main/java/ru/otus/basicarchitecture/MainActivity.kt
@@ -1,11 +1,28 @@
package ru.otus.basicarchitecture
-import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.navigation.fragment.NavHostFragment
+import androidx.navigation.ui.setupActionBarWithNavController
+import dagger.hilt.android.AndroidEntryPoint
+@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
+
+ val navHostFragment =
+ supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
+ val navController = navHostFragment.navController
+ setupActionBarWithNavController(navController)
+ }
+
+ override fun onSupportNavigateUp(): Boolean {
+ val navHostFragment =
+ supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
+ val navController = navHostFragment.navController
+ return navController.navigateUp() || super.onSupportNavigateUp()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/MyApplication.kt b/app/src/main/java/ru/otus/basicarchitecture/MyApplication.kt
new file mode 100644
index 0000000..e078a8e
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/MyApplication.kt
@@ -0,0 +1,7 @@
+package ru.otus.basicarchitecture
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+
+@HiltAndroidApp
+class MyApplication : Application()
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/NetworkModule.kt b/app/src/main/java/ru/otus/basicarchitecture/NetworkModule.kt
new file mode 100644
index 0000000..02c1d1a
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/NetworkModule.kt
@@ -0,0 +1,50 @@
+package ru.otus.basicarchitecture
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import okhttp3.OkHttpClient
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import javax.inject.Singleton
+
+
+private const val BASE_URL = "https://suggestions.dadata.ru/"
+private const val D_DATA_TOKIN = BuildConfig.D_DATA_TOKIN
+
+@Module
+@InstallIn(SingletonComponent::class)
+object NetworkModule {
+
+ @Provides
+ @Singleton
+ fun provideOkHttpClient(): OkHttpClient {
+ return OkHttpClient.Builder()
+ .addInterceptor { chain ->
+ val request = chain.request().newBuilder()
+ .addHeader("Content-Type", "application/json")
+ .addHeader("Accept", "application/json")
+ .addHeader("Authorization", "Token $D_DATA_TOKIN")
+ .build()
+ chain.proceed(request)
+ }
+ .build()
+ }
+
+ @Provides
+ @Singleton
+ fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
+ return Retrofit.Builder()
+ .baseUrl(BASE_URL)
+ .addConverterFactory(GsonConverterFactory.create())
+ .client(okHttpClient)
+ .build()
+ }
+
+ @Provides
+ @Singleton
+ fun provideDaDataService(retrofit: Retrofit): DaDataService {
+ return retrofit.create(DaDataService::class.java)
+ }
+}
diff --git a/app/src/main/java/ru/otus/basicarchitecture/Suggestion.kt b/app/src/main/java/ru/otus/basicarchitecture/Suggestion.kt
new file mode 100644
index 0000000..0895e08
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/Suggestion.kt
@@ -0,0 +1,23 @@
+package ru.otus.basicarchitecture
+
+import com.google.gson.annotations.SerializedName
+
+data class Suggestion(
+ val value: String,
+ @SerializedName("unrestricted_value")
+ val unrestrictedValue: String,
+ val data: Data,
+)
+
+data class Data(
+ @SerializedName("region_kladr_id")
+ val regionKladrId: String,
+ @SerializedName("city_kladr_id")
+ val cityKladrId: String,
+ @SerializedName("kladr_id")
+ val kladrId: String,
+ @SerializedName("geo_lat")
+ val geoLat: String,
+ @SerializedName("geo_lon")
+ val geoLon: String,
+)
diff --git a/app/src/main/java/ru/otus/basicarchitecture/SuggestionsResponse.kt b/app/src/main/java/ru/otus/basicarchitecture/SuggestionsResponse.kt
new file mode 100644
index 0000000..699f09b
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/SuggestionsResponse.kt
@@ -0,0 +1,5 @@
+package ru.otus.basicarchitecture
+
+data class SuggestionsResponse(
+ val suggestions: List
+)
diff --git a/app/src/main/java/ru/otus/basicarchitecture/UserInputAddressViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/UserInputAddressViewModel.kt
new file mode 100644
index 0000000..4f836e0
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/UserInputAddressViewModel.kt
@@ -0,0 +1,79 @@
+package ru.otus.basicarchitecture
+
+import android.util.Log
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class UserInputAddressViewModel @Inject constructor(
+ private val wizardCache: WizardCache,
+ private val daDataService: DaDataService
+) : ViewModel() {
+ val validateState = MutableLiveData()
+ val viewState: MutableLiveData = MutableLiveData(ViewState())
+ val suggestions = MutableLiveData>()
+
+ private val viewModelJob = Job()
+ private val viewModelScope = CoroutineScope(Dispatchers.Main + viewModelJob)
+
+ fun search(query: String) {
+ viewModelScope.launch {
+ try {
+ val response = daDataService.getSuggestions(DaDataRequest(query))
+ if (response.isSuccessful) {
+ val data = response.body()
+ suggestions.value = data?.suggestions
+ Log.d("log----------------", "onResponse: ${data?.suggestions}")
+ } else {
+ Log.d("log----------------", response.toString())
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ Log.d("log----------------", "Exception: $e")
+ }
+ }
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ viewModelJob.cancel()
+ }
+
+
+
+ fun validateAndSaveAddress(): ValidateState {
+ val stats = viewState.value!!
+ val checkFiles = isValidFields(
+ listOf(
+ listOf(stats.address, "Адрес")
+ ))
+
+ val res = if (checkFiles.isNotEmpty()) {
+ ValidateState.LoseFiled(checkFiles)
+ }
+ else {
+ wizardCache.address = stats.address
+ ValidateState.Ok
+ }
+ validateState.value = res
+
+ return res
+
+
+ }
+
+
+ private fun isValidFields(fields: List>): String {
+ for (field in fields)
+ if (field[0].isEmpty()) {
+ return field[1]
+ }
+ return ""
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/UserInputFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/UserInputFragment.kt
new file mode 100644
index 0000000..72ca1c3
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/UserInputFragment.kt
@@ -0,0 +1,86 @@
+package ru.otus.basicarchitecture
+
+import android.os.Bundle
+import android.text.Editable
+import android.text.TextWatcher
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+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.databinding.FragmentUserInputBinding
+
+@AndroidEntryPoint
+class UserInputFragment : Fragment(R.layout.fragment_user_input) {
+ private val viewModel: UserInputViewModel by viewModels()
+ private var _binding: FragmentUserInputBinding? = null
+
+
+ private val binding get() = _binding!!
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentUserInputBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+//
+ binding.dateOfBirthEditText.addTextChangedListener(object : TextWatcher {
+ override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
+ }
+
+ override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
+ binding.nextButton.isEnabled = true
+ if (start != s.length && (s.length == 2 || s.length == 5)) {
+ binding.dateOfBirthEditText.setText("${s}.")
+ binding.dateOfBirthEditText.setSelection(s.length + 1)
+ }
+ }
+
+ override fun afterTextChanged(s: Editable) {
+ viewModel.viewState.value = viewModel.viewState.value!!.copy(dateOfBirth = binding.dateOfBirthEditText.text.toString())
+ }
+ })
+
+ binding.firstNameEditText.addTextChangedListener { text ->
+ viewModel.viewState.value = viewModel.viewState.value!!.copy(firstName = text.toString())
+ }
+ binding.lastNameEditText.addTextChangedListener { text ->
+ viewModel.viewState.value = viewModel.viewState.value!!.copy(lastName = text.toString())
+ }
+
+
+ binding.nextButton.setOnClickListener {
+ viewModel.validateAndSaveFirst()
+ val result = viewModel.validateState.value
+ var outText = ""
+ when (result) {
+ is ValidateState.BedFiled -> outText = "Некорректное поле " + result.filed
+ is ValidateState.LoseFiled -> outText = "Пустое поле " + result.filed
+ ValidateState.Not18 -> {
+ outText = "Нет 18 лет"
+ it.isEnabled = false
+ }
+ ValidateState.Ok -> {
+ findNavController().navigate(R.id.action_userInputFragment_to_userInputFragment2)
+ }
+ null -> outText = "Ошибка"
+ }
+ if (outText.isNotEmpty())
+ Toast.makeText(context, outText, Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
diff --git a/app/src/main/java/ru/otus/basicarchitecture/UserInputFragmentAddress.kt b/app/src/main/java/ru/otus/basicarchitecture/UserInputFragmentAddress.kt
new file mode 100644
index 0000000..682601f
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/UserInputFragmentAddress.kt
@@ -0,0 +1,64 @@
+package ru.otus.basicarchitecture
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+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.databinding.FragmentUserInput2Binding
+
+@AndroidEntryPoint
+class UserInputFragmentAddress : Fragment(R.layout.fragment_user_input2) {
+ private val viewModel: UserInputAddressViewModel by viewModels()
+ private var _binding: FragmentUserInput2Binding? = null
+
+
+ private val binding get() = _binding!!
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentUserInput2Binding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ viewModel.suggestions.observe(viewLifecycleOwner) { suggestions ->
+ val adapter = CustomArrayAdapter(requireContext(), suggestions.map { it.value })
+ binding.myAutoCompleteTextView.setAdapter(adapter)
+ }
+
+ binding.myAutoCompleteTextView.addTextChangedListener { text ->
+ viewModel.search(text.toString())
+ viewModel.viewState.value = viewModel.viewState.value!!.copy(address = text.toString())
+ }
+
+ binding.nextButton2.setOnClickListener {
+ viewModel.validateAndSaveAddress()
+ val result = viewModel.validateState.value
+ var outText = ""
+ when (result) {
+ is ValidateState.LoseFiled -> outText = "Пустое поле " + result.filed
+ ValidateState.Ok -> {
+ findNavController().navigate(R.id.action_userInputFragment2_to_userInputFragment3)
+ }
+ else -> outText = "Ошибка"
+ }
+ if (outText.isNotEmpty())
+ Toast.makeText(context, outText, Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
diff --git a/app/src/main/java/ru/otus/basicarchitecture/UserInputFragmentLast.kt b/app/src/main/java/ru/otus/basicarchitecture/UserInputFragmentLast.kt
new file mode 100644
index 0000000..a0f0d33
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/UserInputFragmentLast.kt
@@ -0,0 +1,65 @@
+package ru.otus.basicarchitecture
+
+import android.graphics.Color
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import com.google.android.flexbox.FlexboxLayout
+import dagger.hilt.android.AndroidEntryPoint
+import ru.otus.basicarchitecture.databinding.FragmentUserInput4Binding
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+@AndroidEntryPoint
+class UserInputFragmentLast : Fragment(R.layout.fragment_user_input4) {
+ private val viewModel: UserInputLastViewModel by viewModels()
+ private var _binding: FragmentUserInput4Binding? = null
+
+
+ private val binding get() = _binding!!
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentUserInput4Binding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ binding.firstNameTextView.text = viewModel.wizardCache.firstName
+ binding.lastNameTextView.text = viewModel.wizardCache.lastName
+ val format = SimpleDateFormat("dd.MM.yyyy", Locale.getDefault())
+ binding.dateOfBirthTextView.text = viewModel.wizardCache.dateOfBirth?.let { format.format(it) }
+ binding.addressTextView.text = viewModel.wizardCache.address
+
+ val flexboxLayout = binding.tagContainer
+ val tags = viewModel.wizardCache.selectedTags
+ tags.forEach { tag ->
+ val textView = TextView(context).apply {
+ text = tag
+ textSize = 32f
+ setTextColor(Color.WHITE)
+ setPadding(30, 18, 30, 18)
+ setBackgroundResource(R.drawable.tag_background2)
+ }
+ val lp = FlexboxLayout.LayoutParams(
+ FlexboxLayout.LayoutParams.WRAP_CONTENT,
+ FlexboxLayout.LayoutParams.WRAP_CONTENT
+ )
+ lp.setMargins(48, 48, 48, 48)
+ flexboxLayout.addView(textView, lp)
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
diff --git a/app/src/main/java/ru/otus/basicarchitecture/UserInputFragmentTags.kt b/app/src/main/java/ru/otus/basicarchitecture/UserInputFragmentTags.kt
new file mode 100644
index 0000000..896d12d
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/UserInputFragmentTags.kt
@@ -0,0 +1,77 @@
+package ru.otus.basicarchitecture
+
+import android.graphics.Color
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.navigation.fragment.findNavController
+import com.google.android.flexbox.FlexboxLayout
+import dagger.hilt.android.AndroidEntryPoint
+import ru.otus.basicarchitecture.databinding.FragmentUserInput3Binding
+
+@AndroidEntryPoint
+class UserInputFragmentTags : Fragment(R.layout.fragment_user_input3) {
+ private val viewModel: UserInputTagsViewModel by viewModels()
+ private var _binding: FragmentUserInput3Binding? = null
+
+
+ private val binding get() = _binding!!
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentUserInput3Binding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val flexboxLayout = binding.tagContainer
+ val tags = listOf("Android", "Kotlin", "Fragment", "Tag", "Cloud")
+ tags.forEach { tag ->
+ val textView = TextView(context).apply {
+ text = tag
+ textSize = 32f
+ setTextColor(Color.WHITE)
+ setPadding(30, 18, 30, 18)
+ setBackgroundResource(R.drawable.tag_background)
+ isSelected = tag in viewModel.wizardCache.selectedTags
+ setOnClickListener {
+ it.isSelected = !it.isSelected
+ if (it.isSelected) {
+ val selectedTags = viewModel.viewState.value!!.selectedTags
+ selectedTags.add(tag)
+ viewModel.viewState.value = viewModel.viewState.value!!.copy(selectedTags = selectedTags)
+ } else {
+ val selectedTags = viewModel.viewState.value!!.selectedTags
+ selectedTags.remove(tag)
+ viewModel.viewState.value = viewModel.viewState.value!!.copy(selectedTags = selectedTags)
+ }
+ }
+ }
+ val lp = FlexboxLayout.LayoutParams(
+ FlexboxLayout.LayoutParams.WRAP_CONTENT,
+ FlexboxLayout.LayoutParams.WRAP_CONTENT
+ )
+ lp.setMargins(48, 48, 48, 48)
+ flexboxLayout.addView(textView, lp)
+ }
+
+
+ binding.nextButton3.setOnClickListener {
+ viewModel.saveTags()
+ findNavController().navigate(R.id.action_userInputFragment3_to_userInputFragment4)
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
diff --git a/app/src/main/java/ru/otus/basicarchitecture/UserInputLastViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/UserInputLastViewModel.kt
new file mode 100644
index 0000000..fb6778a
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/UserInputLastViewModel.kt
@@ -0,0 +1,10 @@
+package ru.otus.basicarchitecture
+
+import androidx.lifecycle.ViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class UserInputLastViewModel @Inject constructor(
+ val wizardCache: WizardCache
+) : ViewModel()
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/UserInputTagsViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/UserInputTagsViewModel.kt
new file mode 100644
index 0000000..889d1b9
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/UserInputTagsViewModel.kt
@@ -0,0 +1,17 @@
+package ru.otus.basicarchitecture
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class UserInputTagsViewModel @Inject constructor(
+ val wizardCache: WizardCache
+) : ViewModel() {
+ val viewState: MutableLiveData = MutableLiveData(ViewState())
+
+ fun saveTags() {
+ wizardCache.selectedTags = viewState.value!!.selectedTags
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/UserInputViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/UserInputViewModel.kt
new file mode 100644
index 0000000..6fdeab2
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/UserInputViewModel.kt
@@ -0,0 +1,78 @@
+package ru.otus.basicarchitecture
+
+import android.icu.util.Calendar
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import javax.inject.Inject
+
+@HiltViewModel
+class UserInputViewModel @Inject constructor(
+ private val wizardCache: WizardCache
+) : ViewModel() {
+ val validateState = MutableLiveData()
+ val viewState: MutableLiveData = MutableLiveData(ViewState())
+
+ fun validateAndSaveFirst() {
+ validateState.value = isValidFirst(viewState.value!!.dateOfBirth,
+ listOf(listOf(viewState.value!!.firstName, "Имя"), listOf(viewState.value!!.lastName, "Фамилия"))
+ )
+ }
+ fun isValidFirst(dateOfBirth: String, fields: List>): ValidateState {
+ val stats = viewState.value!!
+ val checkFiles = isValidFields(fields)
+ if (checkFiles.isNotEmpty())
+ return ValidateState.LoseFiled(checkFiles)
+
+ if (dateOfBirth.isEmpty()) {
+ return ValidateState.LoseFiled("Дата рождения")
+ }
+ val format = SimpleDateFormat("dd.MM.yyyy", Locale.US)
+ try {
+ val date = format.parse(dateOfBirth)
+// val calendar = Calendar.getInstance()
+// calendar.time = date
+// val year = calendar.get(Calendar.YEAR)
+// val month = calendar.get(Calendar.MONTH) + 1
+// val day = calendar.get(Calendar.DAY_OF_MONTH)
+//
+// val currentYear = Calendar.getInstance().get(Calendar.YEAR)
+//
+//
+// if (!(day in 1..31 && month in 1..12 && year in 1900..currentYear)) {
+// return ValidateState.BedFiled("Дата рождения")
+// }
+
+// val cal = Calendar.getInstance()
+// cal.time = date
+// cal.add(Calendar.YEAR, 18)
+// return if (cal.time.before(Date())){
+ return if (2023 - dateOfBirth.split(".")[2].toInt() > 18){
+ wizardCache.firstName = stats.firstName
+ wizardCache.lastName = stats.lastName
+ wizardCache.dateOfBirth = date
+ ValidateState.Ok
+ }
+ else
+ ValidateState.Not18
+
+ } catch (e: ParseException) {
+ e.printStackTrace()
+ }
+
+ return ValidateState.BedFiled("Дата рождения")
+ }
+
+ private fun isValidFields(fields: List>): String {
+ for (field in fields)
+ if (field[0].isEmpty()) {
+ return field[1]
+ }
+ return ""
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/ValidateState.kt b/app/src/main/java/ru/otus/basicarchitecture/ValidateState.kt
new file mode 100644
index 0000000..6e5a7b4
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/ValidateState.kt
@@ -0,0 +1,8 @@
+package ru.otus.basicarchitecture
+
+sealed class ValidateState {
+ object Ok : ValidateState()
+ object Not18 : ValidateState()
+ data class BedFiled(val filed: String) : ValidateState()
+ data class LoseFiled(val filed: String) : ValidateState()
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/otus/basicarchitecture/ViewState.kt b/app/src/main/java/ru/otus/basicarchitecture/ViewState.kt
new file mode 100644
index 0000000..ea0981b
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/ViewState.kt
@@ -0,0 +1,9 @@
+package ru.otus.basicarchitecture
+
+data class ViewState (
+ val firstName: String = "",
+ val lastName: String = "",
+ val dateOfBirth: String = "",
+ val address: String = "",
+ val selectedTags: MutableList = mutableListOf()
+)
\ No newline at end of file
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..f686279
--- /dev/null
+++ b/app/src/main/java/ru/otus/basicarchitecture/WizardCache.kt
@@ -0,0 +1,26 @@
+package ru.otus.basicarchitecture
+
+import dagger.hilt.android.scopes.ActivityRetainedScoped
+import java.util.Date
+import javax.inject.Inject
+
+@ActivityRetainedScoped
+open class WizardCache @Inject constructor() {
+ open var firstName: String? = null
+ open var lastName: String? = null
+ open var dateOfBirth: Date? = null
+ open var country: String? = null
+ open var city: String? = null
+ open var address: String? = null
+ open var selectedTags: MutableList = mutableListOf()
+}
+
+class FakeWizardCache : WizardCache() {
+ override var firstName: String? = null
+ override var lastName: String? = null
+ override var dateOfBirth: Date? = null
+ override var country: String? = null
+ override var city: String? = null
+ override var address: String? = null
+ override var selectedTags: MutableList = mutableListOf()
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/tag_background.xml b/app/src/main/res/drawable/tag_background.xml
new file mode 100644
index 0000000..4ecb1f2
--- /dev/null
+++ b/app/src/main/res/drawable/tag_background.xml
@@ -0,0 +1,15 @@
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/tag_background2.xml b/app/src/main/res/drawable/tag_background2.xml
new file mode 100644
index 0000000..6e4a4d8
--- /dev/null
+++ b/app/src/main/res/drawable/tag_background2.xml
@@ -0,0 +1,15 @@
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 0b15a20..b0b89ea 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,9 +1,17 @@
-
+ android:layout_height="match_parent">
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_user_input.xml b/app/src/main/res/layout/fragment_user_input.xml
new file mode 100644
index 0000000..723f935
--- /dev/null
+++ b/app/src/main/res/layout/fragment_user_input.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_user_input2.xml b/app/src/main/res/layout/fragment_user_input2.xml
new file mode 100644
index 0000000..89da7a3
--- /dev/null
+++ b/app/src/main/res/layout/fragment_user_input2.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_user_input3.xml b/app/src/main/res/layout/fragment_user_input3.xml
new file mode 100644
index 0000000..945b7ac
--- /dev/null
+++ b/app/src/main/res/layout/fragment_user_input3.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_user_input4.xml b/app/src/main/res/layout/fragment_user_input4.xml
new file mode 100644
index 0000000..9100438
--- /dev/null
+++ b/app/src/main/res/layout/fragment_user_input4.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..b336816
--- /dev/null
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..978d640 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,21 @@
BasicArchitecture
+ Введите имя
+ Введите фамилию
+ Укажите дату рождения
+ Далее
+ Укажите страну
+ Укажите город
+ Укажите адрес
+ Выберите интересы
+ Имя
+ Фамилия
+ Дата рождения
+ Адрес проживания
+ Интересы
+ %1$s, %2$s, %3$s
+ Личные данные
+ Адрес прожтвания
+ Интересы
+ Личная карточка
\ No newline at end of file
diff --git a/app/src/test/java/ru/otus/basicarchitecture/UserInputAddressViewModelTest.kt b/app/src/test/java/ru/otus/basicarchitecture/UserInputAddressViewModelTest.kt
new file mode 100644
index 0000000..801a139
--- /dev/null
+++ b/app/src/test/java/ru/otus/basicarchitecture/UserInputAddressViewModelTest.kt
@@ -0,0 +1,38 @@
+package ru.otus.basicarchitecture
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class UserInputAddressViewModelTest {
+
+ private lateinit var viewModel: UserInputAddressViewModel
+ private lateinit var fakeWizardCache: WizardCache
+ private lateinit var fakeDaDataService: FakeDaDataService
+
+ @get:Rule
+ val instantExecutorRule = InstantTaskExecutorRule()
+
+ @Before
+ fun setUp() {
+ fakeWizardCache = FakeWizardCache()
+ fakeDaDataService = FakeDaDataService()
+ viewModel = UserInputAddressViewModel(fakeWizardCache, fakeDaDataService)
+ }
+
+ @Test
+ fun `test validateAndSaveAddress with empty address should return LoseField`() {
+ viewModel.viewState.value = ViewState(address = "")
+ val result = viewModel.validateAndSaveAddress()
+ Assert.assertEquals(ValidateState.LoseFiled("Адрес"), result)
+ }
+
+ @Test
+ fun `test validateAndSaveAddress with non-empty address should return Ok`() {
+ viewModel.viewState.value = ViewState(address = "Some address")
+ val result = viewModel.validateAndSaveAddress()
+ Assert.assertEquals(ValidateState.Ok, result)
+ }
+}
diff --git a/app/src/test/java/ru/otus/basicarchitecture/UserInputViewModelTest.kt b/app/src/test/java/ru/otus/basicarchitecture/UserInputViewModelTest.kt
new file mode 100644
index 0000000..4a5d378
--- /dev/null
+++ b/app/src/test/java/ru/otus/basicarchitecture/UserInputViewModelTest.kt
@@ -0,0 +1,50 @@
+package ru.otus.basicarchitecture
+
+import org.junit.Before
+import org.junit.Test
+import org.junit.Assert.*
+import org.mockito.Mockito
+import org.mockito.Mockito.`when`
+import java.time.Instant
+
+class UserInputViewModelTest {
+
+ private lateinit var viewModel: UserInputViewModel
+ private lateinit var fakeWizardCache: WizardCache
+ private lateinit var mockClock: Clock
+
+ @Before
+ fun setUp() {
+ fakeWizardCache = FakeWizardCache()
+ mockClock = Mockito.mock(Clock::class.java)
+ viewModel = UserInputViewModel(fakeWizardCache, mockClock)
+ }
+
+ @Test
+ fun testIsValidFirst_missingField() {
+ val result = viewModel.isValidFirst("", listOf(listOf("", "Имя"), listOf("", "Фамилия")))
+ assertEquals(ValidateState.LoseFiled("Имя"), result)
+ }
+
+ @Test
+ fun testIsValidFirst_invalidDate() {
+ val result = viewModel.isValidFirst("invalid_date", listOf(listOf("John", "Имя"), listOf("Doe", "Фамилия")))
+ assertEquals(ValidateState.BedFiled("Дата рождения"), result)
+ }
+
+ @Test
+ fun testIsValidFirst_notAdult() {
+ `when`(mockClock.currentMillis()).thenReturn(Instant.parse("2023-01-01T00:00:00Z").toEpochMilli())
+ val notAdultDate = "10.01.2010" // Дата, когда еще нет 18 лет
+ val result = viewModel.isValidFirst(notAdultDate, listOf(listOf("John", "Имя"), listOf("Doe", "Фамилия")))
+ assertEquals(ValidateState.Not18, result)
+ }
+
+ @Test
+ fun testIsValidFirst_valid() {
+ `when`(mockClock.currentMillis()).thenReturn(Instant.parse("2023-01-01T00:00:00Z").toEpochMilli())
+ val validDate = "10.01.1990" // Валидная дата рождения
+ val result = viewModel.isValidFirst(validDate, listOf(listOf("John", "Имя"), listOf("Doe", "Фамилия")))
+ assertEquals(ValidateState.Ok, result)
+ }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index c84cccf..d5269e0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,6 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
- id 'com.android.application' version '8.0.0' apply false
- id 'com.android.library' version '8.0.0' apply false
+ id 'com.android.application' version '8.0.2' apply false
+ id 'com.android.library' version '8.0.2' apply false
id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
+ id 'com.google.dagger.hilt.android' version '2.47' apply false
}
\ No newline at end of file