diff --git a/app/src/main/java/ru/otus/basicarchitecture/App.kt b/app/src/main/java/ru/otus/basicarchitecture/App.kt new file mode 100644 index 0000000..41902a3 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/App.kt @@ -0,0 +1,7 @@ +package ru.otus.basicarchitecture + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class App : Application() \ 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..080ecdb 100644 --- a/app/src/main/java/ru/otus/basicarchitecture/MainActivity.kt +++ b/app/src/main/java/ru/otus/basicarchitecture/MainActivity.kt @@ -2,7 +2,9 @@ package ru.otus.basicarchitecture import androidx.appcompat.app.AppCompatActivity import android.os.Bundle +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/data/AddressRepository.kt b/app/src/main/java/ru/otus/basicarchitecture/data/AddressRepository.kt new file mode 100644 index 0000000..dbaf470 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/data/AddressRepository.kt @@ -0,0 +1,22 @@ +package ru.otus.basicarchitecture.data + +import ru.otus.basicarchitecture.data.network.DadataApi +import ru.otus.basicarchitecture.data.network.DadataRequest +import javax.inject.Inject + +class AddressRepository @Inject constructor( + private val api: DadataApi +) { + suspend fun getSuggestions(query: String): List { + return try { + val response = api.getAddressSuggestions(DadataRequest(query)) + if (response.isSuccessful) { + response.body()?.suggestions?.map { it.value } ?: emptyList() + } else { + emptyList() + } + } catch (_: Exception) { + emptyList() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/data/WizardCache.kt b/app/src/main/java/ru/otus/basicarchitecture/data/WizardCache.kt new file mode 100644 index 0000000..a3a1baf --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/data/WizardCache.kt @@ -0,0 +1,45 @@ +package ru.otus.basicarchitecture.data + +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class WizardCache @Inject constructor() { + + private var _name: String = "" + val name: String get() = _name + + private var _surname: String = "" + val surname: String get() = _surname + + private var _birthDate: String = "" + val birthDate: String get() = _birthDate + + private var _country: String = "" + val country: String get() = _country + + private var _city: String = "" + val city: String get() = _city + + private var _address: String = "" + val address: String get() = _address + + private var _interests: List = emptyList() + val interests: List get() = _interests + + fun updatePersonalInfo(name: String, surname: String, birthDate: String) { + _name = name + _surname = surname + _birthDate = birthDate + } + + fun updateAddress(country: String, city: String, address: String) { + _country = country + _city = city + _address = address + } + + fun updateInterests(interests: List) { + _interests = interests + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/data/network/DadataApi.kt b/app/src/main/java/ru/otus/basicarchitecture/data/network/DadataApi.kt new file mode 100644 index 0000000..b372468 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/data/network/DadataApi.kt @@ -0,0 +1,12 @@ +package ru.otus.basicarchitecture.data.network + +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.POST + +interface DadataApi { + @POST("suggestions/api/4_1/rs/suggest/address") + suspend fun getAddressSuggestions( + @Body request: DadataRequest + ): Response +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/data/network/DadataModels.kt b/app/src/main/java/ru/otus/basicarchitecture/data/network/DadataModels.kt new file mode 100644 index 0000000..0c4598d --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/data/network/DadataModels.kt @@ -0,0 +1,16 @@ +package ru.otus.basicarchitecture.data.network + +import com.google.gson.annotations.SerializedName + +data class DadataRequest( + @SerializedName("query") val query: String, @SerializedName("count") val count: Int = 10 +) + +data class DadataResponse( + @SerializedName("suggestions") val suggestions: List +) + +data class DadataSuggestion( + @SerializedName("value") val value: String, + @SerializedName("unrestricted_value") val unrestrictedValue: String +) \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/di/NetworkModule.kt b/app/src/main/java/ru/otus/basicarchitecture/di/NetworkModule.kt new file mode 100644 index 0000000..562268f --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/di/NetworkModule.kt @@ -0,0 +1,52 @@ +package ru.otus.basicarchitecture.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import ru.otus.basicarchitecture.BuildConfig +import ru.otus.basicarchitecture.data.network.DadataApi +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + + @Provides + @Singleton + fun provideOkHttpClient(): OkHttpClient { + val logging = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + val authInterceptor = Interceptor { chain -> + val original = chain.request() + val request = original.newBuilder().header( + "Authorization", "Token ${BuildConfig.DADATA_API_KEY}" + ).header("Content-Type", "application/json").header("Accept", "application/json") + .method(original.method, original.body).build() + chain.proceed(request) + } + + return OkHttpClient.Builder().addInterceptor(authInterceptor).addInterceptor(logging) + .build() + } + + @Provides + @Singleton + fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit { + return Retrofit.Builder().baseUrl("https://suggestions.dadata.ru/") + .addConverterFactory(GsonConverterFactory.create()).client(okHttpClient).build() + } + + @Provides + @Singleton + fun provideDadataApi(retrofit: Retrofit): DadataApi { + return retrofit.create(DadataApi::class.java) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/NoFilterAdapter.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/NoFilterAdapter.kt new file mode 100644 index 0000000..05011ba --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/NoFilterAdapter.kt @@ -0,0 +1,23 @@ +package ru.otus.basicarchitecture.ui + +import android.content.Context +import android.widget.ArrayAdapter + +class NoFilterAdapter(context: Context, resource: Int, objects: List) : + ArrayAdapter(context, resource, objects) { + + private val noFilter = object : android.widget.Filter() { + override fun performFiltering(constraint: CharSequence?): FilterResults { + val results = FilterResults() + results.values = objects + results.count = objects.size + return results + } + + override fun publishResults(constraint: CharSequence?, results: FilterResults?) { + notifyDataSetChanged() + } + } + + override fun getFilter(): android.widget.Filter = noFilter +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/ViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/ViewModel.kt new file mode 100644 index 0000000..32db295 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/ViewModel.kt @@ -0,0 +1,149 @@ +package ru.otus.basicarchitecture.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import ru.otus.basicarchitecture.data.WizardCache +import java.time.LocalDate +import java.time.Period +import java.time.format.DateTimeFormatter +import javax.inject.Inject +import ru.otus.basicarchitecture.data.AddressRepository + +// StepOneViewModel +// region +@HiltViewModel +class StepOneViewModel @Inject constructor( + private val wizardCache: WizardCache +) : ViewModel() { + + val name = MutableStateFlow("") + val surname = MutableStateFlow("") + val birthDate = MutableStateFlow("") + + private val _isNextButtonEnabled = MutableStateFlow(false) + val isNextButtonEnabled: StateFlow = _isNextButtonEnabled + + private val _errorEvents = MutableSharedFlow() + val errorEvents = _errorEvents.asSharedFlow() + + fun onInputChanged(newName: String, newSurname: String, newBirthDate: String) { + name.value = newName + surname.value = newSurname + birthDate.value = newBirthDate + validate() + } + + private fun validate() { + val currentName = name.value + val currentSurname = surname.value + val currentDate = birthDate.value + + val isDateValidLength = currentDate.length == 10 + val isAdult = if (isDateValidLength) checkIsAdult(currentDate) else false + + val isValid = currentName.isNotBlank() && currentSurname.isNotBlank() && isAdult + _isNextButtonEnabled.value = isValid + + if (isDateValidLength && !isAdult) { + viewModelScope.launch { _errorEvents.emit("Вам должно быть больше 18 лет") } + } + } + + fun onNextClicked() { + if (_isNextButtonEnabled.value) { + wizardCache.updatePersonalInfo(name.value, surname.value, birthDate.value) + } else { + viewModelScope.launch { _errorEvents.emit("Заполните все поля корректно") } + } + } + + private fun checkIsAdult(birthDateStr: String): Boolean { + return try { + val formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy") + val date = LocalDate.parse(birthDateStr, formatter) + Period.between(date, LocalDate.now()).years >= 18 + } catch (_: Exception) { + false + } + } +} +// endregion + +// StepTwoViewModel +// region +@HiltViewModel +class StepTwoViewModel @Inject constructor( + private val repository: AddressRepository, private val wizardCache: WizardCache +) : ViewModel() { + + private val _suggestions = MutableStateFlow>(emptyList()) + val suggestions: StateFlow> = _suggestions + + private val _isNextButtonEnabled = MutableStateFlow(false) + val isNextButtonEnabled: StateFlow = _isNextButtonEnabled + + fun onAddressInputChanged(query: String) { + _isNextButtonEnabled.value = query.isNotBlank() && query.length > 5 + } + + fun getSuggestions(query: String) { + viewModelScope.launch { + val list = repository.getSuggestions(query) + _suggestions.value = list + } + } + + fun onNextClicked(fullAddress: String) { + wizardCache.updateAddress("", "", fullAddress) + } +} +// endregion + +// StepThreeViewModel +// region +@HiltViewModel +class StepThreeViewModel @Inject constructor( + private val wizardCache: WizardCache +) : ViewModel() { + + val allInterests = listOf("Музыка", "Спорт", "Кино", "IT", "Путешествия", "Наука", "Книги") + private val selectedInterests = mutableSetOf() + + private val _isNextButtonEnabled = MutableStateFlow(false) + val isNextButtonEnabled: StateFlow = _isNextButtonEnabled + + fun onInterestChanged(interest: String, isSelected: Boolean) { + if (isSelected) { + selectedInterests.add(interest) + } else { + selectedInterests.remove(interest) + } + _isNextButtonEnabled.value = selectedInterests.isNotEmpty() + } + + fun onNextClicked() { + wizardCache.updateInterests(selectedInterests.toList()) + } + +} +// endregion + +// StepFourViewModel +// region +@HiltViewModel +class StepFourViewModel @Inject constructor( + wizardCache: WizardCache +) : ViewModel() { + val birthDate = wizardCache.birthDate + val fullAddress = "${wizardCache.country} ${wizardCache.city} ${wizardCache.address}".trim() + val interests = wizardCache.interests + val name = wizardCache.name + val surname = wizardCache.surname +} +// endregion \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/steps/StepFourFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/steps/StepFourFragment.kt new file mode 100644 index 0000000..6ec6e16 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/steps/StepFourFragment.kt @@ -0,0 +1,42 @@ +package ru.otus.basicarchitecture.ui.steps + +import android.os.Bundle +import android.util.TypedValue +import android.view.View +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import com.google.android.material.chip.Chip +import com.google.android.material.chip.ChipGroup +import dagger.hilt.android.AndroidEntryPoint +import ru.otus.basicarchitecture.R +import ru.otus.basicarchitecture.ui.StepFourViewModel + +@AndroidEntryPoint +class StepFourFragment : Fragment(R.layout.fragment_step_four) { + + private val viewModel: StepFourViewModel by viewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + view.findViewById(R.id.tvName).text = viewModel.name + view.findViewById(R.id.tvSurname).text = viewModel.surname + view.findViewById(R.id.tvBirthDate).text = viewModel.birthDate + view.findViewById(R.id.tvAddress).text = viewModel.fullAddress + + val chipGroup = view.findViewById(R.id.cgInterests) + viewModel.interests.forEach { interest -> + val chip = Chip(requireContext()).apply { + text = interest + isCheckable = false + setChipBackgroundColorResource(android.R.color.transparent) + setChipStrokeColorResource(android.R.color.darker_gray) + chipStrokeWidth = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 2f, resources.displayMetrics + ) + } + chipGroup.addView(chip) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/steps/StepOneFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/steps/StepOneFragment.kt new file mode 100644 index 0000000..aa7426e --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/steps/StepOneFragment.kt @@ -0,0 +1,103 @@ +package ru.otus.basicarchitecture.ui.steps + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.View +import android.widget.Button +import android.widget.EditText +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.fragment.findNavController +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import ru.otus.basicarchitecture.R +import ru.otus.basicarchitecture.ui.StepOneViewModel + +@AndroidEntryPoint +class StepOneFragment : Fragment(R.layout.fragment_step_one) { + private val viewModel: StepOneViewModel by viewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val etName = view.findViewById(R.id.etName) + val etSurname = view.findViewById(R.id.etSurname) + val etBirthDate = view.findViewById(R.id.etBirthDate) + val btnNext = view.findViewById