Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/src/main/java/ru/otus/basicarchitecture/App.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ru.otus.basicarchitecture

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class App : Application()
2 changes: 2 additions & 0 deletions app/src/main/java/ru/otus/basicarchitecture/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> {
return try {
val response = api.getAddressSuggestions(DadataRequest(query))
if (response.isSuccessful) {
response.body()?.suggestions?.map { it.value } ?: emptyList()
} else {
emptyList()
}
} catch (_: Exception) {
emptyList()
}
}
}
45 changes: 45 additions & 0 deletions app/src/main/java/ru/otus/basicarchitecture/data/WizardCache.kt
Original file line number Diff line number Diff line change
@@ -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<String> = emptyList()
val interests: List<String> 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<String>) {
_interests = interests
}
}
Original file line number Diff line number Diff line change
@@ -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<DadataResponse>
}
Original file line number Diff line number Diff line change
@@ -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<DadataSuggestion>
)

data class DadataSuggestion(
@SerializedName("value") val value: String,
@SerializedName("unrestricted_value") val unrestrictedValue: String
)
52 changes: 52 additions & 0 deletions app/src/main/java/ru/otus/basicarchitecture/di/NetworkModule.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
23 changes: 23 additions & 0 deletions app/src/main/java/ru/otus/basicarchitecture/ui/NoFilterAdapter.kt
Original file line number Diff line number Diff line change
@@ -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<String>) :
ArrayAdapter<String>(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
}
149 changes: 149 additions & 0 deletions app/src/main/java/ru/otus/basicarchitecture/ui/ViewModel.kt
Original file line number Diff line number Diff line change
@@ -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<Boolean> = _isNextButtonEnabled

private val _errorEvents = MutableSharedFlow<String>()
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<List<String>>(emptyList())
val suggestions: StateFlow<List<String>> = _suggestions

private val _isNextButtonEnabled = MutableStateFlow(false)
val isNextButtonEnabled: StateFlow<Boolean> = _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<String>()

private val _isNextButtonEnabled = MutableStateFlow(false)
val isNextButtonEnabled: StateFlow<Boolean> = _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
Original file line number Diff line number Diff line change
@@ -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<TextView>(R.id.tvName).text = viewModel.name
view.findViewById<TextView>(R.id.tvSurname).text = viewModel.surname
view.findViewById<TextView>(R.id.tvBirthDate).text = viewModel.birthDate
view.findViewById<TextView>(R.id.tvAddress).text = viewModel.fullAddress

val chipGroup = view.findViewById<ChipGroup>(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)
}
}
}
Loading