diff --git a/app/build.gradle b/app/build.gradle index e515992..9fea610 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,10 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' + id 'com.google.devtools.ksp' + id 'com.google.dagger.hilt.android' + id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' + id 'kotlinx-serialization' } android { @@ -9,12 +13,12 @@ android { defaultConfig { applicationId "ru.otus.basicarchitecture" - minSdk 24 + minSdk 26 targetSdk 35 versionCode 1 versionName "1.0" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "dagger.hilt.android.testing.HiltTestRunner" } buildTypes { @@ -30,15 +34,47 @@ android { kotlinOptions { jvmTarget = '17' } + buildFeatures { + viewBinding = true + buildConfig = true + } + testOptions { + unitTests { + includeAndroidResources = true + returnDefaultValues = true + } + } } dependencies { - implementation 'androidx.core:core-ktx:1.15.0' - implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'androidx.core:core-ktx:1.16.0' + implementation 'androidx.appcompat:appcompat:1.7.1' implementation 'com.google.android.material:material:1.12.0' - implementation 'androidx.constraintlayout:constraintlayout:2.2.0' + implementation 'androidx.constraintlayout:constraintlayout:2.2.1' + implementation 'androidx.navigation:navigation-fragment-ktx:2.9.0' + implementation 'androidx.navigation:navigation-ui-ktx:2.9.0' + implementation 'androidx.databinding:viewbinding:8.11.0' + implementation 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1' + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3' + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3' + implementation 'com.squareup.okhttp3:okhttp:4.12.0' + implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0' + implementation 'com.squareup.retrofit2:retrofit:2.11.0' + implementation 'com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0' + implementation 'com.google.dagger:hilt-android:2.56.2' + ksp 'com.google.dagger:hilt-android-compiler:2.56.2' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.2.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' -} \ No newline at end of file + androidTestImplementation 'androidx.test:core:1.5.0' + testImplementation 'androidx.arch.core:core-testing:2.2.0' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2' + 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' + androidTestImplementation 'com.google.dagger:hilt-android-testing:2.56.2' + testImplementation 'com.google.dagger:hilt-android-testing:2.55' + testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1' + testImplementation 'net.bytebuddy:byte-buddy:1.14.15' +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ea17fa5..4346e2f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,10 @@ + + = interestsMap +} + + +@Module +@InstallIn(ActivityRetainedComponent::class) +object WizardCacheModule { + @Provides + @ActivityRetainedScoped + fun wizardCache(): WizardCache = WizardCache() +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/net/AddressSuggestionApi.kt b/app/src/main/java/ru/otus/basicarchitecture/net/AddressSuggestionApi.kt new file mode 100644 index 0000000..275f8ab --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/net/AddressSuggestionApi.kt @@ -0,0 +1,31 @@ +package ru.otus.basicarchitecture.net + +import kotlinx.serialization.json.Json +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import kotlinx.serialization.Serializable +import okhttp3.OkHttpClient +import okhttp3.MediaType.Companion.toMediaType +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.http.Body +import retrofit2.http.POST + +private const val baseUrl = "https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/" + +@Serializable data class SuggestionQuery(val query:String) +@Serializable data class Suggestion(val value:String) +@Serializable data class Suggestions(val suggestions: List) + +interface AddressSuggestionApi { + @POST("address") + suspend fun getSuggestions(@Body query: SuggestionQuery): Response +} + +fun buildRetrofit(okHttpClient: OkHttpClient): Retrofit { + val json = Json { ignoreUnknownKeys = true } + return Retrofit.Builder() + .baseUrl(baseUrl) + .client(okHttpClient) + .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) + .build() +} diff --git a/app/src/main/java/ru/otus/basicarchitecture/net/AuthInterceptor.kt b/app/src/main/java/ru/otus/basicarchitecture/net/AuthInterceptor.kt new file mode 100644 index 0000000..0e754e7 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/net/AuthInterceptor.kt @@ -0,0 +1,16 @@ +package ru.otus.basicarchitecture.net + +import okhttp3.Interceptor +import okhttp3.Response +import javax.inject.Inject + +class AuthInterceptor @Inject constructor(private val sessionManager: SessionManager): Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val requestWithAuth = request.newBuilder() + .header("Authorization", "Token ${sessionManager.getToken()}") + .build() + + return chain.proceed(requestWithAuth) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/net/GetSuggestion.kt b/app/src/main/java/ru/otus/basicarchitecture/net/GetSuggestion.kt new file mode 100644 index 0000000..7050280 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/net/GetSuggestion.kt @@ -0,0 +1,18 @@ +package ru.otus.basicarchitecture.net + +import java.io.IOException +import javax.inject.Inject + +interface GetSuggestions { + suspend operator fun invoke(query: String): Suggestions + + class Impl @Inject constructor(private val api: AddressSuggestionApi) : GetSuggestions { + override suspend fun invoke(query: String): Suggestions { + val response = api.getSuggestions(SuggestionQuery(query)) + if (!response.isSuccessful) { + throw IOException("Unexpected code $response") + } + return response.body() ?: throw IOException("Empty body $response") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/net/NetService.kt b/app/src/main/java/ru/otus/basicarchitecture/net/NetService.kt new file mode 100644 index 0000000..ebc87cc --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/net/NetService.kt @@ -0,0 +1,13 @@ +package ru.otus.basicarchitecture.net + +import javax.inject.Inject + +interface NetService { + suspend fun getSuggestions(query: String): Suggestions + + class Impl @Inject constructor( + private val getSuggestionCommand: GetSuggestions, + ) : NetService { + override suspend fun getSuggestions(query: String): Suggestions = getSuggestionCommand(query) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/net/SessionManager.kt b/app/src/main/java/ru/otus/basicarchitecture/net/SessionManager.kt new file mode 100644 index 0000000..be43999 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/net/SessionManager.kt @@ -0,0 +1,12 @@ +package ru.otus.basicarchitecture.net + +import ru.otus.basicarchitecture.BuildConfig +import javax.inject.Inject + +interface SessionManager { + fun getToken(): String + + class Impl @Inject constructor() : SessionManager { + override fun getToken(): String = BuildConfig.daDataApiKey + } +} diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/Chips.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/Chips.kt new file mode 100644 index 0000000..c312200 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/Chips.kt @@ -0,0 +1,43 @@ +package ru.otus.basicarchitecture.ui + +import android.content.Context +import com.google.android.material.chip.Chip +import com.google.android.material.chip.ChipGroup +import com.google.android.material.shape.CornerFamily +import com.google.android.material.shape.ShapeAppearanceModel +import ru.otus.basicarchitecture.R + +object InterestChips { + + private fun getCornerRadius(context: Context): Float = + context.resources.getDimensionPixelSize(R.dimen.chip_corner_radius).toFloat() + + fun load(chipGroup: ChipGroup, tags: Map, style: (Chip.() -> Unit) = { + isClickable = true + }) { + val sam = ShapeAppearanceModel.builder() + .setAllCorners(CornerFamily.ROUNDED, getCornerRadius(chipGroup.context)) + .build() + + tags.forEach { tag -> + val chip = Chip(chipGroup.context).apply { + style() + isCheckable = isClickable + + if (isCheckable) { + isChecked = tag.value + /*setTextColor( + resources.getColor( + com.google.android.material.R.color.design_default_color_primary_variant + ) + )*/ + } + shapeAppearanceModel = sam + text = tag.key + } + + if (chip.isCheckable) chipGroup.addView(chip) + else if (tag.value) chipGroup.addView(chip) + } + } +} diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/FragmentBindingDelegate.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/FragmentBindingDelegate.kt new file mode 100644 index 0000000..9553878 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/FragmentBindingDelegate.kt @@ -0,0 +1,49 @@ +package ru.otus.basicarchitecture.ui + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.viewbinding.ViewBinding + +/** + * Binds fragment view-binding + */ +class FragmentBindingDelegate(private val fragment: Fragment) { + + private var binding: VB? = null + + /** + * Binds fragment view-binding + * Put inside `onCreateView` + * See: https://developer.android.com/topic/libraries/view-binding#fragments + * @param container View container + * @param inflate Binding inflater + */ + fun bind( + container: ViewGroup?, + inflate: (LayoutInflater, ViewGroup?, Boolean) -> VB + ): View { + fragment.viewLifecycleOwner.lifecycle.addObserver(BindingDestroyer()) + binding = inflate(fragment.layoutInflater, container, false) + return binding!!.root + } + + /** + * Runs [block] with binding + */ + fun withBinding(block: VB.() -> R): R { + return checkNotNull(binding) { "Binding is not initialized" }.block() + } + + /** + * Destroys binding on view destroy + */ + private inner class BindingDestroyer : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + binding = null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/address/AddressFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/address/AddressFragment.kt new file mode 100644 index 0000000..76ff16b --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/address/AddressFragment.kt @@ -0,0 +1,101 @@ +package ru.otus.basicarchitecture.ui.address + +import android.content.Context +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.widget.ArrayAdapter +import android.widget.TextView.OnEditorActionListener +import androidx.core.widget.doAfterTextChanged +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import dagger.hilt.android.AndroidEntryPoint +import ru.otus.basicarchitecture.R +import ru.otus.basicarchitecture.databinding.FragmentAddressBinding +import ru.otus.basicarchitecture.net.Suggestion +import ru.otus.basicarchitecture.ui.FragmentBindingDelegate + + +@AndroidEntryPoint +class AddressFragment : Fragment() { + + private val binding = FragmentBindingDelegate(this) + private val viewModel: AddressViewModel by viewModels() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View = binding.bind( + container, + FragmentAddressBinding::inflate + ) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.withBinding { + viewModel.getFromCache() + addressInput.setText(viewModel.data) + addressInput.addTextChangedListener(object: TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(editable: Editable?) { + viewModel.getSuggestions(editable.toString()) + } + + }) + addressInput.doAfterTextChanged { + viewModel.setAddress(it.toString()) + } + + addressInput.setOnEditorActionListener(OnEditorActionListener { v, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_ACTION_NEXT) { + addressInput.dismissDropDown() + val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(v.windowToken, 0) + return@OnEditorActionListener true // Focus will do whatever you put in the logic. + } + false // Focus will change according to the actionId + }) + + viewModel.suggestions.observe(viewLifecycleOwner) { + if (it != null) { + val adapter = ArrayAdapter( + requireContext(), + android.R.layout.simple_list_item_1, + it.suggestions.map(Suggestion::value) + ) + addressInput.setAdapter(adapter) + addressInput.popupElevation + if (addressInput.applicationWindowToken != null) { + addressInput.showDropDown() + } + } + else { + addressInput.dismissDropDown() + } + } + + viewModel.showNext.observe(viewLifecycleOwner) { + txtFillFields.visibility = if (it) View.INVISIBLE else View.VISIBLE + buttonNext.isEnabled = it + } + + buttonNext.setOnClickListener { + viewModel.putToCache() + findNavController().navigate(R.id.action_to_interests) + } + } + } + + override fun onDestroyView() { + viewModel.stopSuggestions() + super.onDestroyView() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/address/AddressViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/address/AddressViewModel.kt new file mode 100644 index 0000000..bc0ef12 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/address/AddressViewModel.kt @@ -0,0 +1,107 @@ +package ru.otus.basicarchitecture.ui.address + +import android.util.Log +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import ru.otus.basicarchitecture.data.WizardCache +import ru.otus.basicarchitecture.net.AddressSuggestionApi +import ru.otus.basicarchitecture.net.AuthInterceptor +import ru.otus.basicarchitecture.net.GetSuggestions +import ru.otus.basicarchitecture.net.NetService +import ru.otus.basicarchitecture.net.Suggestions +import ru.otus.basicarchitecture.net.buildRetrofit +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +@HiltViewModel +class AddressViewModel @Inject constructor( + private val wizardCache: WizardCache, + private val service: NetService +): ViewModel() { + + var data: String = "" + var showNext = MutableLiveData(false) + var suggestions = MutableLiveData(null) + private var activeJob: Job? = null + + private fun checkData() { + showNext.value = data.isNotEmpty() + } + + fun setAddress(address: String) { + data = address + checkData() + } + + fun getFromCache() { + data = wizardCache.address + checkData() + } + + fun putToCache() { + wizardCache.address = data + } + + fun getSuggestions(edit: String) { + if (edit != data) { + activeJob?.cancel() + activeJob = viewModelScope.launch { + try { + suggestions.value = service.getSuggestions(edit) + } catch (e: Exception) { + Log.e("address viewmodel getSuggestions Error: ", e.message.toString()) + } + } + } + else { + suggestions.value = null + } + } + + fun stopSuggestions() { + activeJob?.cancel() + activeJob = null + } +} + +@Module +@InstallIn(ViewModelComponent::class) +abstract class MainModule { + @Binds + abstract fun netService(impl: NetService.Impl) : NetService + + @Binds + abstract fun getSuggestions(impl: GetSuggestions.Impl) : GetSuggestions +} + +@Module +@InstallIn(ViewModelComponent::class) +class MainModuleProvider { + @Provides + fun okHttp(authInterceptor: AuthInterceptor): OkHttpClient = OkHttpClient.Builder() + .callTimeout(10, TimeUnit.SECONDS) + .addInterceptor(authInterceptor) + .addInterceptor(HttpLoggingInterceptor().apply { + setLevel(HttpLoggingInterceptor.Level.BASIC) + }) + .build() + + @Provides + fun retrofit(okHttp: OkHttpClient): Retrofit = buildRetrofit(okHttp) + + @Provides + fun api(retrofit: Retrofit): AddressSuggestionApi = + retrofit.create(AddressSuggestionApi::class.java) +} diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/interests/InterestsFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/interests/InterestsFragment.kt new file mode 100644 index 0000000..1672a68 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/interests/InterestsFragment.kt @@ -0,0 +1,51 @@ +package ru.otus.basicarchitecture.ui.interests + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.children +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import com.google.android.material.chip.Chip +import dagger.hilt.android.AndroidEntryPoint +import ru.otus.basicarchitecture.R +import ru.otus.basicarchitecture.ui.InterestChips +import ru.otus.basicarchitecture.databinding.FragmentInterestsBinding +import ru.otus.basicarchitecture.ui.FragmentBindingDelegate + +@AndroidEntryPoint +class InterestsFragment : Fragment() { + + private val binding = FragmentBindingDelegate(this) + private val viewModel: InterestsViewModel by viewModels() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View = binding.bind( + container, + FragmentInterestsBinding::inflate + ) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.withBinding { + viewModel.getFromCache() + + InterestChips.load(interestsChips, viewModel.data) + + buttonNext.setOnClickListener { + val map = mutableMapOf() + interestsChips.children.toList().filterIsInstance().map { + map.put(it.text.toString(), it.isChecked) + } + viewModel.data = map + viewModel.putToCache() + findNavController().navigate(R.id.action_to_summary) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/interests/InterestsViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/interests/InterestsViewModel.kt new file mode 100644 index 0000000..c4e40e2 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/interests/InterestsViewModel.kt @@ -0,0 +1,20 @@ +package ru.otus.basicarchitecture.ui.interests + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import ru.otus.basicarchitecture.data.WizardCache +import javax.inject.Inject + +@HiltViewModel +class InterestsViewModel @Inject constructor(private val wizardCache: WizardCache): ViewModel() { + + var data: Map = wizardCache.interests + + fun getFromCache() { + data = wizardCache.interests + } + + fun putToCache() { + wizardCache.interests = data + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/name/NameFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/name/NameFragment.kt new file mode 100644 index 0000000..4340c9b --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/name/NameFragment.kt @@ -0,0 +1,104 @@ +package ru.otus.basicarchitecture.ui.name + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.widget.doAfterTextChanged +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import com.google.android.material.datepicker.MaterialDatePicker +import dagger.hilt.android.AndroidEntryPoint +import ru.otus.basicarchitecture.R +import ru.otus.basicarchitecture.databinding.FragmentNameBinding +import ru.otus.basicarchitecture.ui.FragmentBindingDelegate +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.Calendar + +@AndroidEntryPoint +class NameFragment : Fragment() { + + private val binding = FragmentBindingDelegate(this) + private val viewModel: NameViewModel by viewModels() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View = binding.bind( + container, + FragmentNameBinding::inflate + ) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.withBinding { + viewModel.getFromCache() + + viewModel.showNext.observe(viewLifecycleOwner) { + buttonNext.isEnabled = it + } + + viewModel.showAgeRestricted.observe(viewLifecycleOwner) { + txtRestrictedAccess.visibility = + if (it) View.VISIBLE else View.INVISIBLE + } + + viewModel.showFieldsEmpty.observe(viewLifecycleOwner) { + txtFillFields.visibility = + if (it) View.VISIBLE else View.INVISIBLE + } + + nameInput.setText(viewModel.data.name) + surnameInput.setText(viewModel.data.surName) + dateInput.setText(formatDate(viewModel.data.birthDate)) + + dateInput.setOnClickListener { + pickDate(viewModel.data.birthDate) { newDate -> + dateInput.setText(formatDate(newDate)) + viewModel.setBirthDate(newDate) + } + } + + nameInput.doAfterTextChanged { viewModel.setName(it.toString()) } + surnameInput.doAfterTextChanged { viewModel.setSurName(it.toString()) } + + buttonNext.setOnClickListener { + viewModel.putToCache() + findNavController().navigate(R.id.action_to_address) + } + } + } + + private fun formatDate(date: LocalDate): String = date.format( + DateTimeFormatter.ofPattern(resources.getText(R.string.date_pattern).toString()) + ) + + private fun pickDate(selDate: LocalDate, newDateHandler: (LocalDate) -> Unit ) { + + binding.withBinding { + + val calendar = Calendar.getInstance() + calendar.set(selDate.year,selDate.monthValue - 1,selDate.dayOfMonth) + + val datePicker = MaterialDatePicker.Builder.datePicker() + .setTitleText(resources.getText(R.string.select_birthday)) + .setInputMode(MaterialDatePicker.INPUT_MODE_TEXT) + .setSelection(calendar.timeInMillis) + .build() + + datePicker.addOnPositiveButtonClickListener { selectedDate -> + val newDate = Instant.ofEpochMilli(selectedDate) + .atZone(ZoneId.systemDefault()) + .toLocalDate() + + newDateHandler(newDate) + } + datePicker.show(parentFragmentManager, "DATE_PICKER") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/name/NameViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/name/NameViewModel.kt new file mode 100644 index 0000000..9c7fb82 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/name/NameViewModel.kt @@ -0,0 +1,56 @@ +package ru.otus.basicarchitecture.ui.name + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import ru.otus.basicarchitecture.data.PersonName +import ru.otus.basicarchitecture.data.WizardCache +import java.time.LocalDate +import java.time.Period +import javax.inject.Inject + +@HiltViewModel +class NameViewModel @Inject constructor(private val wizardCache: WizardCache): ViewModel() { + + var data = PersonName("", "", LocalDate.now()) + + var showNext = MutableLiveData(false) + var showFieldsEmpty = MutableLiveData(false) + var showAgeRestricted = MutableLiveData(false) + + private fun checkAge(): Boolean = Period.between(data.birthDate, LocalDate.now()).years >= 18 + private fun checkName(): Boolean = data.name.isNotEmpty() && data.surName.isNotEmpty() + + private fun checkData() { + val ageOk = checkAge() + val nameOk = checkName() + + showNext.value = ageOk && nameOk + showAgeRestricted.value = !ageOk && nameOk + showFieldsEmpty.value = !nameOk + } + + fun setName(name: String) { + data = PersonName(name, data.surName, data.birthDate) + checkData() + } + + fun setSurName(surName: String) { + data = PersonName(data.name, surName, data.birthDate) + checkData() + } + + fun setBirthDate(date: LocalDate) { + data = PersonName(data.name, data.surName, date) + checkData() + } + + fun getFromCache() { + data = wizardCache.name + checkData() + } + + fun putToCache() { + wizardCache.name = data + } +} diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/summary/SummaryFragment.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/summary/SummaryFragment.kt new file mode 100644 index 0000000..13fc0fb --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/summary/SummaryFragment.kt @@ -0,0 +1,47 @@ +package ru.otus.basicarchitecture.ui.summary + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import dagger.hilt.android.AndroidEntryPoint +import ru.otus.basicarchitecture.R +import ru.otus.basicarchitecture.databinding.FragmentSummaryBinding +import ru.otus.basicarchitecture.ui.FragmentBindingDelegate +import ru.otus.basicarchitecture.ui.InterestChips +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +@AndroidEntryPoint +class SummaryFragment : Fragment() { + + private val binding = FragmentBindingDelegate(this) + private val viewModel: SummaryViewModel by viewModels() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View = binding.bind( + container, + FragmentSummaryBinding::inflate + ) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.withBinding { + name.text = viewModel.name.name + surname.text = viewModel.name.surName + birthDate.text = formatDate(viewModel.name.birthDate) + address.text = viewModel.address + + InterestChips.load(interestsChips, viewModel.interests) { isClickable = false } + } + } + + private fun formatDate(date: LocalDate): String = date.format( + DateTimeFormatter.ofPattern(resources.getText(R.string.date_pattern).toString()) + ) +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/basicarchitecture/ui/summary/SummaryViewModel.kt b/app/src/main/java/ru/otus/basicarchitecture/ui/summary/SummaryViewModel.kt new file mode 100644 index 0000000..0d5bc34 --- /dev/null +++ b/app/src/main/java/ru/otus/basicarchitecture/ui/summary/SummaryViewModel.kt @@ -0,0 +1,13 @@ +package ru.otus.basicarchitecture.ui.summary + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import ru.otus.basicarchitecture.data.WizardCache +import javax.inject.Inject + +@HiltViewModel +class SummaryViewModel @Inject constructor(private val wizardCache: WizardCache): ViewModel() { + var name = wizardCache.name + var address = wizardCache.address + var interests = wizardCache.interests +} \ 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..58950a1 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -6,4 +6,11 @@ android:layout_height="match_parent" tools:context=".MainActivity"> + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_address.xml b/app/src/main/res/layout/fragment_address.xml new file mode 100644 index 0000000..6e5ca78 --- /dev/null +++ b/app/src/main/res/layout/fragment_address.xml @@ -0,0 +1,43 @@ + + + + + + + + + +