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
48 changes: 39 additions & 9 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id "com.google.devtools.ksp"
id 'com.google.dagger.hilt.android'
}

android {
namespace 'ru.otus.basicarchitecture'
compileSdk 35
compileSdk 36

defaultConfig {
applicationId "ru.otus.basicarchitecture"
minSdk 24
targetSdk 35
minSdk 36
targetSdk 36
versionCode 1
versionName "1.0"

buildConfigField "String", "dadata_api_key", "\"666c54c11310bd246fa5fb41224b9e4e74df886b\""

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

Expand All @@ -30,15 +34,41 @@ android {
kotlinOptions {
jvmTarget = '17'
}

buildFeatures{
viewBinding = true
buildConfig = true
}
}

dependencies {

implementation 'androidx.core:core-ktx:1.15.0'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.core:core-ktx:1.17.0'
implementation 'androidx.activity:activity-ktx:1.12.0'
implementation 'androidx.fragment:fragment-ktx:1.8.8'
implementation("androidx.navigation:navigation-fragment-ktx:2.9.0" )
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'com.google.android.material:material:1.13.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
implementation 'androidx.navigation:navigation-fragment-ktx:2.8.6'
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
implementation("com.google.code.gson:gson:2.8.5")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation 'com.squareup.okhttp3:okhttp:4.12.0'

testImplementation 'androidx.arch.core:core-testing:2.2.0'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0'
testImplementation 'org.mockito:mockito-core:5.12.0'
testImplementation 'org.mockito.kotlin:mockito-kotlin:5.2.1'
testImplementation 'org.mockito:mockito-inline:5.2.0'
testImplementation 'net.bytebuddy:byte-buddy:1.14.15'
androidTestImplementation 'org.mockito:mockito-android:5.12.0'


implementation("com.google.dagger:hilt-android:2.57.2")
ksp "com.google.dagger:hilt-compiler:2.57.2"
}
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET"/>

<application
android:name=".HiltApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
Expand Down
140 changes: 140 additions & 0 deletions app/src/main/java/ru/otus/basicarchitecture/AddressFragment.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package ru.otus.basicarchitecture

import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.viewModels
import kotlin.getValue
import ru.otus.basicarchitecture.databinding.FragmentAddressBinding
import dagger.hilt.android.AndroidEntryPoint
import android.widget.ArrayAdapter
import androidx.core.widget.doOnTextChanged

@AndroidEntryPoint
class AddressFragment : Fragment() {
private var _binding: FragmentAddressBinding? = null
private val binding: FragmentAddressBinding
get() = _binding ?: throw RuntimeException("FragmentAddressBinding == null")

private val viewModel: AddressViewModel by viewModels()
private val adapter by lazy {
ArrayAdapter(
requireContext(),
android.R.layout.simple_dropdown_item_1line,
mutableListOf<String>()
)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.addressInput.setAdapter(adapter)
observeViewModel()
addTextChangedListeners()
binding.buttonNext.setOnClickListener {
viewModel.validateData()
}

binding.addressInput.setOnItemClickListener { _, _, position, _ ->
val selectedItem = binding.addressInput.adapter.getItem(position) as? UserAddress
?: return@setOnItemClickListener
selectedItem.let {
val address = listOf(
it.country,
it.city,
it.street,
it.house,
it.block
).filter { !it.isBlank() }
.joinToString(", ")
binding.addressInput.setText(address)
}
}
}

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentAddressBinding.inflate(inflater, container, false)
return binding.root
}

fun observeViewModel(){
viewModel.errorNetwork.observe(viewLifecycleOwner) {
if (it) {
Toast.makeText(
requireContext(),
getString(R.string.error_network),
Toast.LENGTH_SHORT
).show()
}
}

viewModel.errAddress.observe(viewLifecycleOwner) {
with(binding) {
if (it){
addressInput.error = String.format(resources.getString(R.string.empty_field),
addressInput.hint.toString()
)
} else {
addressInput.error = null
}
}
}

viewModel.listUserAddress.observe(viewLifecycleOwner) { listUserAddress ->
adapter.clear()
adapter.addAll(listUserAddress.map {
listOf(
it.country,
it.city,
it.street,
it.house,
it.block
).filter { !it.isBlank() }
.joinToString(", ")
})
}

viewModel.canGoNext.observe(viewLifecycleOwner) {
if (it){
requireActivity().supportFragmentManager.beginTransaction()
.replace(R.id.mainContainer, InterestsFragment())
.commit()
}
}
}

private fun addTextChangedListeners(){
with(binding){
addressInput.doOnTextChanged { text, _, _, _ ->
val address = addressInput.text.toString()
viewModel.setAddress(address)
viewModel.searchAddress(address)
}
}
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}

companion object {
private const val EXTRA_USER_NAME = "user_name"

fun newInstance() = AddressFragment()
}
}



64 changes: 64 additions & 0 deletions app/src/main/java/ru/otus/basicarchitecture/AddressVewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package ru.otus.basicarchitecture

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.launch
import ru.otus.basicarchitecture.address_by_dadata.AddressSuggestUseCase
import javax.inject.Inject

@HiltViewModel
class AddressViewModel @Inject constructor(
private val cache: WizardCache,
private val addressSuggestUseCase: AddressSuggestUseCase
): ViewModel() {
private var _listUserAddress = MutableLiveData<List<UserAddress>>()
val listUserAddress: LiveData<List<UserAddress>>
get() = _listUserAddress
private var _canGoNext = MutableLiveData<Boolean>(false)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AnGuru-60 , тут все хорошо и правильно. Как вариант, я бы предложил закинуть реактивные свойства (лайвдату или флоу) прямо в визардкеш и проксировать их оттуда через модель. Таким образом, у нас будет один источник истины в данных. Во вьюхе подписываться на них из модели.
Сейчас у нас получается два источника:

  • инпуты. Они управляются пользователем + saved state выполняет ОС
  • кеш. Мы кладем туда состояние при помощи логики
    Где "истинное" значение данных?
    Это может быть потенциальной проблемой, так как у нас несколько мест хранения и состояние не централизовано. Такие штуки описываются концепциями "single source of truth", UDF, MVI и прочим. И композ тоже делить состояние и отображение явно - как раз с этой целью

val canGoNext: LiveData<Boolean> get() = _canGoNext
private var _errAddress = MutableLiveData<Boolean>()
val errAddress: LiveData<Boolean> get() = _errAddress
private var _errorNetwork = MutableLiveData<Boolean>()
val errorNetwork: LiveData<Boolean>
get() = _errorNetwork

fun validateData() {
var success = checkEmptyFields()

if (success == false){
_canGoNext.value = false
return
}
_canGoNext.value = true
}

fun setAddress(fullAddress: String) {
cache.userAddress.fullAddress = fullAddress
}

fun searchAddress(query: String) {
viewModelScope.launch {
try {
val result = addressSuggestUseCase.invoke(query)
_listUserAddress.postValue(result)
} catch (e: Exception) {
_errorNetwork.postValue(true)
}
}
}

private fun checkEmptyFields(): Boolean{
var success = true

if (cache.userAddress.fullAddress.isBlank()){
_errAddress.value = true
success = false
} else{
_errAddress.value = false
}
return success
}
}
51 changes: 51 additions & 0 deletions app/src/main/java/ru/otus/basicarchitecture/Application.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package ru.otus.basicarchitecture

import android.app.Application
import dagger.hilt.android.HiltAndroidApp
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import ru.otus.basicarchitecture.address_by_dadata.AddressApiService
import ru.otus.basicarchitecture.address_by_dadata.AddressCollector
import ru.otus.basicarchitecture.address_by_dadata.AddressCollectorImpl
import ru.otus.basicarchitecture.address_by_dadata.AddressSuggestUseCase
import javax.inject.Singleton

@HiltAndroidApp
class HiltApplication : Application() {

}

@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://suggestions.dadata.ru/suggestions/api/4_1/rs/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}

@Provides
@Singleton
fun provideDaDataService(retrofit: Retrofit): AddressApiService {
return retrofit.create(AddressApiService::class.java)
}

@Provides
@Singleton
fun provideAddressRepository(impl: AddressCollectorImpl): AddressCollector {
return impl
}

@Provides
@Singleton
fun provideAddressSuggestUseCase(repository: AddressCollector): AddressSuggestUseCase {
return AddressSuggestUseCase(repository)
}
}
Loading