diff --git a/app/build.gradle b/app/build.gradle index 9c99d98..fb726a4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,16 +1,18 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' + id("org.jetbrains.kotlin.kapt") + id("org.jetbrains.kotlin.plugin.serialization") } android { namespace 'ru.otus.basicarchitecture' - compileSdk 33 + compileSdk 34 defaultConfig { applicationId "ru.otus.basicarchitecture" - minSdk 24 - targetSdk 33 + minSdk 26 + targetSdk 34 versionCode 1 versionName "1.0" @@ -24,20 +26,40 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + buildFeatures { + viewBinding true } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' + } + kapt { + correctErrorTypes true } } dependencies { + implementation("com.google.dagger:dagger:2.48.1") + kapt("com.google.dagger:dagger-compiler:2.48.1") - 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 'androidx.navigation:navigation-fragment-ktx:2.7.6' + implementation 'androidx.navigation:navigation-ui-ktx:2.7.6' + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.12.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/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1e81fea..e6f0ec2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,9 @@ - + + android:exported="true"> + + + + + + \ No newline at end of file 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..eea3fb6 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/App.kt @@ -0,0 +1,12 @@ +package ru.otus.basicarchitecture + +import android.app.Application +import ru.otus.basicarchitecture.DI.MainComponent.ApplicationModule +import ru.otus.basicarchitecture.DI.MainComponent.DaggerMainComponent + +class App : Application() { + val component by lazy { + DaggerMainComponent.builder() + .applicationModule(ApplicationModule(this)).build() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Core/Model/Address.kt b/app/src/main/java/ru/otus/basicarchitecture/Core/Model/Address.kt new file mode 100644 index 0000000..56fc9fe --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Core/Model/Address.kt @@ -0,0 +1,5 @@ +package ru.otus.basicarchitecture.Core.Model + +data class Address( + val address: String +) : BaseModel \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Core/Model/BaseModel.kt b/app/src/main/java/ru/otus/basicarchitecture/Core/Model/BaseModel.kt new file mode 100644 index 0000000..c6590e7 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Core/Model/BaseModel.kt @@ -0,0 +1,3 @@ +package ru.otus.basicarchitecture.Core.Model + +sealed interface BaseModel \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Core/Model/DTO/Suggestion.kt b/app/src/main/java/ru/otus/basicarchitecture/Core/Model/DTO/Suggestion.kt new file mode 100644 index 0000000..fb9d809 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Core/Model/DTO/Suggestion.kt @@ -0,0 +1,22 @@ +package ru.otus.basicarchitecture.Core.Model.DTO + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SuggestionRequest( + @SerialName("query") + val query: String +) + +@Serializable +data class Suggestions( + @SerialName("suggestions") + val suggestions: List? +) + +@Serializable +data class Suggestion( + @SerialName("value") + val value: String? +) \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Core/Model/Interests.kt b/app/src/main/java/ru/otus/basicarchitecture/Core/Model/Interests.kt new file mode 100644 index 0000000..c0b7a78 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Core/Model/Interests.kt @@ -0,0 +1,5 @@ +package ru.otus.basicarchitecture.Core.Model + +data class Interests ( + var interests: String +) : BaseModel \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Core/Model/Person.kt b/app/src/main/java/ru/otus/basicarchitecture/Core/Model/Person.kt new file mode 100644 index 0000000..d7ebfc1 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Core/Model/Person.kt @@ -0,0 +1,22 @@ +package ru.otus.basicarchitecture.Core.Model + +import ru.otus.basicarchitecture.DI.FragmentScope + +@FragmentScope +data class Person( + var firstName: String, + var surName: String, + var dateOfBirth: String +) : BaseModel { + companion object { + const val defaultValueProperty = "" + + fun defaultPerson(): Person { + return Person( + defaultValueProperty, + defaultValueProperty, + defaultValueProperty + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Core/Model/ViewModelFactory.kt b/app/src/main/java/ru/otus/basicarchitecture/Core/Model/ViewModelFactory.kt new file mode 100644 index 0000000..a47bf1c --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Core/Model/ViewModelFactory.kt @@ -0,0 +1,13 @@ +package ru.otus.basicarchitecture.Core.Model + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import javax.inject.Inject + +class ViewModelFactory @Inject constructor( + private val viewModels: @JvmSuppressWildcards Map +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return viewModels[modelClass.simpleName] as T + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Core/Utils/AppConstant.kt b/app/src/main/java/ru/otus/basicarchitecture/Core/Utils/AppConstant.kt new file mode 100644 index 0000000..9c7b389 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Core/Utils/AppConstant.kt @@ -0,0 +1,5 @@ +package ru.otus.basicarchitecture.Core.Utils + +object AppConstant { + const val API_KEY = "463d2f8b8abd97cf30e7ee538d5e6e6f36242b3e" +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Core/Utils/ErrorService.kt b/app/src/main/java/ru/otus/basicarchitecture/Core/Utils/ErrorService.kt new file mode 100644 index 0000000..5f2858c --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Core/Utils/ErrorService.kt @@ -0,0 +1,26 @@ +package ru.otus.basicarchitecture.Core.Utils + +import android.app.AlertDialog +import android.app.Dialog +import android.content.Context +import android.content.DialogInterface +import android.text.SpannableString +import android.widget.Toast +import ru.otus.basicarchitecture.R +import javax.inject.Inject + +class ErrorService @Inject constructor() { + private var context: Context? = null + fun show(message: String) { + val title = "Ошибка" + if (context!= null) { + Toast.makeText(context,message ,Toast.LENGTH_SHORT).show(); + + } + + } + + fun setContext(context: Context?) { + this.context = context + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Core/Utils/Helper.kt b/app/src/main/java/ru/otus/basicarchitecture/Core/Utils/Helper.kt new file mode 100644 index 0000000..7d62dea --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Core/Utils/Helper.kt @@ -0,0 +1,5 @@ +package ru.otus.basicarchitecture.Core.Utils + +import android.text.Editable + +fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this) \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment1/Fragment1SubComponent.kt b/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment1/Fragment1SubComponent.kt new file mode 100644 index 0000000..29dc82f --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment1/Fragment1SubComponent.kt @@ -0,0 +1,17 @@ +package ru.otus.basicarchitecture.DI.Fragment1 + +import dagger.Subcomponent +import ru.otus.basicarchitecture.DI.FragmentComponents.FirstScreen.PersonModule +import ru.otus.basicarchitecture.DI.FragmentScope +import ru.otus.basicarchitecture.Ui.Fragment1.Fragment1 + +@FragmentScope +@Subcomponent(modules = [PersonModule::class, Fragment1ViewModelModule::class]) +interface Fragment1SubComponent { + @Subcomponent.Builder + interface Builder { + fun build() : Fragment1SubComponent + } + + fun inject(fragment1: Fragment1) +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment1/Fragment1ViewModelModule.kt b/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment1/Fragment1ViewModelModule.kt new file mode 100644 index 0000000..2d4e8f4 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment1/Fragment1ViewModelModule.kt @@ -0,0 +1,18 @@ +package ru.otus.basicarchitecture.DI.Fragment1 + +import androidx.lifecycle.ViewModel +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap +import dagger.multibindings.StringKey +import ru.otus.basicarchitecture.Ui.Fragment1.Fragment1ViewModel + +@Module +interface Fragment1ViewModelModule { + + @IntoMap + @StringKey("Fragment1ViewModel") + @Binds + fun bindFirstScreenViewModel(impl: Fragment1ViewModel): ViewModel + +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment1/PersonModule.kt b/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment1/PersonModule.kt new file mode 100644 index 0000000..cb27c5b --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment1/PersonModule.kt @@ -0,0 +1,17 @@ +package ru.otus.basicarchitecture.DI.FragmentComponents.FirstScreen + +import dagger.Module +import dagger.Provides +import ru.otus.basicarchitecture.Core.Model.Person +import ru.otus.basicarchitecture.DI.FragmentScope + + +@Module +class PersonModule { + @FragmentScope + @Provides + fun providePerson(): Person { + return Person.defaultPerson() + } +} + diff --git a/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment2/AddressModule.kt b/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment2/AddressModule.kt new file mode 100644 index 0000000..931ba35 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment2/AddressModule.kt @@ -0,0 +1,14 @@ +package ru.otus.basicarchitecture.DI.Fragment2 +import dagger.Module +import dagger.Provides +import ru.otus.basicarchitecture.Core.Model.Address +import ru.otus.basicarchitecture.DI.FragmentScope + +@Module +class AddressModule { + @FragmentScope + @Provides + fun providesAddress(): Address { + return Address("") + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment2/Fragment2SubComponent.kt b/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment2/Fragment2SubComponent.kt new file mode 100644 index 0000000..5cfac3f --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment2/Fragment2SubComponent.kt @@ -0,0 +1,19 @@ +package ru.otus.basicarchitecture.DI.Fragment2 + +import android.content.Context +import dagger.BindsInstance +import dagger.Subcomponent +import ru.otus.basicarchitecture.DI.FragmentComponents.FirstScreen.PersonModule +import ru.otus.basicarchitecture.DI.FragmentScope +import ru.otus.basicarchitecture.Ui.Fragment1.Fragment2 + +@FragmentScope +@Subcomponent(modules = [AddressModule::class, Fragment2ViewModelModule::class]) +interface Fragment2SubComponent { + @Subcomponent.Builder + interface Builder { + fun build() : Fragment2SubComponent + } + + fun inject(fragment2: Fragment2) +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment2/Fragment2ViewModelModule.kt b/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment2/Fragment2ViewModelModule.kt new file mode 100644 index 0000000..9cbe3ff --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment2/Fragment2ViewModelModule.kt @@ -0,0 +1,16 @@ +package ru.otus.basicarchitecture.DI.Fragment2 +import androidx.lifecycle.ViewModel +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap +import dagger.multibindings.StringKey +import ru.otus.basicarchitecture.Ui.Fragment1.Fragment2ViewModel + +@Module +interface Fragment2ViewModelModule { + @IntoMap + @StringKey("Fragment2ViewModel") + @Binds + fun bindSecondScreenViewModel(impl: Fragment2ViewModel): ViewModel + +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment3/Fragment3SubComponent.kt b/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment3/Fragment3SubComponent.kt new file mode 100644 index 0000000..77c0dd9 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment3/Fragment3SubComponent.kt @@ -0,0 +1,19 @@ +package ru.otus.basicarchitecture.DI.Fragment3 + +import dagger.Subcomponent +import ru.otus.basicarchitecture.DI.FragmentScope +import ru.otus.basicarchitecture.Ui.Fragment3.Fragment3 + +@FragmentScope +@Subcomponent(modules = [Fragment3ViewModelModule::class]) +interface Fragment3SubComponent { + + @Subcomponent.Builder + interface Builder { + fun build() : Fragment3SubComponent + } + + fun inject(fragmentThird: Fragment3) + + +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment3/Fragment3ViewModelModule.kt b/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment3/Fragment3ViewModelModule.kt new file mode 100644 index 0000000..0e704b9 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment3/Fragment3ViewModelModule.kt @@ -0,0 +1,18 @@ +package ru.otus.basicarchitecture.DI.Fragment3 + +import androidx.lifecycle.ViewModel +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap +import dagger.multibindings.StringKey +import ru.otus.basicarchitecture.Ui.Fragment3.Fragment3ViewModel + +@Module +interface Fragment3ViewModelModule { + + @IntoMap + @StringKey("Fragment3ViewModel") + @Binds + fun bindThirdScreenViewModel(impl: Fragment3ViewModel): ViewModel + +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment4/Fragment4SubComponent.kt b/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment4/Fragment4SubComponent.kt new file mode 100644 index 0000000..e86eb8a --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment4/Fragment4SubComponent.kt @@ -0,0 +1,19 @@ +package ru.otus.basicarchitecture.DI.FragmentComponents.Fragment4 + +import dagger.Subcomponent +import ru.otus.basicarchitecture.DI.FragmentScope +import ru.otus.basicarchitecture.Ui.Fragment4.Fragment4 + +@FragmentScope +@Subcomponent(modules = [Fragment4ViewModelModule::class]) +interface Fragment4SubComponent { + + + @Subcomponent.Builder + interface Builder { + fun build() : Fragment4SubComponent + } + + fun inject(fragment: Fragment4) + +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment4/Fragment4ViewModelModule.kt b/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment4/Fragment4ViewModelModule.kt new file mode 100644 index 0000000..3a65868 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/DI/Fragment4/Fragment4ViewModelModule.kt @@ -0,0 +1,18 @@ +package ru.otus.basicarchitecture.DI.FragmentComponents.Fragment4 + +import androidx.lifecycle.ViewModel +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap +import dagger.multibindings.StringKey +import ru.otus.basicarchitecture.Ui.Fragment4.Fragment4ViewModel + + +@Module +interface Fragment4ViewModelModule { + + @IntoMap + @StringKey("Fragment4ViewModel") + @Binds + fun bindFourthScreenViewModel(impl: Fragment4ViewModel): ViewModel +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/DI/FragmentScope.kt b/app/src/main/java/ru/otus/basicarchitecture/DI/FragmentScope.kt new file mode 100644 index 0000000..8141879 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/DI/FragmentScope.kt @@ -0,0 +1,7 @@ +package ru.otus.basicarchitecture.DI + +import javax.inject.Scope + +@Scope +@Retention(value = AnnotationRetention.RUNTIME) +annotation class FragmentScope() \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/DI/MainComponent/ApplicationModule.kt b/app/src/main/java/ru/otus/basicarchitecture/DI/MainComponent/ApplicationModule.kt new file mode 100644 index 0000000..d803a78 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/DI/MainComponent/ApplicationModule.kt @@ -0,0 +1,75 @@ +package ru.otus.basicarchitecture.DI.MainComponent + +import android.content.Context +import android.util.Log +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import dagger.Module +import dagger.Provides +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import me.amitshekhar.mvvm.di.ApplicationContext +import me.amitshekhar.mvvm.di.BaseUrl +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import ru.otus.basicarchitecture.App +import ru.otus.basicarchitecture.Core.Utils.AppConstant +import ru.otus.basicarchitecture.R +import ru.otus.basicarchitecture.data.NetworkService +import java.util.concurrent.TimeUnit +import javax.inject.Singleton + +@Module +class ApplicationModule(private val application: App) { + + @ApplicationContext + @Provides + fun provideContext(): Context { + return application + } + + @BaseUrl + @Provides + fun provideBaseUrl(): String = "http://suggestions.dadata.ru/" + + @Provides + @Singleton + fun provideNetworkService( + @BaseUrl baseUrl: String): NetworkService { + val loggingInterceptor = HttpLoggingInterceptor { message -> + Log.d("Server", message) + }.apply { + level = HttpLoggingInterceptor.Level.BODY + } + + val token = application.getString(R.string.dadata_api_key) + val okHttp = OkHttpClient.Builder() + .connectTimeout(60, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(60, TimeUnit.SECONDS) + .addInterceptor(loggingInterceptor) + .addInterceptor(Interceptor { chain -> + chain.proceed( + chain.request().newBuilder() + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .header( + "Authorization", + "Token ${token}") + .build()) + }) + .build() + val contentType = "application/json".toMediaType() + return Retrofit.Builder() + .baseUrl(baseUrl) + .client(okHttp) + .addConverterFactory(Json{ignoreUnknownKeys = true}.asConverterFactory(contentType)) + .build() + .create(NetworkService::class.java) + + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/DI/MainComponent/MainComponent.kt b/app/src/main/java/ru/otus/basicarchitecture/DI/MainComponent/MainComponent.kt new file mode 100644 index 0000000..52987f9 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/DI/MainComponent/MainComponent.kt @@ -0,0 +1,31 @@ +package ru.otus.basicarchitecture.DI.MainComponent + +import android.content.Context +import dagger.Component +import me.amitshekhar.mvvm.di.ApplicationContext +import ru.otus.basicarchitecture.DI.Fragment1.Fragment1SubComponent +import ru.otus.basicarchitecture.DI.Fragment2.Fragment2SubComponent +import ru.otus.basicarchitecture.DI.Fragment3.Fragment3SubComponent +import ru.otus.basicarchitecture.DI.FragmentComponents.Fragment4.Fragment4SubComponent +import ru.otus.basicarchitecture.Domain.Repository +import ru.otus.basicarchitecture.data.NetworkService +import ru.otus.basicarchitecture.data.WizardCache +import javax.inject.Singleton + +@Singleton +@Component(modules = [WizardCacheModule::class, RepositoryModule::class, ApplicationModule::class]) +interface MainComponent { + fun fragment1SubComponent() : Fragment1SubComponent.Builder + fun fragment2SubComponent() : Fragment2SubComponent.Builder + fun fragment3SubComponent() : Fragment3SubComponent.Builder + fun fragment4SubComponent() : Fragment4SubComponent.Builder + + fun wizardCache(): WizardCache + + fun repository(): Repository + + @ApplicationContext + fun getContext(): Context + + fun getNetworkService(): NetworkService +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/DI/MainComponent/RepositoryModule.kt b/app/src/main/java/ru/otus/basicarchitecture/DI/MainComponent/RepositoryModule.kt new file mode 100644 index 0000000..0c5e0df --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/DI/MainComponent/RepositoryModule.kt @@ -0,0 +1,14 @@ +package ru.otus.basicarchitecture.DI.MainComponent + +import dagger.Binds +import dagger.Module +import ru.otus.basicarchitecture.data.RepositoryImpl +import ru.otus.basicarchitecture.Domain.Repository +import javax.inject.Singleton + +@Module +interface RepositoryModule { + @Singleton + @Binds + fun getRepository(impl: RepositoryImpl) : Repository +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/DI/MainComponent/WizardCacheModule.kt b/app/src/main/java/ru/otus/basicarchitecture/DI/MainComponent/WizardCacheModule.kt new file mode 100644 index 0000000..c7f2597 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/DI/MainComponent/WizardCacheModule.kt @@ -0,0 +1,21 @@ +package ru.otus.basicarchitecture.DI.MainComponent + +import dagger.Module +import dagger.Provides +import ru.otus.basicarchitecture.data.WizardCache +import javax.inject.Singleton + +@Module +class WizardCacheModule { + @Singleton + @Provides + fun provideWizardCache() : WizardCache{ + return WizardCache( + "", + "", + "", + "", + "" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/DI/qualifiers.kt b/app/src/main/java/ru/otus/basicarchitecture/DI/qualifiers.kt new file mode 100644 index 0000000..0781c3d --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/DI/qualifiers.kt @@ -0,0 +1,15 @@ +package me.amitshekhar.mvvm.di + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class ApplicationContext + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class ActivityContext + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class BaseUrl \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Domain/Data/AddressUseCase.kt b/app/src/main/java/ru/otus/basicarchitecture/Domain/Data/AddressUseCase.kt new file mode 100644 index 0000000..ca68e78 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Domain/Data/AddressUseCase.kt @@ -0,0 +1,13 @@ +package ru.otus.basicarchitecture.Domain.Data + +import ru.otus.basicarchitecture.Core.Model.Address +import ru.otus.basicarchitecture.Domain.Repository +import javax.inject.Inject + +class AddressUseCase @Inject constructor( + private val repository: Repository +) { + fun setAddress(address: Address){ + repository.setAddress(address) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Domain/Data/InterestsUseCase.kt b/app/src/main/java/ru/otus/basicarchitecture/Domain/Data/InterestsUseCase.kt new file mode 100644 index 0000000..b9a20e6 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Domain/Data/InterestsUseCase.kt @@ -0,0 +1,17 @@ +package ru.otus.basicarchitecture.Domain.Data + +import ru.otus.basicarchitecture.Core.Model.Interests +import ru.otus.basicarchitecture.Domain.Repository +import javax.inject.Inject + +class InterestsUseCase @Inject constructor( + private val repository: Repository +) { + fun setInterests(interests: Interests){ + repository.setInterests(interests) + } + + fun getList(): List{ + return repository.getListInterests() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Domain/Data/PersonUseCase.kt b/app/src/main/java/ru/otus/basicarchitecture/Domain/Data/PersonUseCase.kt new file mode 100644 index 0000000..6173731 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Domain/Data/PersonUseCase.kt @@ -0,0 +1,22 @@ +package ru.otus.basicarchitecture.Domain.Data + +import ru.otus.basicarchitecture.Core.Model.BaseModel +import ru.otus.basicarchitecture.Core.Model.Person +import ru.otus.basicarchitecture.Domain.Repository +import javax.inject.Inject + +class PersonUseCase @Inject constructor( + private val repository: Repository +) { + fun setPerson(person: Person) { + repository.setPerson(person) + } + + fun getList(): List{ + return repository.getListInterests() + } + + fun getPerson(): Map { + return repository.getInfoPersonUseCase() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Domain/Repository.kt b/app/src/main/java/ru/otus/basicarchitecture/Domain/Repository.kt new file mode 100644 index 0000000..0c3d604 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Domain/Repository.kt @@ -0,0 +1,14 @@ +package ru.otus.basicarchitecture.Domain + +import ru.otus.basicarchitecture.Core.Model.Address +import ru.otus.basicarchitecture.Core.Model.BaseModel +import ru.otus.basicarchitecture.Core.Model.Interests +import ru.otus.basicarchitecture.Core.Model.Person + +interface Repository { + fun setPerson(person: Person) + fun getInfoPersonUseCase() : Map + fun setAddress(address: Address) + fun setInterests(interests: Interests) + fun getListInterests(): List +} \ 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..ae320ee 100644 --- a/app/src/main/java/ru/otus/basicarchitecture/MainActivity.kt +++ b/app/src/main/java/ru/otus/basicarchitecture/MainActivity.kt @@ -1,11 +1,23 @@ package ru.otus.basicarchitecture -import androidx.appcompat.app.AppCompatActivity import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import ru.otus.basicarchitecture.Ui.Fragment1.Fragment1 +import ru.otus.basicarchitecture.Ui.Fragment1.Fragment2 + class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + + if(savedInstanceState == null ){ + val fragment = Fragment2() + supportFragmentManager + .beginTransaction() + .replace(R.id.fragmentContainer, fragment) + .addToBackStack(null) + .commit() + } } } \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Network/Endpoints.kt b/app/src/main/java/ru/otus/basicarchitecture/Network/Endpoints.kt new file mode 100644 index 0000000..ba1403f --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Network/Endpoints.kt @@ -0,0 +1,5 @@ +package ru.otus.basicarchitecture.Network + +object Endpoints { + const val address = "suggestions/api/4_1/rs/suggest/address/" +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Network/NetworkService.kt b/app/src/main/java/ru/otus/basicarchitecture/Network/NetworkService.kt new file mode 100644 index 0000000..a973b5f --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Network/NetworkService.kt @@ -0,0 +1,21 @@ +package ru.otus.basicarchitecture.data + + +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Headers +import retrofit2.http.POST +import retrofit2.http.Query +import ru.otus.basicarchitecture.Core.Model.DTO.SuggestionRequest +import ru.otus.basicarchitecture.Core.Model.DTO.Suggestions +import ru.otus.basicarchitecture.Core.Model.Person +import ru.otus.basicarchitecture.Core.Utils.AppConstant +import ru.otus.basicarchitecture.Network.Endpoints +import javax.inject.Singleton + +@Singleton +interface NetworkService { + @POST(Endpoints.address) + suspend fun getAddress(@Body body: SuggestionRequest): Response +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment1/Fragment1.kt b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment1/Fragment1.kt new file mode 100644 index 0000000..05ed3b2 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment1/Fragment1.kt @@ -0,0 +1,137 @@ +package ru.otus.basicarchitecture.Ui.Fragment1 + +import android.app.DatePickerDialog +import android.content.Context +import android.os.Bundle +import android.text.InputType +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import ru.otus.basicarchitecture.App +import ru.otus.basicarchitecture.Core.Model.ViewModelFactory +import ru.otus.basicarchitecture.R +import ru.otus.basicarchitecture.databinding.Fragment1Binding +import java.util.Calendar +import java.util.Locale +import javax.inject.Inject + +class Fragment1: Fragment() { + private lateinit var binding: Fragment1Binding + @Inject + lateinit var viewModelFactory: ViewModelFactory + private val MESSAGE_TOAST_INCORRECT_AGE ="Возраст пользователя не может быть меньше 18" + + private val viewModel by lazy { + ViewModelProvider(this, viewModelFactory)[Fragment1ViewModel::class.java] + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + binding = Fragment1Binding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + (requireActivity().application as App).component + .fragment1SubComponent() + .build() + .inject(this) + } + + override fun onResume() { + super.onResume() + setupView() + } + + private fun setupView() { + binding.birthInputLayout.apply { + startIconDrawable = ContextCompat.getDrawable(context, R.drawable.calendar) + startIconContentDescription = context.getString(R.string.DateOfBirthHint) + } + binding.birthEditText.apply { + inputType = InputType.TYPE_NULL + // isEnabled = false + isCursorVisible = false + keyListener = null + + onFocusChangeListener = View.OnFocusChangeListener { view, hasFocus -> + if (hasFocus) { + showDatePicker(view.context) + }else{ + viewModel.validateEmptyValue() + } + this.clearFocus() + } + + } + setFocusListener() + + binding.nextButton.let { button -> + viewModel.enabledButtonLiveData.observe(viewLifecycleOwner) { + button.isEnabled = it + } + } + + binding.nextButton.setOnClickListener { + viewModel.setData( + binding.firstNameEditText.text.toString(), + binding.surNameEditText.text.toString(), + binding.birthEditText.text.toString() + ) { openSecondFragment() } + } + } + + + private fun setFocusListener(){ + val lossFocus = View.OnFocusChangeListener{_,hasFocus -> + if(!hasFocus){ + viewModel.validateEmptyValue() + } + } + binding.firstNameEditText.onFocusChangeListener = lossFocus + binding.surNameEditText.onFocusChangeListener = lossFocus + + } + + private fun openSecondFragment() { + val fragment = Fragment2() + parentFragmentManager + .beginTransaction() + .replace(R.id.fragmentContainer, fragment) + .addToBackStack(null) + .commit() + } + + // возможно надо перенести внутрь ViewModel + private fun showDatePicker(context: Context) { + val currentDate = Calendar.getInstance() + val datePicker = DatePickerDialog( + context, + { _, year, month, day -> + val selectedData = + String.format(Locale.getDefault(), "%04d-%02d-%02d", year, month + 1, day) + viewModel.validData(day, month, year, + { showToast(MESSAGE_TOAST_INCORRECT_AGE, context) } + ) + binding.birthEditText.setText(selectedData) + }, + currentDate.get(Calendar.YEAR), + currentDate.get(Calendar.MONTH), + currentDate.get(Calendar.DAY_OF_MONTH) + ) + datePicker.show() + } + + + + + private fun showToast(message: String, context: Context) { + Toast.makeText(context, message, Toast.LENGTH_LONG).show() + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment1/Fragment1ViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment1/Fragment1ViewModel.kt new file mode 100644 index 0000000..b73124f --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment1/Fragment1ViewModel.kt @@ -0,0 +1,75 @@ +package ru.otus.basicarchitecture.Ui.Fragment1 + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import ru.otus.basicarchitecture.Core.Model.Person +import ru.otus.basicarchitecture.Domain.Data.PersonUseCase +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.time.temporal.ChronoUnit +import javax.inject.Inject + +class Fragment1ViewModel @Inject constructor( + val personUseCase: PersonUseCase +) : ViewModel() { + private val enabledButtonMutableLiveData = MutableLiveData() + val enabledButtonLiveData = enabledButtonMutableLiveData + + private var name = DEFFAULT_VALUE + private var surName = DEFFAULT_VALUE + private var birthDate = DEFFAULT_VALUE + + + init { + enabledButtonMutableLiveData.postValue(false) + } + + fun setData(name: String?, surName: String?, birthDate: String?, openFragment: () -> Unit) { + this.name = name ?: DEFFAULT_VALUE + this.surName = surName ?: DEFFAULT_VALUE + this.birthDate = birthDate ?: DEFFAULT_VALUE + if(validateEmptyValue()){ + val person = Person(this.name, this.surName, this.birthDate) + personUseCase.setPerson(person) + openFragment.invoke() + } + } + + fun validateEmptyValue(): Boolean { + return if (name.isEmpty() || surName.isEmpty() || birthDate.isEmpty()) { + enabledButtonMutableLiveData.postValue(false) + false + } else { + enabledButtonMutableLiveData.postValue(true) + true + } + } + + fun validData(day: Int, month: Int, year: Int, showToast: () -> Unit) { + val correctAge = 18 + val currentDate = LocalDate.now() + val monthText = if (month < 9) "0${month + 1}" else "${month + 1}" + val dayText = if (day<10) "0${day+1}" else "$day" + val selectedDateValue = LocalDate.parse("$year-$monthText-$dayText", DateTimeFormatter.ISO_DATE) + val diff = ChronoUnit.YEARS.between(selectedDateValue, currentDate) + if (diff < correctAge) { + setupFalseButton(showToast) + } else { + enabledButtonLiveData.postValue(true) + } + } + + private fun setupFalseButton(showToast: () -> Unit) { + enabledButtonMutableLiveData.postValue(false) + showToast.invoke() + } + + fun enabledButton(){ + enabledButtonMutableLiveData.postValue(true) + } + + + companion object { + private val DEFFAULT_VALUE = "" + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment2/Fragment2.kt b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment2/Fragment2.kt new file mode 100644 index 0000000..eff81fd --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment2/Fragment2.kt @@ -0,0 +1,123 @@ +package ru.otus.basicarchitecture.Ui.Fragment1 + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.widget.doAfterTextChanged +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import ru.otus.basicarchitecture.App +import ru.otus.basicarchitecture.Core.Model.DTO.Suggestion +import ru.otus.basicarchitecture.Core.Model.ViewModelFactory +import ru.otus.basicarchitecture.Core.Utils.toEditable +import ru.otus.basicarchitecture.R +import ru.otus.basicarchitecture.Ui.Fragment2.Fragment2Adapter +import ru.otus.basicarchitecture.Ui.Fragment2.OnItemClickListener +import ru.otus.basicarchitecture.Ui.Fragment3.Fragment3 +import ru.otus.basicarchitecture.databinding.Fragment2Binding +import javax.inject.Inject + +class Fragment2: Fragment() { + private lateinit var binding: Fragment2Binding + @Inject + lateinit var viewModelFactory: ViewModelFactory + lateinit var adapter: Fragment2Adapter + + private val viewModel by lazy { + ViewModelProvider(this, viewModelFactory)[Fragment2ViewModel::class.java] + } + + + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + binding = Fragment2Binding.inflate(inflater, container, false) + return binding.root + } + + + override fun onResume() { + super.onResume() + setupView() + } + + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + (requireActivity().application as App).component + .fragment2SubComponent() + .build() + .inject(this) + + viewModel.errorService.setContext(requireContext()) + setupView() + } + + private fun setupView() { + binding.nextButton.let { button -> + setListeners() + viewModel.enabledButtonLiveData.observe(viewLifecycleOwner) { + button.isEnabled = it + } + } + + + binding.addressField.doAfterTextChanged { + val context = requireContext() + viewModel.loadSuggestions(input = it.toString()) + } + + viewModel.suggestionsLiveData.observe(viewLifecycleOwner) { + Log.d("Server", "Size=" + it.size.toString()) + adapter.addList(it) + } + + viewModel.showProgress.observe(viewLifecycleOwner) { + if(it) { + binding.progressBar.visibility = View.VISIBLE + } else { + binding.progressBar.visibility = View.GONE + } + } + + configureRecycler() + setFocusListener() + } + + fun configureRecycler() { + adapter = Fragment2Adapter(listener = object : OnItemClickListener { + override fun onItemClick(item: Suggestion?) { + item?.value?.let { viewModel.setQuery(it) } + binding.addressField.text = item?.value?.toEditable() + } + }) + binding.recyclerAddress.adapter = adapter + } + + private fun setListeners() { + binding.nextButton.setOnClickListener { + viewModel.setData( + binding.addressField.text.toString() + ) { openFragment() } + } + } + + + private fun openFragment() { + val fragment = Fragment3() + parentFragmentManager + .beginTransaction() + .replace(R.id.fragmentContainer, fragment) + .addToBackStack(null) + .commit() + } + + private fun setFocusListener() { + val lossFocus = View.OnFocusChangeListener { _, hasFocus -> + if (hasFocus) viewModel.buttonEnabled() + } + + binding.addressField.onFocusChangeListener = lossFocus + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment2/Fragment2Adapter.kt b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment2/Fragment2Adapter.kt new file mode 100644 index 0000000..e7aff06 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment2/Fragment2Adapter.kt @@ -0,0 +1,49 @@ +package ru.otus.basicarchitecture.Ui.Fragment2 + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import ru.otus.basicarchitecture.Core.Model.DTO.Suggestion +import ru.otus.basicarchitecture.databinding.AddressLayoutBinding + + +interface OnItemClickListener { + fun onItemClick(item: Suggestion?) +} + +class Fragment2Adapter(private val listener: OnItemClickListener): RecyclerView.Adapter() { + private var items: MutableList = mutableListOf() + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddressItemViewHolder { + val inflater = LayoutInflater.from(parent.context) + val binding = AddressLayoutBinding.inflate(inflater, parent, false) + return AddressItemViewHolder(binding) + } + + override fun onBindViewHolder(holder: AddressItemViewHolder, position: Int) { + holder.bind(items[position]) + holder.itemView.setOnClickListener { + listener.onItemClick(items[position]) + } + } + + override fun getItemCount(): Int { + return items.size + } + + fun setList(list: List) { + items.clear() + addList(list) + } + + fun addList(list: List) { + items.addAll(list) + notifyDataSetChanged() + } + + inner class AddressItemViewHolder(private val binding: AddressLayoutBinding) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: Suggestion) { + binding.name.text = item.value + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment2/Fragment2ViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment2/Fragment2ViewModel.kt new file mode 100644 index 0000000..2ed62d0 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment2/Fragment2ViewModel.kt @@ -0,0 +1,109 @@ +package ru.otus.basicarchitecture.Ui.Fragment1 + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import ru.otus.basicarchitecture.Core.Model.Address +import ru.otus.basicarchitecture.Domain.Data.AddressUseCase +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import retrofit2.http.Query +import ru.otus.basicarchitecture.Core.Model.DTO.Suggestion +import ru.otus.basicarchitecture.Core.Utils.ErrorService +import ru.otus.basicarchitecture.data.AddressImpl +import javax.inject.Inject + +class Fragment2ViewModel @Inject constructor ( + private val addressUseCase: AddressUseCase, + private val repository: AddressImpl, + public val errorService: ErrorService +) : ViewModel() { + + private var prevQuery = "" + private val enabledButtonMutableLiveData = MutableLiveData() + val enabledButtonLiveData = enabledButtonMutableLiveData + + private val mSuggestionsMutableLiveData = MutableLiveData>() + val suggestionsLiveData = mSuggestionsMutableLiveData + + private val mShowProgress = MutableLiveData() + val showProgress = mShowProgress + + + private val UNKNOWN_VALUE = "" + private var country: String = UNKNOWN_VALUE + private var city: String = UNKNOWN_VALUE + private var address: String = UNKNOWN_VALUE + + private var loadingSuggestionsTask: Job = Job() + + + init { + enabledButtonMutableLiveData.postValue(true) + } + + fun setData(address: String?, openFragment: () -> Unit){ + this.address = address ?: UNKNOWN_VALUE + if(validateEmptyValue(this.address)) { + val addres = Address(this.address) + addressUseCase.setAddress(addres) + openFragment.invoke() + } + } + + fun validateEmptyValue(address: String): Boolean { + return if (address.isEmpty()) { + enabledButtonMutableLiveData.postValue(false) + false + } else { + enabledButtonMutableLiveData.postValue(true) + true + } + } + + fun buttonEnabled(){ + enabledButtonMutableLiveData.postValue(true) + } + + + fun loadSuggestions(input: String) { + if (input == prevQuery) { + return + } + prevQuery = input + + loadingSuggestionsTask.cancel() + mShowProgress.value = true + loadingSuggestionsTask = viewModelScope.launch { + delay(2000) + try { + withContext(Dispatchers.IO) { repository.getSuggestions(input) } + .takeIf { it.isSuccess } + ?.let { + mShowProgress.value = false + mSuggestionsMutableLiveData.value = + it.getOrNull() + ?.suggestions + ?.filter { s -> s.value != input } + ?.mapNotNull { s -> s.value?.let { v -> Suggestion(v) } } + ?: emptyList() + } ?: let { + mShowProgress.value = false + errorService.show("Ошибка загрузки") + + } + } catch (t: Throwable) { + mShowProgress.value = false + + } + } + } + + fun setQuery(query: String) { + prevQuery = query + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment3/Fragment3.kt b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment3/Fragment3.kt new file mode 100644 index 0000000..463ac13 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment3/Fragment3.kt @@ -0,0 +1,82 @@ +package ru.otus.basicarchitecture.Ui.Fragment3 + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.GridLayoutManager +import ru.otus.basicarchitecture.App +import ru.otus.basicarchitecture.Core.Model.ViewModelFactory +import ru.otus.basicarchitecture.R +import ru.otus.basicarchitecture.Ui.Fragment4.Fragment4 +import ru.otus.basicarchitecture.databinding.Fragment3Binding +import javax.inject.Inject + +class Fragment3: Fragment() { + private lateinit var binding: Fragment3Binding + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private lateinit var adapter: InterestsAdapter + + private val viewModel by lazy { + ViewModelProvider(this, viewModelFactory)[Fragment3ViewModel::class.java] + } + + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + binding = Fragment3Binding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + (requireActivity().application as App).component + .fragment3SubComponent() + .build() + .inject(this) + } + + override fun onResume() { + super.onResume() + view?.let { setupRV(it) } + setupView() + + + } + + fun setupView() { + binding.nextButton.setOnClickListener { + viewModel.setData { + openFourthFragment() + } + } + + viewModel.listInterestsLD.observe(viewLifecycleOwner) { + adapter.interestsList = it + } + } + + private fun openFourthFragment() { + val fragment = Fragment4() + parentFragmentManager + .beginTransaction() + .replace(R.id.fragmentContainer, fragment) + .addToBackStack(null) + .commit() + } + + private fun setupRV(view: View) { + adapter = InterestsAdapter() + val layoutManager = GridLayoutManager(view.context,3) + binding.recyclerView.adapter = adapter + binding.recyclerView.layoutManager = layoutManager + + adapter.onClickListener = { + viewModel.changeEnabledState(it) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment3/Fragment3ViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment3/Fragment3ViewModel.kt new file mode 100644 index 0000000..8efa961 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment3/Fragment3ViewModel.kt @@ -0,0 +1,61 @@ +package ru.otus.basicarchitecture.Ui.Fragment3 + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import ru.otus.basicarchitecture.Core.Model.Interests +import ru.otus.basicarchitecture.Domain.Data.InterestsUseCase +import ru.otus.basicarchitecture.presentation.Fragment3.ModelInterestsForView +import javax.inject.Inject + +class Fragment3ViewModel @Inject constructor( + private val interestsUseCase: InterestsUseCase +) : ViewModel() { + + private val saveInterests = mutableListOf() + + private val mutableListAllInterests = sortedSetOf( + { o1, o2 -> o1.id.compareTo(o2.id) } + ) + private val _listInterestsLD = MutableLiveData>() + + val listInterestsLD: LiveData> = _listInterestsLD + + init { + val aa = interestsUseCase.getList() + for (a in aa.indices) { + val model = ModelInterestsForView(a, aa[a], false) + mutableListAllInterests.add(model) + } + _listInterestsLD.postValue(mutableListAllInterests.toList()) + } + + fun setData(openFragment: () -> Unit){ + + val list = mutableListOf() + saveInterests.forEach { + list.add(it.interests) + } + interestsUseCase.setInterests( + Interests(list.joinToString()) + ) + openFragment.invoke() + } + + fun changeEnabledState(model: ModelInterestsForView) { + val newModel = model.copy(enabled = !model.enabled) + mutableListAllInterests.remove(model) + mutableListAllInterests.add(newModel) + if(newModel.enabled){ + saveInterests.add(newModel) + }else{ + saveInterests.remove(newModel) + } + updateList() + } + + private fun updateList() { + _listInterestsLD.postValue(mutableListAllInterests.toList()) + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment3/InterestsAdapter.kt b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment3/InterestsAdapter.kt new file mode 100644 index 0000000..7cc9927 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment3/InterestsAdapter.kt @@ -0,0 +1,55 @@ +package ru.otus.basicarchitecture.Ui.Fragment3 + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import ru.otus.basicarchitecture.R +import ru.otus.basicarchitecture.presentation.Fragment3.InterestsDiffUtil +import ru.otus.basicarchitecture.presentation.Fragment3.InterestsViewHolder +import ru.otus.basicarchitecture.presentation.Fragment3.ModelInterestsForView + +class InterestsAdapter: RecyclerView.Adapter() { + + var onClickListener: ((ModelInterestsForView) -> Unit)? = null + + var interestsList = listOf() + set(value){ + val callback = InterestsDiffUtil(interestsList, value) + val diffResult = DiffUtil.calculateDiff(callback) + diffResult.dispatchUpdatesTo(this) + field = value + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InterestsViewHolder { + + val layout = when (viewType) { + DISABLED_VIEW -> R.layout.interests_disabled + ENABLED_VIEW -> R.layout.interests_enabled + else -> throw RuntimeException("Unknown view type: $viewType") + } + val view = LayoutInflater.from(parent.context).inflate(layout, parent, false) + return InterestsViewHolder(view) + } + + override fun onBindViewHolder(holder: InterestsViewHolder, position: Int) { + val item = interestsList[position] + holder.view.setOnClickListener { + onClickListener?.invoke(item) + } + holder.textView.text = item.interests + } + override fun getItemViewType(position: Int): Int { + val item = interestsList[position] + return if (item.enabled) ENABLED_VIEW else DISABLED_VIEW + } + + override fun getItemCount(): Int { + return interestsList.size + } + + companion object { + val ENABLED_VIEW = 100 + val DISABLED_VIEW = 101 + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment3/InterestsDiffUtil.kt b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment3/InterestsDiffUtil.kt new file mode 100644 index 0000000..a56f0e6 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment3/InterestsDiffUtil.kt @@ -0,0 +1,25 @@ +package ru.otus.basicarchitecture.presentation.Fragment3 + +import androidx.recyclerview.widget.DiffUtil + +class InterestsDiffUtil( + private val oldList: List, + private val newList: List, +) : DiffUtil.Callback() { + override fun getOldListSize(): Int { + return oldList.size + } + + override fun getNewListSize(): Int { + return newList.size + } + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldList[oldItemPosition].id == newList[newItemPosition].id + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldList[oldItemPosition] == newList[newItemPosition] + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment3/InterestsViewHolder.kt b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment3/InterestsViewHolder.kt new file mode 100644 index 0000000..f29c1a1 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment3/InterestsViewHolder.kt @@ -0,0 +1,10 @@ +package ru.otus.basicarchitecture.presentation.Fragment3 + +import android.view.View +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import ru.otus.basicarchitecture.R + +class InterestsViewHolder(val view: View) : RecyclerView.ViewHolder(view) { + val textView = view.findViewById(R.id.textInterests) +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment3/ModelInterestsForView.kt b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment3/ModelInterestsForView.kt new file mode 100644 index 0000000..26b18b8 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment3/ModelInterestsForView.kt @@ -0,0 +1,20 @@ +package ru.otus.basicarchitecture.presentation.Fragment3 + +import ru.otus.basicarchitecture.Core.Model.Interests + +data class ModelInterestsForView( + var id: Int, + val interests: String, + var enabled: Boolean +){ + + companion object { + fun toDomain(modelView: ModelInterestsForView): Interests { + return Interests(modelView.interests) + } + + fun toModel(id: Int, interests: Interests): ModelInterestsForView { + return ModelInterestsForView(id, interests.interests, false) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment4/Fragment4.kt b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment4/Fragment4.kt new file mode 100644 index 0000000..511a71e --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment4/Fragment4.kt @@ -0,0 +1,66 @@ +package ru.otus.basicarchitecture.Ui.Fragment4 + +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.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.StaggeredGridLayoutManager +import ru.otus.basicarchitecture.App +import ru.otus.basicarchitecture.Core.Model.ViewModelFactory +import ru.otus.basicarchitecture.R +import ru.otus.basicarchitecture.databinding.Fragment3Binding +import ru.otus.basicarchitecture.databinding.Fragment4Binding + +import javax.inject.Inject + +class Fragment4: Fragment() { + private lateinit var binding: Fragment4Binding + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + + private val viewModel by lazy{ + ViewModelProvider(this, viewModelFactory)[Fragment4ViewModel::class.java] + } + + private var adapter = Fragment4Adapter() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + binding = Fragment4Binding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + (requireActivity().application as App).component + .fragment4SubComponent() + .build() + .inject(this) + } + + override fun onResume() { + super.onResume() + setupView() + } + + private fun setupView(){ + binding.Name.text = viewModel.fragment4Model.name + binding.Surname.text = viewModel.fragment4Model.surName + binding.DateOfBirth.text = viewModel.fragment4Model.birthDate + binding.Address.text = viewModel.fragment4Model.fullAddress + adapter.listInterestsInfo = viewModel.fragment4Model.interests + + val spanCount = 2 + val orientation = StaggeredGridLayoutManager.VERTICAL + val layoutManager = StaggeredGridLayoutManager(spanCount, orientation) + + binding.recyclerView4.layoutManager = layoutManager + binding.recyclerView4.adapter = adapter + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment4/Fragment4Adapter.kt b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment4/Fragment4Adapter.kt new file mode 100644 index 0000000..18b5f81 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment4/Fragment4Adapter.kt @@ -0,0 +1,31 @@ +package ru.otus.basicarchitecture.Ui.Fragment4 + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import ru.otus.basicarchitecture.R +import ru.otus.basicarchitecture.presentation.Fragment3.InterestsViewHolder + + +class Fragment4Adapter : RecyclerView.Adapter() { + + var listInterestsInfo: List = listOf() + set(value) { + field = value + notifyDataSetChanged() + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InterestsViewHolder { + val layout = R.layout.interests_disabled + val view = LayoutInflater.from(parent.context).inflate(layout, parent,false) + return InterestsViewHolder(view) + } + + override fun getItemCount(): Int { + return listInterestsInfo.size + } + + override fun onBindViewHolder(holder: InterestsViewHolder, position: Int) { + val item = listInterestsInfo[position] + holder.textView.text = item + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment4/Fragment4Model.kt b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment4/Fragment4Model.kt new file mode 100644 index 0000000..dadcf6b --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment4/Fragment4Model.kt @@ -0,0 +1,9 @@ +package ru.otus.basicarchitecture.Ui.Fragment4 + +data class Fragment4Model( + val name: String, + val surName: String, + val birthDate: String, + val fullAddress: String, + val interests: List +) \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment4/Fragment4ViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment4/Fragment4ViewModel.kt new file mode 100644 index 0000000..27d66d4 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/Ui/Fragment4/Fragment4ViewModel.kt @@ -0,0 +1,30 @@ +package ru.otus.basicarchitecture.Ui.Fragment4 + +import androidx.lifecycle.ViewModel +import ru.otus.basicarchitecture.Core.Model.Address +import ru.otus.basicarchitecture.Core.Model.Interests +import ru.otus.basicarchitecture.Core.Model.Person +import ru.otus.basicarchitecture.Domain.Data.PersonUseCase +import ru.otus.basicarchitecture.Ui.Fragment4.Fragment4Model +import javax.inject.Inject + +class Fragment4ViewModel @Inject constructor( + info: PersonUseCase +) : ViewModel() { + + val fragment4Model: Fragment4Model + + init { + val information = info.getPerson() + val person = information["person"] as Person + val address = information["address"] as Address + val interest = information["interests"] as Interests + fragment4Model = Fragment4Model( + person.firstName, + person.surName, + person.dateOfBirth, + address.address, + interest.interests.split(",") + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/data/AddressImpl.kt b/app/src/main/java/ru/otus/basicarchitecture/data/AddressImpl.kt new file mode 100644 index 0000000..00ec059 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/data/AddressImpl.kt @@ -0,0 +1,21 @@ +package ru.otus.basicarchitecture.data + +import retrofit2.Response +import ru.otus.basicarchitecture.Core.Model.DTO.SuggestionRequest +import java.io.IOException +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AddressImpl @Inject constructor(private val networkService: NetworkService) { + + suspend fun getSuggestions(input: String) = networkCall { + networkService.getAddress(SuggestionRequest(input)) + } + + 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/data/RepositoryImpl.kt b/app/src/main/java/ru/otus/basicarchitecture/data/RepositoryImpl.kt new file mode 100644 index 0000000..5f7b038 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/data/RepositoryImpl.kt @@ -0,0 +1,91 @@ +package ru.otus.basicarchitecture.data + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import ru.otus.basicarchitecture.Core.Model.Address +import ru.otus.basicarchitecture.Core.Model.BaseModel +import ru.otus.basicarchitecture.Core.Model.Interests +import ru.otus.basicarchitecture.Core.Model.Person +import ru.otus.basicarchitecture.Domain.Repository +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class RepositoryImpl @Inject constructor( + private val wizardCache: WizardCache +) : Repository { + + + private val listInterests = listOf( + "Йога и медитация", + "Игры настольные", + "Пение в караоке", + "Реставрация старинной мебели", + "Занятия танцами", + "Развитие навыков программирования", + "Садоводство и уход за растениями", + "Фотография архитектурных сооружений", + "Шитье и создание одежды", + "Философия и обсуждение философских вопросов", + "Развитие навыков игры на музыкальных инструментах", + "Прослушивание подкастов о науке", + "Рукоделие и создание поделок", + "Экстримальные виды спорта: парашютный спорт, сноубординг, скейтбординг" + ) + + + + + private val mutableLiveDataWizardCache = MutableLiveData() + + val liveDataWizardCache: LiveData = mutableLiveDataWizardCache + + + + override fun setPerson(person: Person) { + wizardCache.firstName = person.firstName + wizardCache.surName = person.surName + wizardCache.dateOfBirth = person.firstName + updateWizardCache() + } + + override fun setAddress(address: Address) { + wizardCache.address = address.address + updateWizardCache() + } + + private fun updateWizardCache() { + mutableLiveDataWizardCache.postValue(wizardCache) + } + + override fun getListInterests(): List = listInterests + + override fun setInterests(interests: Interests) { + wizardCache.interests = interests.interests + updateWizardCache() + } + + override fun getInfoPersonUseCase(): Map { + val person = Person( + wizardCache.firstName, + wizardCache.surName, + wizardCache.dateOfBirth + ) + + val address = Address( + wizardCache.address + ) + + val interests = Interests( + wizardCache.interests + ) + + + return mapOf( + "person" to person, + "address" to address, + "interests" to interests + ) + } + +} \ 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..4db9cfc --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/data/WizardCache.kt @@ -0,0 +1,19 @@ +package ru.otus.basicarchitecture.data + + +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +data class WizardCache( + //person + var firstName: String, + var surName: String, + var dateOfBirth: String, + + //address + var address: String, + + //interests + var interests: String +) \ No newline at end of file diff --git a/app/src/main/res/drawable/background_state_button.xml b/app/src/main/res/drawable/background_state_button.xml new file mode 100644 index 0000000..4ababae --- /dev/null +++ b/app/src/main/res/drawable/background_state_button.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_selector.xml b/app/src/main/res/drawable/button_selector.xml new file mode 100644 index 0000000..132c26d --- /dev/null +++ b/app/src/main/res/drawable/button_selector.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/calendar.xml b/app/src/main/res/drawable/calendar.xml new file mode 100644 index 0000000..0083b28 --- /dev/null +++ b/app/src/main/res/drawable/calendar.xml @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/form_text_view_for_rv.xml b/app/src/main/res/drawable/form_text_view_for_rv.xml new file mode 100644 index 0000000..c01d672 --- /dev/null +++ b/app/src/main/res/drawable/form_text_view_for_rv.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ 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..d242c41 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -6,4 +6,9 @@ android:layout_height="match_parent" tools:context=".MainActivity"> + + \ No newline at end of file diff --git a/app/src/main/res/layout/address_layout.xml b/app/src/main/res/layout/address_layout.xml new file mode 100644 index 0000000..01da18c --- /dev/null +++ b/app/src/main/res/layout/address_layout.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment1.xml b/app/src/main/res/layout/fragment1.xml new file mode 100644 index 0000000..e39657a --- /dev/null +++ b/app/src/main/res/layout/fragment1.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +