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.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
alias(libs.plugins.dagger.hilt.android)
|
||||
kotlin("kapt")
|
||||
}
|
||||
|
||||
android {
|
||||
@ -40,6 +42,10 @@ android {
|
||||
}
|
||||
|
||||
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.lifecycle.runtime.ktx)
|
||||
@ -56,4 +62,6 @@ dependencies {
|
||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
|
||||
implementation(project(":api-core"))
|
||||
}
|
@ -2,7 +2,9 @@
|
||||
<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"
|
||||
|
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.compose.setContent
|
||||
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 dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
ApiTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
Greeting(
|
||||
name = "Android",
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
)
|
||||
}
|
||||
TestScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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.kotlin.android) 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"
|
||||
activityCompose = "1.10.1"
|
||||
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]
|
||||
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" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
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-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
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]
|
||||
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" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", 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"
|
||||
include(":app")
|
||||
|
||||
include(":api-core")
|
||||
|
Loading…
x
Reference in New Issue
Block a user