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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 42 additions & 6 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 {
Expand All @@ -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'
}
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'
}
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

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

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

import android.app.Application
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.HiltAndroidApp
import dagger.hilt.components.SingletonComponent
import ru.otus.basicarchitecture.net.SessionManager
import javax.inject.Singleton

@HiltAndroidApp
class App : Application()

@Module
@InstallIn(SingletonComponent::class)
abstract class AppModule {
@Binds
@Singleton
abstract fun sessionManager(impl: SessionManager.Impl): SessionManager
}
10 changes: 10 additions & 0 deletions app/src/main/java/ru/otus/basicarchitecture/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,20 @@ package ru.otus.basicarchitecture

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.addCallback
import androidx.navigation.findNavController
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

onBackPressedDispatcher.addCallback(this) {
if (!findNavController(R.id.fragment_container_view).popBackStack()) {
finish()
}
}
}
}
9 changes: 9 additions & 0 deletions app/src/main/java/ru/otus/basicarchitecture/data/Person.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ru.otus.basicarchitecture.data

import java.time.LocalDate

data class PersonName(
val name: String,
val surName: String,
val birthDate: LocalDate
)
35 changes: 35 additions & 0 deletions app/src/main/java/ru/otus/basicarchitecture/data/WizardCache.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package ru.otus.basicarchitecture.data

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityRetainedComponent
import dagger.hilt.android.scopes.ActivityRetainedScoped
import java.time.LocalDate

private val interestsMap = mapOf(
Pair("Sports", false),
Pair("Music", false),
Pair("Fine Arts", false),
Pair("Books", false),
Pair("Travels", false),
Pair("Dancing", false),
Pair("Computer Games", false),
Pair("Food", false),
Pair("Pets", false)
)

class WizardCache {
var name: PersonName = PersonName("", "", LocalDate.now())
var address: String = ""
var interests: Map<String, Boolean> = interestsMap
}


@Module
@InstallIn(ActivityRetainedComponent::class)
object WizardCacheModule {
@Provides
@ActivityRetainedScoped
fun wizardCache(): WizardCache = WizardCache()
}
Original file line number Diff line number Diff line change
@@ -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<Suggestion>)

interface AddressSuggestionApi {
@POST("address")
suspend fun getSuggestions(@Body query: SuggestionQuery): Response<Suggestions>
}

fun buildRetrofit(okHttpClient: OkHttpClient): Retrofit {
val json = Json { ignoreUnknownKeys = true }
return Retrofit.Builder()
.baseUrl(baseUrl)
.client(okHttpClient)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
}
16 changes: 16 additions & 0 deletions app/src/main/java/ru/otus/basicarchitecture/net/AuthInterceptor.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
18 changes: 18 additions & 0 deletions app/src/main/java/ru/otus/basicarchitecture/net/GetSuggestion.kt
Original file line number Diff line number Diff line change
@@ -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")
}
}
}
13 changes: 13 additions & 0 deletions app/src/main/java/ru/otus/basicarchitecture/net/NetService.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
12 changes: 12 additions & 0 deletions app/src/main/java/ru/otus/basicarchitecture/net/SessionManager.kt
Original file line number Diff line number Diff line change
@@ -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
}
}
43 changes: 43 additions & 0 deletions app/src/main/java/ru/otus/basicarchitecture/ui/Chips.kt
Original file line number Diff line number Diff line change
@@ -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<String, Boolean>, 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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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<VB : ViewBinding>(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 <R> 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
}
}
}
Loading