Merge pull request 'sprint-2' (#1) from sprint-2 into master
Reviewed-on: #1
This commit is contained in:
commit
8a52a8537b
1
api-core/.gitignore
vendored
Normal file
1
api-core/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
52
api-core/build.gradle.kts
Normal file
52
api-core/build.gradle.kts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.library)
|
||||||
|
alias(libs.plugins.kotlin.android)
|
||||||
|
alias(libs.plugins.kotlin.serialization)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.example.api.core"
|
||||||
|
compileSdk = 35
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 33
|
||||||
|
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
consumerProguardFiles("consumer-rules.pro")
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "11"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
implementation(libs.androidx.datastore.preferences)
|
||||||
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
implementation(libs.retrofit2.kotlinx.serialization.converter)
|
||||||
|
implementation(libs.retrofit2)
|
||||||
|
implementation(libs.okhttp)
|
||||||
|
implementation(libs.logging.interceptor)
|
||||||
|
|
||||||
|
implementation(libs.androidx.core.ktx)
|
||||||
|
implementation(libs.androidx.appcompat)
|
||||||
|
implementation(libs.material)
|
||||||
|
testImplementation(libs.junit)
|
||||||
|
testImplementation(libs.mockwebserver)
|
||||||
|
androidTestImplementation(libs.androidx.junit)
|
||||||
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
}
|
0
api-core/consumer-rules.pro
Normal file
0
api-core/consumer-rules.pro
Normal file
21
api-core/proguard-rules.pro
vendored
Normal file
21
api-core/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
@ -0,0 +1,24 @@
|
|||||||
|
package com.example.api.core
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class ApiRepositoryTest {
|
||||||
|
@Test
|
||||||
|
fun useAppContext() {
|
||||||
|
// Context of the app under test.
|
||||||
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
assertEquals("com.example.api.core.test", appContext.packageName)
|
||||||
|
}
|
||||||
|
}
|
4
api-core/src/main/AndroidManifest.xml
Normal file
4
api-core/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
</manifest>
|
@ -0,0 +1,35 @@
|
|||||||
|
package com.example.api.core.data.core
|
||||||
|
|
||||||
|
import com.example.api.core.domain.ApiRepository
|
||||||
|
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.create
|
||||||
|
|
||||||
|
/**
|
||||||
|
* фабрика для создание инстанса интерфейса без внешнего доступа к реализации
|
||||||
|
* */
|
||||||
|
object ApiFactory {
|
||||||
|
|
||||||
|
fun provideRepository(dataStoreRepository: DataStoreRepository): ApiRepository {
|
||||||
|
val json = Json { ignoreUnknownKeys = true }
|
||||||
|
val client = OkHttpClient.Builder()
|
||||||
|
.addInterceptor(HttpLoggingInterceptor().apply {
|
||||||
|
level = HttpLoggingInterceptor.Level.BODY
|
||||||
|
})
|
||||||
|
.addInterceptor(AuthInterceptor(dataStoreRepository))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val retrofit = Retrofit.Builder()
|
||||||
|
.baseUrl("https://api.matule.ru/api/")
|
||||||
|
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
||||||
|
.client(client)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
return BaseApiRepository(retrofit.create<ApiService>(), dataStoreRepository, Handle.Base())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
package com.example.api.core.data.core
|
||||||
|
|
||||||
|
import com.example.api.core.data.dto.AuthUserResponseDto
|
||||||
|
import com.example.api.core.data.dto.CatalogDto
|
||||||
|
import com.example.api.core.data.dto.CatalogItemDto
|
||||||
|
import com.example.api.core.data.dto.ProjectsDto
|
||||||
|
import com.example.api.core.data.dto.RegisterUserDto
|
||||||
|
import com.example.api.core.data.dto.SalesAndNewsDto
|
||||||
|
import com.example.api.core.data.dto.SignInUserDto
|
||||||
|
import com.example.api.core.data.dto.UserResponseDto
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Path
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Автор: Манякин Дмитрий (user5)
|
||||||
|
* Дата создания: 27.05.2025
|
||||||
|
* */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* общий сервис для запросов
|
||||||
|
* */
|
||||||
|
internal interface ApiService {
|
||||||
|
|
||||||
|
@POST("collections/users/records")
|
||||||
|
suspend fun register(@Body registerUserDto: RegisterUserDto): UserResponseDto
|
||||||
|
|
||||||
|
@POST("collections/users/auth-with-password")
|
||||||
|
suspend fun auth(@Body signInUserDto: SignInUserDto): AuthUserResponseDto
|
||||||
|
|
||||||
|
@GET("collections/project/records")
|
||||||
|
suspend fun projects(): ProjectsDto
|
||||||
|
|
||||||
|
@GET("collections/news/records")
|
||||||
|
suspend fun salesAndNews(): SalesAndNewsDto
|
||||||
|
|
||||||
|
@GET("collections/products/records")
|
||||||
|
suspend fun catalog(): CatalogDto
|
||||||
|
|
||||||
|
@GET("collections/products/records")
|
||||||
|
suspend fun catalog(@Query("filter") filter: String): CatalogDto
|
||||||
|
|
||||||
|
@GET("collections/products/records/{id_product}")
|
||||||
|
suspend fun productDescription(@Path("id_product") productId: String): CatalogItemDto
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package com.example.api.core.data.core
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Автор: Манякин Дмитрий (user5)
|
||||||
|
* Дата создания: 27.05.2025
|
||||||
|
* */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Перехватчик для установки токена в заголовок запроса
|
||||||
|
* */
|
||||||
|
class AuthInterceptor(private val dataStoreRepository: DataStoreRepository) : Interceptor {
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val token = runBlocking { dataStoreRepository.token().first() }
|
||||||
|
val request = chain.request().newBuilder().apply {
|
||||||
|
token?.let { addHeader("Authorization", "Bearer $token") }
|
||||||
|
}
|
||||||
|
|
||||||
|
return chain.proceed(request.build())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
package com.example.api.core.data.core
|
||||||
|
|
||||||
|
import com.example.api.core.domain.ApiRepository
|
||||||
|
import com.example.api.core.domain.AuthUserResponse
|
||||||
|
import com.example.api.core.domain.FetchResult
|
||||||
|
import com.example.api.core.domain.RegisterUser
|
||||||
|
import com.example.api.core.domain.Projects
|
||||||
|
import retrofit2.HttpException
|
||||||
|
import java.net.UnknownHostException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Автор: Манякин Дмитрий (user5)
|
||||||
|
* Дата создания: 27.05.2025
|
||||||
|
* */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* общая структура обработки запросов
|
||||||
|
* */
|
||||||
|
internal interface Handle {
|
||||||
|
suspend fun <T> handle(action: suspend () -> T): FetchResult<T>
|
||||||
|
|
||||||
|
class Base : Handle {
|
||||||
|
override suspend fun <T> handle(action: suspend () -> T): FetchResult<T> {
|
||||||
|
return try {
|
||||||
|
val result = action.invoke()
|
||||||
|
|
||||||
|
FetchResult.Success(result)
|
||||||
|
} catch (e: UnknownHostException) {
|
||||||
|
FetchResult.Error(null, "no internet connection")
|
||||||
|
} catch (e: HttpException) {
|
||||||
|
FetchResult.Error(null, e.message())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
FetchResult.Error(null, e.message!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class BaseApiRepository(
|
||||||
|
private val service: ApiService,
|
||||||
|
private val dataStoreRepository: DataStoreRepository,
|
||||||
|
private val handle: Handle,
|
||||||
|
) : RemoteMapper(), ApiRepository {
|
||||||
|
|
||||||
|
override suspend fun auth(registerUser: RegisterUser): FetchResult<AuthUserResponse> = handle.handle {
|
||||||
|
service.register(registerUser.toRegisterUserDto())
|
||||||
|
val response = service.auth(registerUser.toSignInUserDto())
|
||||||
|
dataStoreRepository.saveToken(response.token)
|
||||||
|
response.toUserResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override suspend fun projects(): FetchResult<Projects> = handle.handle {
|
||||||
|
service.projects().toDomain()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package com.example.api.core.data.core
|
||||||
|
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Автор: Манякин Дмитрий (user5)
|
||||||
|
* Дата создания: 27.05.2025
|
||||||
|
* */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Локальное хранение токена
|
||||||
|
* */
|
||||||
|
interface DataStoreRepository {
|
||||||
|
|
||||||
|
suspend fun saveToken(token: String)
|
||||||
|
|
||||||
|
fun token(): Flow<String?>
|
||||||
|
|
||||||
|
class Base(private val preferences: DataStore<Preferences>) : DataStoreRepository {
|
||||||
|
override suspend fun saveToken(token: String) {
|
||||||
|
preferences.edit { settings ->
|
||||||
|
settings[TOKEN_KEY] = token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun token(): Flow<String?> {
|
||||||
|
return preferences.data.map { preferences ->
|
||||||
|
preferences[TOKEN_KEY]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
val TOKEN_KEY = stringPreferencesKey("token_key")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
package com.example.api.core.data.core
|
||||||
|
|
||||||
|
import com.example.api.core.data.dto.AuthUserResponseDto
|
||||||
|
import com.example.api.core.data.dto.RegisterUserDto
|
||||||
|
import com.example.api.core.data.dto.SaleItemDto
|
||||||
|
import com.example.api.core.data.dto.ProjectsDto
|
||||||
|
import com.example.api.core.data.dto.SignInUserDto
|
||||||
|
import com.example.api.core.data.dto.SignInUserResponseDto
|
||||||
|
import com.example.api.core.domain.AuthUserResponse
|
||||||
|
import com.example.api.core.domain.RegisterUser
|
||||||
|
import com.example.api.core.domain.SaleItem
|
||||||
|
import com.example.api.core.domain.Projects
|
||||||
|
import com.example.api.core.domain.SignInUserResponse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Автор: Манякин Дмитрий (user5)
|
||||||
|
* Дата создания: 27.05.2025
|
||||||
|
* */
|
||||||
|
|
||||||
|
/**
|
||||||
|
Маппер для преобразования объектов сервера в доменные
|
||||||
|
* */
|
||||||
|
internal abstract class RemoteMapper {
|
||||||
|
protected fun RegisterUser.toRegisterUserDto(): RegisterUserDto {
|
||||||
|
return RegisterUserDto(
|
||||||
|
email = email,
|
||||||
|
password = password,
|
||||||
|
passwordConfirm = passwordConfirm
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun RegisterUser.toSignInUserDto(): SignInUserDto {
|
||||||
|
return SignInUserDto(
|
||||||
|
identity = email,
|
||||||
|
password = password
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun AuthUserResponseDto.toUserResponse(): AuthUserResponse {
|
||||||
|
return AuthUserResponse(
|
||||||
|
record = record.toUserResponse(),
|
||||||
|
token = token
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun SignInUserResponseDto.toUserResponse(): SignInUserResponse {
|
||||||
|
return SignInUserResponse(
|
||||||
|
collectionId = collectionId,
|
||||||
|
collectionName = collectionName,
|
||||||
|
created = created,
|
||||||
|
dateBirthday = dateBirthday,
|
||||||
|
emailVisibility = emailVisibility,
|
||||||
|
email = email,
|
||||||
|
firstname = firstname,
|
||||||
|
gender = gender,
|
||||||
|
id = id,
|
||||||
|
lastname = lastname,
|
||||||
|
secondname = secondname,
|
||||||
|
updated = updated,
|
||||||
|
verified = verified
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun ProjectsDto.toDomain(): Projects {
|
||||||
|
return Projects(
|
||||||
|
page = page,
|
||||||
|
perPage = perPage,
|
||||||
|
totalPage = totalPages,
|
||||||
|
totalItems = totalItems,
|
||||||
|
items = items.map { it.toSaleItem() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun SaleItemDto.toSaleItem(): SaleItem {
|
||||||
|
return SaleItem(
|
||||||
|
id = id,
|
||||||
|
collectionId = collectionId,
|
||||||
|
collectionName = collectionName,
|
||||||
|
created = created,
|
||||||
|
updated = updated,
|
||||||
|
title = title,
|
||||||
|
dateStart = dateStart,
|
||||||
|
dateEnd = dateEnd,
|
||||||
|
gender = gender,
|
||||||
|
descriptionSource = descriptionSource,
|
||||||
|
category = category,
|
||||||
|
image = image,
|
||||||
|
userId = userId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package com.example.api.core.data.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
internal data class AuthUserResponseDto(
|
||||||
|
val record: SignInUserResponseDto,
|
||||||
|
val token: String
|
||||||
|
)
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.example.api.core.data.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
internal data class CatalogDto(
|
||||||
|
val items: List<CatalogItemDto>,
|
||||||
|
val page: Int,
|
||||||
|
val perPage: Int,
|
||||||
|
val totalItems: Int,
|
||||||
|
val totalPages: Int
|
||||||
|
)
|
@ -0,0 +1,19 @@
|
|||||||
|
package com.example.api.core.data.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
internal data class CatalogItemDto(
|
||||||
|
@SerialName("approximate_cost") val approximateCost: String,
|
||||||
|
val collectionId: String,
|
||||||
|
val collectionName: String,
|
||||||
|
val created: String,
|
||||||
|
val description: String,
|
||||||
|
val id: String,
|
||||||
|
val price: Int,
|
||||||
|
val title: String,
|
||||||
|
val type: String,
|
||||||
|
val typeCloses: String,
|
||||||
|
val updated: String
|
||||||
|
)
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.example.api.core.data.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
internal data class ProjectsDto(
|
||||||
|
val page: Int,
|
||||||
|
val perPage: Int,
|
||||||
|
val totalPages: Int,
|
||||||
|
val totalItems: Int,
|
||||||
|
val items: List<SaleItemDto>
|
||||||
|
)
|
@ -0,0 +1,10 @@
|
|||||||
|
package com.example.api.core.data.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
internal data class RegisterUserDto(
|
||||||
|
val email: String,
|
||||||
|
val password: String,
|
||||||
|
val passwordConfirm: String
|
||||||
|
)
|
@ -0,0 +1,21 @@
|
|||||||
|
package com.example.api.core.data.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
internal data class SaleItemDto(
|
||||||
|
val id: String,
|
||||||
|
val collectionId: String,
|
||||||
|
val collectionName: String,
|
||||||
|
val created: String,
|
||||||
|
val updated: String,
|
||||||
|
val title: String,
|
||||||
|
val dateStart: String,
|
||||||
|
val dateEnd: String,
|
||||||
|
val gender: String,
|
||||||
|
@SerialName("description_source") val descriptionSource: String,
|
||||||
|
val category: String,
|
||||||
|
val image: String,
|
||||||
|
@SerialName("user_id") val userId: String
|
||||||
|
)
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.example.api.core.data.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
internal data class SalesAndNewsDto(
|
||||||
|
val items: List<SalesAndNewsItemDto>,
|
||||||
|
val page: Int,
|
||||||
|
val perPage: Int,
|
||||||
|
val totalItems: Int,
|
||||||
|
val totalPages: Int
|
||||||
|
)
|
@ -0,0 +1,13 @@
|
|||||||
|
package com.example.api.core.data.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
internal data class SalesAndNewsItemDto(
|
||||||
|
val collectionId: String,
|
||||||
|
val collectionName: String,
|
||||||
|
val created: String,
|
||||||
|
val id: String,
|
||||||
|
val newsImage: String,
|
||||||
|
val updated: String
|
||||||
|
)
|
@ -0,0 +1,9 @@
|
|||||||
|
package com.example.api.core.data.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
internal data class SignInUserDto(
|
||||||
|
val identity: String,
|
||||||
|
val password: String
|
||||||
|
)
|
@ -0,0 +1,20 @@
|
|||||||
|
package com.example.api.core.data.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SignInUserResponseDto(
|
||||||
|
val collectionId: String,
|
||||||
|
val collectionName: String,
|
||||||
|
val created: String,
|
||||||
|
val dateBirthday: String,
|
||||||
|
val email: String,
|
||||||
|
val emailVisibility: Boolean,
|
||||||
|
val firstname: String,
|
||||||
|
val gender: String,
|
||||||
|
val id: String,
|
||||||
|
val lastname: String,
|
||||||
|
val secondname: String,
|
||||||
|
val updated: String,
|
||||||
|
val verified: Boolean
|
||||||
|
)
|
@ -0,0 +1,19 @@
|
|||||||
|
package com.example.api.core.data.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
internal data class UserResponseDto(
|
||||||
|
val collectionId: String,
|
||||||
|
val collectionName: String,
|
||||||
|
val created: String,
|
||||||
|
val dateBirthday: String,
|
||||||
|
val emailVisibility: Boolean,
|
||||||
|
val firstname: String,
|
||||||
|
val gender: String,
|
||||||
|
val id: String,
|
||||||
|
val lastname: String,
|
||||||
|
val secondname: String,
|
||||||
|
val updated: String,
|
||||||
|
val verified: Boolean
|
||||||
|
)
|
@ -0,0 +1,13 @@
|
|||||||
|
package com.example.api.core.domain
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Автор: Манякин Дмитрий (user5)
|
||||||
|
* Дата создания: 27.05.2025
|
||||||
|
* */
|
||||||
|
|
||||||
|
interface ApiRepository {
|
||||||
|
|
||||||
|
suspend fun auth(registerUser: RegisterUser): FetchResult<AuthUserResponse>
|
||||||
|
|
||||||
|
suspend fun projects(): FetchResult<Projects>
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package com.example.api.core.domain
|
||||||
|
|
||||||
|
data class AuthUserResponse(
|
||||||
|
val record: SignInUserResponse,
|
||||||
|
val token: String
|
||||||
|
)
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.example.api.core.domain
|
||||||
|
|
||||||
|
data class CatalogResponse(
|
||||||
|
val items: List<SaleItem>
|
||||||
|
)
|
@ -0,0 +1,31 @@
|
|||||||
|
package com.example.api.core.domain
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Автор: Манякин Дмитрий (user5)
|
||||||
|
* Дата создания: 27.05.2025
|
||||||
|
* */
|
||||||
|
|
||||||
|
interface FetchResult<D> {
|
||||||
|
|
||||||
|
fun <Ui> map(mapper: Mapper<D, Ui>): Ui
|
||||||
|
|
||||||
|
interface Mapper<D, Ui> {
|
||||||
|
|
||||||
|
fun mapSuccess(data: D): Ui
|
||||||
|
|
||||||
|
fun mapError(data: D?, message: String): Ui
|
||||||
|
}
|
||||||
|
|
||||||
|
class Success<D>(private val data: D) : FetchResult<D> {
|
||||||
|
override fun <Ui> map(mapper: Mapper<D, Ui>): Ui {
|
||||||
|
return mapper.mapSuccess(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Error<D>(private val data: D?, private val message: String) : FetchResult<D> {
|
||||||
|
override fun <Ui> map(mapper: Mapper<D, Ui>): Ui {
|
||||||
|
return mapper.mapError(data, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package com.example.api.core.domain
|
||||||
|
|
||||||
|
data class Projects(
|
||||||
|
val page: Int,
|
||||||
|
val perPage: Int,
|
||||||
|
val totalPage: Int,
|
||||||
|
val totalItems: Int,
|
||||||
|
val items: List<SaleItem>
|
||||||
|
)
|
@ -0,0 +1,7 @@
|
|||||||
|
package com.example.api.core.domain
|
||||||
|
|
||||||
|
data class RegisterUser(
|
||||||
|
val email: String,
|
||||||
|
val password: String,
|
||||||
|
val passwordConfirm: String
|
||||||
|
)
|
@ -0,0 +1,17 @@
|
|||||||
|
package com.example.api.core.domain
|
||||||
|
|
||||||
|
data class SaleItem(
|
||||||
|
val id: String,
|
||||||
|
val collectionId: String,
|
||||||
|
val collectionName: String,
|
||||||
|
val created: String,
|
||||||
|
val updated: String,
|
||||||
|
val title: String,
|
||||||
|
val dateStart: String,
|
||||||
|
val dateEnd: String,
|
||||||
|
val gender: String,
|
||||||
|
val descriptionSource: String,
|
||||||
|
val category: String,
|
||||||
|
val image: String,
|
||||||
|
val userId: String
|
||||||
|
)
|
@ -0,0 +1,6 @@
|
|||||||
|
package com.example.api.core.domain
|
||||||
|
|
||||||
|
data class SignInUser(
|
||||||
|
val identity: String,
|
||||||
|
val password: String
|
||||||
|
)
|
@ -0,0 +1,17 @@
|
|||||||
|
package com.example.api.core.domain
|
||||||
|
|
||||||
|
data class SignInUserResponse(
|
||||||
|
val collectionId: String,
|
||||||
|
val collectionName: String,
|
||||||
|
val created: String,
|
||||||
|
val dateBirthday: String,
|
||||||
|
val emailVisibility: Boolean,
|
||||||
|
val email: String,
|
||||||
|
val firstname: String,
|
||||||
|
val gender: String,
|
||||||
|
val id: String,
|
||||||
|
val lastname: String,
|
||||||
|
val secondname: String,
|
||||||
|
val updated: String,
|
||||||
|
val verified: Boolean
|
||||||
|
)
|
@ -0,0 +1,16 @@
|
|||||||
|
package com.example.api.core.domain
|
||||||
|
|
||||||
|
data class UserResponse(
|
||||||
|
val collectionId: String,
|
||||||
|
val collectionName: String,
|
||||||
|
val created: String,
|
||||||
|
val dateBirthday: String,
|
||||||
|
val emailVisibility: Boolean,
|
||||||
|
val firstname: String,
|
||||||
|
val gender: String,
|
||||||
|
val id: String,
|
||||||
|
val lastname: String,
|
||||||
|
val secondname: String,
|
||||||
|
val updated: String,
|
||||||
|
val verified: Boolean
|
||||||
|
)
|
336
api-core/src/test/java/com/example/api/core/ServiceTest.kt
Normal file
336
api-core/src/test/java/com/example/api/core/ServiceTest.kt
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
package com.example.api.core
|
||||||
|
|
||||||
|
import com.example.api.core.data.core.ApiService
|
||||||
|
import com.example.api.core.data.dto.RegisterUserDto
|
||||||
|
import com.example.api.core.data.dto.SignInUserDto
|
||||||
|
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||||
|
import junit.framework.TestCase.assertEquals
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import okhttp3.mockwebserver.MockResponse
|
||||||
|
import okhttp3.mockwebserver.MockWebServer
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.create
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Автор: Манякин Дмитрий (user5)
|
||||||
|
* Дата создания: 27.05.2025
|
||||||
|
* */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Общая структура тестов
|
||||||
|
* фейк ответ
|
||||||
|
* сверка с dto некоторые параметры
|
||||||
|
* проверка типа запроса
|
||||||
|
* проверка пути запроса
|
||||||
|
* опционально параметры и указание пути*/
|
||||||
|
class ServiceTest {
|
||||||
|
|
||||||
|
private lateinit var mockWebServer: MockWebServer
|
||||||
|
private lateinit var service: ApiService
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
mockWebServer = MockWebServer()
|
||||||
|
mockWebServer.start()
|
||||||
|
|
||||||
|
val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
|
service = Retrofit.Builder()
|
||||||
|
.baseUrl(mockWebServer.url("/"))
|
||||||
|
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
||||||
|
.client(OkHttpClient.Builder().addInterceptor(HttpLoggingInterceptor().apply {
|
||||||
|
level = HttpLoggingInterceptor.Level.BODY
|
||||||
|
}).build())
|
||||||
|
.build().create<ApiService>()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_registration() = runBlocking {
|
||||||
|
val response = """
|
||||||
|
{
|
||||||
|
"collectionId": "_pb_users_auth_",
|
||||||
|
"collectionName": "users",
|
||||||
|
"created": "2025-05-27 06:08:13.833Z",
|
||||||
|
"dateBirthday": "",
|
||||||
|
"emailVisibility": false,
|
||||||
|
"firstname": "",
|
||||||
|
"gender": "",
|
||||||
|
"id": "w20q0pj1e2824oz",
|
||||||
|
"lastname": "",
|
||||||
|
"secondname": "",
|
||||||
|
"updated": "2025-05-27 06:08:13.833Z",
|
||||||
|
"verified": false
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(response))
|
||||||
|
|
||||||
|
val responseResult = service.register(
|
||||||
|
RegisterUserDto(
|
||||||
|
email = "email@test.com",
|
||||||
|
password = "12345678",
|
||||||
|
passwordConfirm = "12345678"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val request = mockWebServer.takeRequest()
|
||||||
|
|
||||||
|
assertEquals("w20q0pj1e2824oz", responseResult.id)
|
||||||
|
assertEquals("2025-05-27 06:08:13.833Z", responseResult.created)
|
||||||
|
|
||||||
|
assertEquals("POST", request.method)
|
||||||
|
assertEquals("/collections/users/records", request.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_sign_in() = runBlocking {
|
||||||
|
val response = """
|
||||||
|
{
|
||||||
|
"record": {
|
||||||
|
"collectionId": "_pb_users_auth_",
|
||||||
|
"collectionName": "users",
|
||||||
|
"created": "2025-05-27 06:08:13.833Z",
|
||||||
|
"dateBirthday": "",
|
||||||
|
"email": "example123@test.ru",
|
||||||
|
"emailVisibility": false,
|
||||||
|
"firstname": "",
|
||||||
|
"gender": "",
|
||||||
|
"id": "w20q0pj1e2824oz",
|
||||||
|
"lastname": "",
|
||||||
|
"secondname": "",
|
||||||
|
"updated": "2025-05-27 06:08:13.833Z",
|
||||||
|
"verified": false
|
||||||
|
},
|
||||||
|
"token": "eyJhbGciOiJIUzIqwwe1NiIsInR5cCI6IkpXVCJg9.eyJjb2xsZWN0aW9uSWheQiOiJfqcGggJfdXNlcnNfYXV0aFasd8iLCJleHAiOjE3NDg5MzA5MjAsImlkIjoidzIwcTBwajFlMjgyNG96IiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.7Sc_NB_CgnwEfXWp0NjsnDT35gGVXzFJwwLUVD730Gw"
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(response))
|
||||||
|
|
||||||
|
val responseResult = service.auth(
|
||||||
|
SignInUserDto(
|
||||||
|
identity = "example123@test.ru",
|
||||||
|
password = "12345678"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val request = mockWebServer.takeRequest()
|
||||||
|
|
||||||
|
assertEquals("example123@test.ru", responseResult.record.email)
|
||||||
|
assertEquals(
|
||||||
|
"eyJhbGciOiJIUzIqwwe1NiIsInR5cCI6IkpXVCJg9.eyJjb2xsZWN0aW9uSWheQiOiJfqcGggJfdXNlcnNfYXV0aFasd8iLCJleHAiOjE3NDg5MzA5MjAsImlkIjoidzIwcTBwajFlMjgyNG96IiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.7Sc_NB_CgnwEfXWp0NjsnDT35gGVXzFJwwLUVD730Gw",
|
||||||
|
responseResult.token
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals("POST", request.method)
|
||||||
|
assertEquals("/collections/users/auth-with-password", request.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_project_list() = runBlocking {
|
||||||
|
val response = """
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"category": "е",
|
||||||
|
"collectionId": "pbc_3202395908",
|
||||||
|
"collectionName": "project",
|
||||||
|
"created": "2025-05-26 14:47:25.523Z",
|
||||||
|
"dateEnd": "2025-05-24 12:00:00.000Z",
|
||||||
|
"dateStart": "2025-05-15 12:00:00.000Z",
|
||||||
|
"description_source": "е",
|
||||||
|
"gender": "е",
|
||||||
|
"id": "7m35pcmbjr86kh4",
|
||||||
|
"image": "",
|
||||||
|
"title": "е",
|
||||||
|
"typeProject": "е",
|
||||||
|
"updated": "2025-05-26 14:47:25.523Z",
|
||||||
|
"user_id": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"page": 1,
|
||||||
|
"perPage": 30,
|
||||||
|
"totalItems": 1,
|
||||||
|
"totalPages": 1
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(response))
|
||||||
|
|
||||||
|
val responseResult = service.projects()
|
||||||
|
|
||||||
|
val request = mockWebServer.takeRequest()
|
||||||
|
|
||||||
|
assertEquals(1, responseResult.page)
|
||||||
|
assertEquals(1, responseResult.totalItems)
|
||||||
|
assertEquals(1, responseResult.items.size)
|
||||||
|
|
||||||
|
assertEquals("GET", request.method)
|
||||||
|
assertEquals("/collections/project/records", request.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_sales_and_news() = runBlocking {
|
||||||
|
val response = """
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"collectionId": "pbc_2599178718",
|
||||||
|
"collectionName": "News",
|
||||||
|
"created": "2025-05-26 14:21:46.284Z",
|
||||||
|
"id": "yxe08bp4woz996w",
|
||||||
|
"newsImage": "banner_u9uqsy4sf9.png",
|
||||||
|
"updated": "2025-05-26 15:29:41.083Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collectionId": "pbc_2599178718",
|
||||||
|
"collectionName": "News",
|
||||||
|
"created": "2025-05-26 15:29:50.831Z",
|
||||||
|
"id": "lqg3hpgn46efgmz",
|
||||||
|
"newsImage": "banner_2_10pjar2fq7.png",
|
||||||
|
"updated": "2025-05-26 15:29:50.831Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"page": 1,
|
||||||
|
"perPage": 30,
|
||||||
|
"totalItems": 2,
|
||||||
|
"totalPages": 1
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(response))
|
||||||
|
|
||||||
|
val responseResult = service.salesAndNews()
|
||||||
|
|
||||||
|
val request = mockWebServer.takeRequest()
|
||||||
|
|
||||||
|
assertEquals(2, responseResult.items.size)
|
||||||
|
assertEquals("News", responseResult.items.first().collectionName)
|
||||||
|
assertEquals("lqg3hpgn46efgmz", responseResult.items[1].id)
|
||||||
|
|
||||||
|
assertEquals("GET", request.method)
|
||||||
|
assertEquals("/collections/news/records", request.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_catalog_all() = runBlocking {
|
||||||
|
val response = """
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"approximate_cost": "0",
|
||||||
|
"collectionId": "pbc_4092854851",
|
||||||
|
"collectionName": "products",
|
||||||
|
"created": "2025-05-26 15:44:16.919Z",
|
||||||
|
"description": "Мой выбор для этих шапок – кардные составы, которые раскрываются деликатным пушком. Кашемиры, мериносы, смесовки с ними отлично подойдут на шапку.\r\nКардные составы берите в большое количество сложений, вязать будем резинку 1х1, плотненько.\r\nПряжу 1400-1500м в 100г в 4 сложения, пряжу 700м в 2 сложения. Ориентир для конечной ",
|
||||||
|
"id": "45urlx4rj907rrk",
|
||||||
|
"price": 350,
|
||||||
|
"title": "Рубашка Среда для машинного \r\nвязания",
|
||||||
|
"type": "Женская одежда",
|
||||||
|
"typeCloses": "Женщинам",
|
||||||
|
"updated": "2025-05-26 15:44:16.919Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"page": 1,
|
||||||
|
"perPage": 30,
|
||||||
|
"totalItems": 1,
|
||||||
|
"totalPages": 1
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(response))
|
||||||
|
|
||||||
|
val responseResult = service.catalog()
|
||||||
|
val request = mockWebServer.takeRequest()
|
||||||
|
|
||||||
|
assertEquals(1, responseResult.items.size)
|
||||||
|
assertEquals("Женщинам", responseResult.items.first().typeCloses)
|
||||||
|
assertEquals(30, responseResult.perPage)
|
||||||
|
|
||||||
|
assertEquals("GET", request.method)
|
||||||
|
assertEquals("/collections/products/records", request.path)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_catalog_with_filter() = runBlocking {
|
||||||
|
val response = """
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"approximate_cost": "0",
|
||||||
|
"collectionId": "pbc_4092854851",
|
||||||
|
"collectionName": "products",
|
||||||
|
"created": "2025-05-26 15:44:16.919Z",
|
||||||
|
"description": "Мой выбор для этих шапок – кардные составы, которые раскрываются деликатным пушком. Кашемиры, мериносы, смесовки с ними отлично подойдут на шапку.\r\nКардные составы берите в большое количество сложений, вязать будем резинку 1х1, плотненько.\r\nПряжу 1400-1500м в 100г в 4 сложения, пряжу 700м в 2 сложения. Ориентир для конечной ",
|
||||||
|
"id": "45urlx4rj907rrk",
|
||||||
|
"price": 350,
|
||||||
|
"title": "Рубашка Среда для машинного \r\nвязания",
|
||||||
|
"type": "Женская одежда",
|
||||||
|
"typeCloses": "Женщинам",
|
||||||
|
"updated": "2025-05-26 15:44:16.919Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"page": 1,
|
||||||
|
"perPage": 30,
|
||||||
|
"totalItems": 1,
|
||||||
|
"totalPages": 1
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(response))
|
||||||
|
|
||||||
|
val responseResult = service.catalog("(typeCloses ?~ 'Женщинам')")
|
||||||
|
val request = mockWebServer.takeRequest()
|
||||||
|
|
||||||
|
assertEquals(1, responseResult.items.size)
|
||||||
|
assertEquals("Женщинам", responseResult.items.first().typeCloses)
|
||||||
|
assertEquals(30, responseResult.perPage)
|
||||||
|
|
||||||
|
assertEquals("GET", request.method)
|
||||||
|
assertEquals("(typeCloses ?~ 'Женщинам')", request.requestUrl!!.queryParameter("filter"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_product_description() = runBlocking {
|
||||||
|
val response = """
|
||||||
|
{
|
||||||
|
"approximate_cost": "0",
|
||||||
|
"collectionId": "pbc_4092854851",
|
||||||
|
"collectionName": "products",
|
||||||
|
"created": "2025-05-26 15:44:16.919Z",
|
||||||
|
"description": "Мой выбор для этих шапок – кардные составы, которые раскрываются деликатным пушком. Кашемиры, мериносы, смесовки с ними отлично подойдут на шапку.\r\nКардные составы берите в большое количество сложений, вязать будем резинку 1х1, плотненько.\r\nПряжу 1400-1500м в 100г в 4 сложения, пряжу 700м в 2 сложения. Ориентир для конечной ",
|
||||||
|
"id": "45urlx4rj907rrk",
|
||||||
|
"price": 350,
|
||||||
|
"title": "Рубашка Среда для машинного \r\nвязания",
|
||||||
|
"type": "Женская одежда",
|
||||||
|
"typeCloses": "Женщинам",
|
||||||
|
"updated": "2025-05-26 15:44:16.919Z"
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(response))
|
||||||
|
|
||||||
|
val productId = "45urlx4rj907rrk"
|
||||||
|
val responseResult = service.productDescription(productId)
|
||||||
|
val request = mockWebServer.takeRequest()
|
||||||
|
|
||||||
|
assertEquals("0", responseResult.approximateCost)
|
||||||
|
assertEquals("Женская одежда", responseResult.type)
|
||||||
|
|
||||||
|
assertEquals("GET", request.method)
|
||||||
|
assertEquals("/collections/products/records/$productId", request.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun teardown() {
|
||||||
|
mockWebServer.shutdown()
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,8 @@ plugins {
|
|||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
alias(libs.plugins.kotlin.compose)
|
alias(libs.plugins.kotlin.compose)
|
||||||
|
alias(libs.plugins.dagger.hilt.android)
|
||||||
|
kotlin("kapt")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@ -40,6 +42,10 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(libs.androidx.datastore.preferences)
|
||||||
|
kapt(libs.hilt.android.compiler)
|
||||||
|
implementation(libs.hilt.android)
|
||||||
|
implementation(libs.hilt.navigation.compose)
|
||||||
|
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
@ -56,4 +62,6 @@ dependencies {
|
|||||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||||
debugImplementation(libs.androidx.ui.tooling)
|
debugImplementation(libs.androidx.ui.tooling)
|
||||||
debugImplementation(libs.androidx.ui.test.manifest)
|
debugImplementation(libs.androidx.ui.test.manifest)
|
||||||
|
|
||||||
|
implementation(project(":api-core"))
|
||||||
}
|
}
|
@ -2,7 +2,9 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<application
|
<application
|
||||||
|
android:name=".App"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
|
12
app/src/main/java/com/example/api/App.kt
Normal file
12
app/src/main/java/com/example/api/App.kt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package com.example.api
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Автор: Манякин Дмитрий (user5)
|
||||||
|
* Дата создания: 27.05.2025
|
||||||
|
* */
|
||||||
|
|
||||||
|
@HiltAndroidApp
|
||||||
|
class App : Application()
|
25
app/src/main/java/com/example/api/BaseViewModel.kt
Normal file
25
app/src/main/java/com/example/api/BaseViewModel.kt
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package com.example.api
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Автор: Манякин Дмитрий (user5)
|
||||||
|
* Дата создания: 27.05.2025
|
||||||
|
* */
|
||||||
|
|
||||||
|
abstract class BaseViewModel : ViewModel() {
|
||||||
|
|
||||||
|
protected fun<T> handle(action: suspend () -> T, ui: (T) -> Unit) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val result = action.invoke()
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
ui.invoke(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
app/src/main/java/com/example/api/CoreModule.kt
Normal file
51
app/src/main/java/com/example/api/CoreModule.kt
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package com.example.api
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import com.example.api.core.data.core.ApiFactory
|
||||||
|
import com.example.api.core.data.core.DataStoreRepository
|
||||||
|
import com.example.api.core.domain.ApiRepository
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Автор: Манякин Дмитрий (user5)
|
||||||
|
* Дата создания: 27.05.2025
|
||||||
|
* */
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
class CoreModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideContent(@ApplicationContext context: Context): Context = context
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideDataStore(context: Context): DataStore<Preferences> {
|
||||||
|
return context.dataStore
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideDataStoreRepository(preferences: DataStore<Preferences>): DataStoreRepository {
|
||||||
|
return DataStoreRepository.Base(preferences)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideRepository(dataStoreRepository: DataStoreRepository): ApiRepository {
|
||||||
|
return ApiFactory.provideRepository(dataStoreRepository)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val Context.dataStore by preferencesDataStore("settings")
|
||||||
|
}
|
||||||
|
}
|
18
app/src/main/java/com/example/api/FetchResultMapper.kt
Normal file
18
app/src/main/java/com/example/api/FetchResultMapper.kt
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package com.example.api
|
||||||
|
|
||||||
|
import com.example.api.core.domain.FetchResult
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Автор: Манякин Дмитрий (user5)
|
||||||
|
* Дата создания: 27.05.2025
|
||||||
|
* */
|
||||||
|
|
||||||
|
class FetchResultMapper<T> : FetchResult.Mapper<T, FetchResultUiState<T>> {
|
||||||
|
override fun mapSuccess(data: T): FetchResultUiState<T> {
|
||||||
|
return FetchResultUiState.Success(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mapError(data: T?, message: String): FetchResultUiState<T> {
|
||||||
|
return FetchResultUiState.Error(data, message)
|
||||||
|
}
|
||||||
|
}
|
62
app/src/main/java/com/example/api/FetchResultUiState.kt
Normal file
62
app/src/main/java/com/example/api/FetchResultUiState.kt
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package com.example.api
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Автор: Манякин Дмитрий (user5)
|
||||||
|
* Дата создания: 27.05.2025
|
||||||
|
* */
|
||||||
|
|
||||||
|
interface FetchResultUiState<T> {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Show(
|
||||||
|
onSuccess: @Composable (T) -> Unit,
|
||||||
|
onError: @Composable (T?, String) -> Unit,
|
||||||
|
onLoading: @Composable (T?) -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
|
class Success<T>(val data: T) : FetchResultUiState<T> {
|
||||||
|
@Composable
|
||||||
|
override fun Show(
|
||||||
|
onSuccess: @Composable (T) -> Unit,
|
||||||
|
onError: @Composable (T?, String) -> Unit,
|
||||||
|
onLoading: @Composable (T?) -> Unit
|
||||||
|
) {
|
||||||
|
onSuccess(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Error<T>(private val data: T?, private val message: String) : FetchResultUiState<T> {
|
||||||
|
@Composable
|
||||||
|
override fun Show(
|
||||||
|
onSuccess: @Composable (T) -> Unit,
|
||||||
|
onError: @Composable (T?, String) -> Unit,
|
||||||
|
onLoading: @Composable (T?) -> Unit
|
||||||
|
) {
|
||||||
|
onError(data, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Loading<T>(private val data: T? = null) : FetchResultUiState<T> {
|
||||||
|
@Composable
|
||||||
|
override fun Show(
|
||||||
|
onSuccess: @Composable (T) -> Unit,
|
||||||
|
onError: @Composable (T?, String) -> Unit,
|
||||||
|
onLoading: @Composable (T?) -> Unit
|
||||||
|
) {
|
||||||
|
onLoading(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Initial<T> : FetchResultUiState<T> {
|
||||||
|
@Composable
|
||||||
|
override fun Show(
|
||||||
|
onSuccess: @Composable (T) -> Unit,
|
||||||
|
onError: @Composable (T?, String) -> Unit,
|
||||||
|
onLoading: @Composable (T?) -> Unit
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,44 +4,18 @@ import android.os.Bundle
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import com.example.api.ui.theme.ApiTheme
|
import com.example.api.ui.theme.ApiTheme
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
ApiTheme {
|
ApiTheme {
|
||||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
TestScreen()
|
||||||
Greeting(
|
|
||||||
name = "Android",
|
|
||||||
modifier = Modifier.padding(innerPadding)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
|
||||||
Text(
|
|
||||||
text = "Hello $name!",
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
fun GreetingPreview() {
|
|
||||||
ApiTheme {
|
|
||||||
Greeting("Android")
|
|
||||||
}
|
|
||||||
}
|
|
56
app/src/main/java/com/example/api/MainViewModel.kt
Normal file
56
app/src/main/java/com/example/api/MainViewModel.kt
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package com.example.api
|
||||||
|
|
||||||
|
import com.example.api.core.domain.ApiRepository
|
||||||
|
import com.example.api.core.domain.AuthUserResponse
|
||||||
|
import com.example.api.core.domain.Projects
|
||||||
|
import com.example.api.core.domain.RegisterUser
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Автор: Манякин Дмитрий (user5)
|
||||||
|
* Дата создания: 27.05.2025
|
||||||
|
* */
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class MainViewModel @Inject constructor(
|
||||||
|
private val apiRepository: ApiRepository
|
||||||
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
private val _authUiState =
|
||||||
|
MutableStateFlow<FetchResultUiState<AuthUserResponse>>(FetchResultUiState.Initial())
|
||||||
|
val authUiState: StateFlow<FetchResultUiState<AuthUserResponse>>
|
||||||
|
get() = _authUiState.asStateFlow()
|
||||||
|
|
||||||
|
private val _projectsUiState =
|
||||||
|
MutableStateFlow<FetchResultUiState<Projects>>(FetchResultUiState.Initial())
|
||||||
|
val projectsUiState: StateFlow<FetchResultUiState<Projects>>
|
||||||
|
get() = _projectsUiState.asStateFlow()
|
||||||
|
|
||||||
|
fun auth(email: String, password: String) {
|
||||||
|
handle(
|
||||||
|
action = {
|
||||||
|
apiRepository.auth(
|
||||||
|
RegisterUser(
|
||||||
|
email = email,
|
||||||
|
password = password,
|
||||||
|
passwordConfirm = password
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
_authUiState.value = it.map(FetchResultMapper())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun salesAndProjects() {
|
||||||
|
handle(
|
||||||
|
action = { apiRepository.projects() }
|
||||||
|
) {
|
||||||
|
_projectsUiState.value = it.map(FetchResultMapper())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
app/src/main/java/com/example/api/SalesAndNews.kt
Normal file
9
app/src/main/java/com/example/api/SalesAndNews.kt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package com.example.api
|
||||||
|
|
||||||
|
data class SalesAndNews(
|
||||||
|
val items: List<SalesAndNewsItem>,
|
||||||
|
val page: Int,
|
||||||
|
val perPage: Int,
|
||||||
|
val totalItems: Int,
|
||||||
|
val totalPages: Int
|
||||||
|
)
|
10
app/src/main/java/com/example/api/SalesAndNewsItem.kt
Normal file
10
app/src/main/java/com/example/api/SalesAndNewsItem.kt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package com.example.api
|
||||||
|
|
||||||
|
data class SalesAndNewsItem(
|
||||||
|
val collectionId: String,
|
||||||
|
val collectionName: String,
|
||||||
|
val created: String,
|
||||||
|
val id: String,
|
||||||
|
val newsImage: String,
|
||||||
|
val updated: String
|
||||||
|
)
|
52
app/src/main/java/com/example/api/TestScreen.kt
Normal file
52
app/src/main/java/com/example/api/TestScreen.kt
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package com.example.api
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Автор: Манякин Дмитрий (user5)
|
||||||
|
* Дата создания: 27.05.2025
|
||||||
|
* */
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TestScreen(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: MainViewModel = hiltViewModel()
|
||||||
|
) {
|
||||||
|
val state by viewModel.authUiState.collectAsState()
|
||||||
|
val state2 by viewModel.projectsUiState.collectAsState()
|
||||||
|
|
||||||
|
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Column {
|
||||||
|
Button(onClick = { viewModel.auth("someemael@te3s11t1.com", "12345678") }) {
|
||||||
|
Text("register")
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Show(
|
||||||
|
onSuccess = { Text(text = it.token) },
|
||||||
|
onError = { _, message -> Text(text = message) },
|
||||||
|
onLoading = { CircularProgressIndicator() }
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(onClick = { viewModel.salesAndProjects() }) {
|
||||||
|
Text("sales and projects")
|
||||||
|
}
|
||||||
|
|
||||||
|
state2.Show(
|
||||||
|
onSuccess = { Text(text = it.items.size.toString()) },
|
||||||
|
onError = { _, message -> Text(text = message) },
|
||||||
|
onLoading = { CircularProgressIndicator() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,4 +3,6 @@ plugins {
|
|||||||
alias(libs.plugins.android.application) apply false
|
alias(libs.plugins.android.application) apply false
|
||||||
alias(libs.plugins.kotlin.android) apply false
|
alias(libs.plugins.kotlin.android) apply false
|
||||||
alias(libs.plugins.kotlin.compose) apply false
|
alias(libs.plugins.kotlin.compose) apply false
|
||||||
|
alias(libs.plugins.android.library) apply false
|
||||||
|
alias(libs.plugins.dagger.hilt.android) apply false
|
||||||
}
|
}
|
@ -8,8 +8,30 @@ espressoCore = "3.6.1"
|
|||||||
lifecycleRuntimeKtx = "2.9.0"
|
lifecycleRuntimeKtx = "2.9.0"
|
||||||
activityCompose = "1.10.1"
|
activityCompose = "1.10.1"
|
||||||
composeBom = "2024.09.00"
|
composeBom = "2024.09.00"
|
||||||
|
appcompat = "1.7.0"
|
||||||
|
material = "1.12.0"
|
||||||
|
retrofit = "2.11.0"
|
||||||
|
okhttp3 = "4.12.0"
|
||||||
|
kotlinxSerializationConverter = "1.0.0"
|
||||||
|
kotlinxSerializationJson = "1.8.0"
|
||||||
|
kotlinxCoroutinesTest = "1.10.2"
|
||||||
|
datastorePreferences = "1.1.6"
|
||||||
|
dagger = "2.55"
|
||||||
|
hiltNavigationCompose = "1.2.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "dagger" }
|
||||||
|
hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "dagger" }
|
||||||
|
hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
|
||||||
|
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastorePreferences" }
|
||||||
|
mockwebserver = { group = "com.squareup.okhttp3", name = "mockwebserver", version.ref = "okhttp3" }
|
||||||
|
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" }
|
||||||
|
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
|
||||||
|
retrofit2-kotlinx-serialization-converter = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "kotlinxSerializationConverter" }
|
||||||
|
logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp3" }
|
||||||
|
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp3" }
|
||||||
|
retrofit2 = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
||||||
|
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||||
@ -24,9 +46,14 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
|
|||||||
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
|
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||||
|
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
dagger-hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "dagger" }
|
||||||
|
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
|
android-library = { id = "com.android.library", version.ref = "agp" }
|
||||||
|
|
||||||
|
@ -21,4 +21,4 @@ dependencyResolutionManagement {
|
|||||||
|
|
||||||
rootProject.name = "api"
|
rootProject.name = "api"
|
||||||
include(":app")
|
include(":app")
|
||||||
|
include(":api-core")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user