auth tests fix

This commit is contained in:
user5 2025-05-27 11:28:52 +03:00
parent fba10fa426
commit 14e2b420e2
43 changed files with 933 additions and 30 deletions

1
api-core/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

52
api-core/build.gradle.kts Normal file
View 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)
}

View File

21
api-core/proguard-rules.pro vendored Normal file
View 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

View File

@ -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)
}
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@ -0,0 +1,27 @@
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 retrofit = Retrofit.Builder()
.baseUrl("https://api.matule.ru/api/")
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.client(OkHttpClient.Builder().addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}).build())
.build()
return BaseApiRepository(retrofit.create<ApiService>(), dataStoreRepository)
}
}

View File

@ -0,0 +1,30 @@
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.SalesAndActionsDto
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
/**
* Автор: Манякин Дмитрий (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 salesAndProjects(): SalesAndActionsDto
// @GET("collections/products/records")
// suspend fun products():
}

View File

@ -0,0 +1,35 @@
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 retrofit2.HttpException
import java.net.UnknownHostException
/**
* Автор: Манякин Дмитрий (user5)
* Дата создания: 27.05.2025
* */
internal class BaseApiRepository(
private val service: ApiService,
private val dataStoreRepository: DataStoreRepository
) : RemoteMapper(), ApiRepository {
override suspend fun auth(registerUser: RegisterUser): FetchResult<AuthUserResponse> {
return try {
service.register(registerUser.toRegisterUserDto())
val response = service.auth(registerUser.toSignInUserDto())
dataStoreRepository.saveToken(response.token)
FetchResult.Success(response.toUserResponse())
} 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!!)
}
}
}

View File

@ -0,0 +1,38 @@
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")
}
}

View File

@ -0,0 +1,58 @@
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.SignInUserDto
import com.example.api.core.data.dto.SignInUserResponseDto
import com.example.api.core.data.dto.UserResponseDto
import com.example.api.core.domain.AuthUserResponse
import com.example.api.core.domain.RegisterUser
import com.example.api.core.domain.SignInUserResponse
import com.example.api.core.domain.UserResponse
/**
* Автор: Манякин Дмитрий (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
)
}
}

View File

@ -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
)

View File

@ -0,0 +1,8 @@
package com.example.api.core.data.dto
import kotlinx.serialization.Serializable
@Serializable
internal data class CatalogResponseDto(
val items: List<SaleItemDto>
)

View File

@ -0,0 +1,3 @@
package com.example.api.core.data.dto
//data class ProductDto()

View File

@ -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
)

View File

@ -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
)

View File

@ -0,0 +1,12 @@
package com.example.api.core.data.dto
import kotlinx.serialization.Serializable
@Serializable
internal data class SalesAndActionsDto(
val page: Int,
val perPage: Int,
val totalPage: Int,
val totalItems: Int,
val items: List<SaleItemDto>
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -0,0 +1,11 @@
package com.example.api.core.domain
/**
* Автор: Манякин Дмитрий (user5)
* Дата создания: 27.05.2025
* */
interface ApiRepository {
suspend fun auth(registerUser: RegisterUser): FetchResult<AuthUserResponse>
}

View File

@ -0,0 +1,6 @@
package com.example.api.core.domain
data class AuthUserResponse(
val record: SignInUserResponse,
val token: String
)

View File

@ -0,0 +1,5 @@
package com.example.api.core.domain
data class CatalogResponse(
val items: List<SaleItem>
)

View File

@ -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)
}
}
}

View File

@ -0,0 +1,7 @@
package com.example.api.core.domain
data class RegisterUser(
val email: String,
val password: String,
val passwordConfirm: String
)

View File

@ -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
)

View File

@ -0,0 +1,9 @@
package com.example.api.core.domain
internal data class SalesAndActions(
val page: Int,
val perPage: Int,
val totalPage: Int,
val totalItems: Int,
val items: List<SaleItem>
)

View File

@ -0,0 +1,6 @@
package com.example.api.core.domain
data class SignInUser(
val identity: String,
val password: String
)

View File

@ -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
)

View File

@ -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
)

View File

@ -0,0 +1,133 @@
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
* */
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)
}
@After
fun teardown() {
mockWebServer.shutdown()
}
}

View File

@ -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"))
} }

View File

@ -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"

View 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()

View 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")
}
}

View 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)
}
}

View 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
) {
}
}
}

View File

@ -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")
}
}

View File

@ -0,0 +1,47 @@
package com.example.api
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.api.core.domain.ApiRepository
import com.example.api.core.domain.AuthUserResponse
import com.example.api.core.domain.RegisterUser
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
/**
* Автор: Манякин Дмитрий (user5)
* Дата создания: 27.05.2025
* */
@HiltViewModel
class MainViewModel @Inject constructor(
private val apiRepository: ApiRepository
) : ViewModel() {
private val _loadResultUiState =
MutableStateFlow<FetchResultUiState<AuthUserResponse>>(FetchResultUiState.Initial())
val loadResultUiState: StateFlow<FetchResultUiState<AuthUserResponse>>
get() = _loadResultUiState.asStateFlow()
fun auth(email: String, password: String) {
viewModelScope.launch(Dispatchers.IO) {
val result = apiRepository.auth(
RegisterUser(
email = email,
password = password,
passwordConfirm = password
)
)
withContext(Dispatchers.Main) {
_loadResultUiState.value = result.map(FetchResultMapper())
}
}
}
}

View File

@ -0,0 +1,41 @@
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.loadResultUiState.collectAsState()
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column {
Button(onClick = { viewModel.auth("someemael@te3st1.com", "12345678") }) {
Text("register")
}
state.Show(
onSuccess = { Text(text = it.token) },
onError = { _, message -> Text(text = message) },
onLoading = { CircularProgressIndicator() }
)
}
}
}

View File

@ -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
} }

View File

@ -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" }

View File

@ -21,4 +21,4 @@ dependencyResolutionManagement {
rootProject.name = "api" rootProject.name = "api"
include(":app") include(":app")
include(":api-core")