feat: moving to ktor and kotlin

This commit is contained in:
a1pha 2025-02-18 20:33:00 +03:00
parent 5adcbadb5d
commit 4b95caf144
34 changed files with 346 additions and 355 deletions

View File

@ -37,6 +37,14 @@ android {
dependencies {
defaultLibrary()
val ktorClientCore = "3.0.3"
implementation("io.ktor:ktor-client-cio:${ktorClientCore}")
implementation("io.ktor:ktor-client-content-negotiation:${ktorClientCore}")
implementation("io.ktor:ktor-client-core:${ktorClientCore}")
implementation("io.ktor:ktor-client-logging:${ktorClientCore}")
implementation("io.ktor:ktor-serialization-kotlinx-json:${ktorClientCore}")
implementation(Dependencies.AndroidX.activity)
implementation(Dependencies.AndroidX.fragment)
implementation(Dependencies.AndroidX.constraintLayout)
@ -49,7 +57,7 @@ dependencies {
implementation("com.squareup.picasso:picasso:2.8")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")
implementation("androidx.datastore:datastore-preferences:1.1.1")
implementation("androidx.datastore:datastore-preferences:1.1.2")
implementation("com.google.mlkit:barcode-scanning:17.3.0")
val cameraX = "1.3.4"

View File

@ -1,5 +1,5 @@
package ru.myitschool.work.core
// БЕРИТЕ И ИЗМЕНЯЙТЕ ХОСТ ТОЛЬКО ЗДЕСЬ И НЕ БЕРИТЕ ИЗ ДРУГИХ МЕСТ. ФАЙЛ ПЕРЕМЕЩАТЬ НЕЛЬЗЯ
object Constants {
const val SERVER_ADDRESS = "http://192.168.1.6:8080"
const val SERVER_ADDRESS = "http://192.168.1.103:8080"
}

View File

@ -0,0 +1,15 @@
package ru.myitschool.work.data
import ru.myitschool.work.data.local.CredentialsLocalDataSource
import ru.myitschool.work.domain.entities.QrEntity
import ru.myitschool.work.domain.qr.QrRepository
class QrRepositoryImpl(
private val networkDataSource: QrNetworkDataSource,
private val credentialsLocalDataSource: CredentialsLocalDataSource
) : QrRepository {
override suspend fun pushQr(qrEntity: QrEntity): Result<Unit> {
return networkDataSource.pushQr(qrEntity, credentialsLocalDataSource.authData!!)
}
}

View File

@ -0,0 +1,46 @@
package ru.myitschool.work.data
import ru.myitschool.work.data.local.CredentialsLocalDataSource
import ru.myitschool.work.data.local.UserLocalDataSource
import ru.myitschool.work.domain.entities.UserEntity
import ru.myitschool.work.domain.login.LoginRepository
import ru.myitschool.work.data.network.UserNetworkDataSource
import ru.myitschool.work.domain.user.UserRepository
class UserRepositoryImpl(
private val credentialsLocalDataSource: CredentialsLocalDataSource,
private val userLocalDatSource: UserLocalDataSource,
private val networkDataSource: UserNetworkDataSource
) : LoginRepository, UserRepository {
override suspend fun login(login: String, password: String): Result<Unit> {
return runCatching {
networkDataSource.login(credentialsLocalDataSource.setAuthData(login, password))
.onSuccess { dto ->
userLocalDatSource.cacheData(
UserEntity(
id = dto.id ?: error("Null user id"),
name = dto.name ?: error("Null user name"),
lastVisit = dto.lastVisit ?: error("Null user lastVisit"),
photoUrl = dto.photoUrl ?: error("Null user photoUrl"),
position = dto.position ?: error("Null user position")
)
)
}
}
}
override suspend fun logout() {
credentialsLocalDataSource.clear()
userLocalDatSource.clear()
}
override suspend fun isUserExist(login: String): Result<Boolean> {
return networkDataSource.isUserExist(login)
}
override suspend fun getCurrentUser(): UserEntity {
return userLocalDatSource.getUser()!!
}
}

View File

@ -1,80 +0,0 @@
package ru.myitschool.work.data;
import androidx.annotation.NonNull;
import java.util.function.Consumer;
import ru.myitschool.work.data.dto.QrDto;
import ru.myitschool.work.data.network.RetrofitFactory;
import ru.myitschool.work.data.source.Credentials;
import ru.myitschool.work.data.source.UserApi;
import ru.myitschool.work.domain.entities.QrEntity;
import ru.myitschool.work.domain.entities.Status;
import ru.myitschool.work.domain.entities.UserEntity;
import ru.myitschool.work.domain.login.LoginRepository;
import ru.myitschool.work.domain.qr.QrRepository;
import ru.myitschool.work.domain.user.UserRepository;
import ru.myitschool.work.utils.CallToConsumer;
public class UserRepositoryImplementation implements UserRepository, LoginRepository, QrRepository {
private static UserRepositoryImplementation INSTANCE;
private final UserApi userApi = RetrofitFactory.getInstance().getUserApi();
private final Credentials credentials = Credentials.getInstance();
private UserRepositoryImplementation() {}
public static synchronized UserRepositoryImplementation getInstance() {
if (INSTANCE == null) {
INSTANCE = new UserRepositoryImplementation();
}
return INSTANCE;
}
@Override
public void getUserByLogin(@NonNull String login, @NonNull Consumer<Status<UserEntity>> callback) {
userApi.getByLogin(login).enqueue(new CallToConsumer<>(
callback,
userDto -> {
if (userDto != null) {
final String resultLogin = userDto.login;
final String id = userDto.id;
final String name = userDto.name;
if (resultLogin != null && id != null && name != null) {
return new UserEntity(
id,
resultLogin,
name,
userDto.lastVisit,
userDto.photoUrl,
userDto.position
);
}
}
return null;
}
));
}
@Override
public void isUserExist(@NonNull String login, Consumer<Status<Void>> callback) {
userApi.isExist(login).enqueue(new CallToConsumer<>(
callback,
dto -> null
));
}
@Override
public void logoutUser() {
credentials.setAuthData(null);
}
@Override
public void pushQr(@NonNull QrEntity qrEntity, @NonNull Consumer<Status<Void>> callback) {
userApi.openDoor(qrEntity.getLogin(),
new QrDto(qrEntity.getQr())).enqueue(new CallToConsumer<>(
callback,
dto -> null
));
}
}

View File

@ -1,15 +1,11 @@
package ru.myitschool.work.data.dto;
package ru.myitschool.work.data.dto
import androidx.annotation.Nullable;
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import com.google.gson.annotations.SerializedName;
public class QrDto {
@Nullable
@SerializedName("value")
public String code;
public QrDto(@Nullable String code) {
this.code = code;
}
}
@Serializable
data class QrDto(
@SerialName("value")
val code: String
)

View File

@ -5,6 +5,9 @@ import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import kotlinx.serialization.Serializable;
@Serializable
public class UserDto {
@Nullable

View File

@ -0,0 +1,35 @@
package ru.myitschool.work.data.local;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class CredentialsLocalDataSource {
private static CredentialsLocalDataSource INSTANCE;
private CredentialsLocalDataSource() {}
public static synchronized CredentialsLocalDataSource getInstance() {
if (INSTANCE == null) {
INSTANCE = new CredentialsLocalDataSource();
}
return INSTANCE;
}
@Nullable
private String authData = null;
public String setAuthData(@NonNull String login, @NonNull String password) {
this.authData = okhttp3.Credentials.basic(login, password);
return this.authData;
}
public void clear() {
this.authData = null;
}
@Nullable
public String getAuthData() {
return authData;
}
}

View File

@ -0,0 +1,20 @@
package ru.myitschool.work.data.local
import ru.myitschool.work.domain.entities.UserEntity
object UserLocalDataSource {
private var currentUser: UserEntity? = null
fun cacheData(user: UserEntity) {
currentUser = user
}
fun getUser(): UserEntity? {
return currentUser
}
fun clear() {
currentUser = null
}
}

View File

@ -0,0 +1,37 @@
package ru.myitschool.work.data.network
import android.util.Log
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
object KtorClient {
val client
get() = HttpClient(CIO) {
install(ContentNegotiation) {
json(Json {
isLenient = true
ignoreUnknownKeys = true
})
}
install(Logging) {
logger = object : Logger {
override fun log(message: String) {
Log.d("KTOR_CLIENT", message)
}
}
level = LogLevel.ALL
}
defaultRequest {
contentType(ContentType.Application.Json)
}
}
}

View File

@ -0,0 +1,38 @@
package ru.myitschool.work.data
import io.ktor.client.request.headers
import io.ktor.client.request.patch
import io.ktor.client.request.setBody
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.dto.QrDto
import ru.myitschool.work.data.network.KtorClient
import ru.myitschool.work.domain.entities.QrEntity
object QrNetworkDataSource {
suspend fun pushQr(qrEntity: QrEntity, token: String): Result<Unit> = withContext(Dispatchers.IO) {
runCatching {
val response = KtorClient.client.patch("${Constants.SERVER_ADDRESS}/api/push_qr") {
headers {
append(HttpHeaders.Authorization, token)
}
contentType(ContentType.Application.Json)
setBody(
QrDto(code = qrEntity.code)
)
}
if (response.status != HttpStatusCode.OK)
error("Status ${response.status}")
Unit
}
}
}

View File

@ -1,28 +0,0 @@
package ru.myitschool.work.data.network;
import static ru.myitschool.work.core.Constants.SERVER_ADDRESS;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import ru.myitschool.work.data.source.UserApi;
public class RetrofitFactory {
private static RetrofitFactory INSTANCE;
public static synchronized RetrofitFactory getInstance() {
if (INSTANCE == null) {
INSTANCE = new RetrofitFactory();
}
return INSTANCE;
}
private final Retrofit retrofit = new Retrofit.Builder()
.baseUrl(SERVER_ADDRESS)
.addConverterFactory(GsonConverterFactory.create())
.build();
public UserApi getUserApi() {
return retrofit.create(UserApi.class);
}
}

View File

@ -0,0 +1,37 @@
package ru.myitschool.work.data.network
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.headers
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.dto.UserDto
object UserNetworkDataSource {
suspend fun isUserExist(login: String): Result<Boolean> = withContext(Dispatchers.IO) {
runCatching {
val result = KtorClient.client.get("${Constants.SERVER_ADDRESS}/api/users/username/$login")
result.status == HttpStatusCode.OK
}
}
suspend fun login(token: String): Result<UserDto> =
withContext(Dispatchers.IO) {
runCatching {
val result = KtorClient.client.get("${Constants.SERVER_ADDRESS}/api/users/login") {
headers {
append(HttpHeaders.Authorization, token)
}
}
if (result.status != HttpStatusCode.OK)
error("Status ${result.status}")
result.body()
}
}
suspend fun getUserByLogin() {}
}

View File

@ -1,29 +0,0 @@
package ru.myitschool.work.data.source;
import androidx.annotation.Nullable;
public class Credentials {
private static Credentials INSTANCE;
private Credentials() {}
public static synchronized Credentials getInstance() {
if (INSTANCE == null) {
INSTANCE = new Credentials();
}
return INSTANCE;
}
@Nullable
private String authData = null;
public void setAuthData(@Nullable String authData) {
this.authData = authData;
}
@Nullable
public String getAuthData() {
return authData;
}
}

View File

@ -1,19 +0,0 @@
package ru.myitschool.work.data.source;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.PATCH;
import retrofit2.http.Path;
import ru.myitschool.work.data.dto.QrDto;
import ru.myitschool.work.data.dto.UserDto;
public interface UserApi {
@GET("api/{login}/info")
Call<UserDto> getByLogin(@Path("login") String login);
@GET("api/{login}/auth")
Call<Void> isExist(@Path("login") String login);
@PATCH("api/{login}/open/")
Call<Void> openDoor(@Path("login") String login, @Body QrDto qrDto);
}

View File

@ -1,27 +1,5 @@
package ru.myitschool.work.domain.entities;
package ru.myitschool.work.domain.entities
import androidx.annotation.NonNull;
public class QrEntity {
@NonNull
private final String login;
@NonNull
public String getQr() {
return qr;
}
@NonNull
private final String qr;
@NonNull
public String getLogin() {
return login;
}
public QrEntity(@NonNull String login, @NonNull String qr) {
this.login = login;
this.qr = qr;
}
}
data class QrEntity(
val code: String
)

View File

@ -1,65 +1,9 @@
package ru.myitschool.work.domain.entities;
package ru.myitschool.work.domain.entities
import androidx.annotation.NonNull;
import javax.annotation.Nullable;
public class UserEntity {
@NonNull
private final String id;
@NonNull
private final String name;
@NonNull
private final String login;
@Nullable
private final String last_visit;
@Nullable
private final String photoUrl;
@Nullable
private final String position;
@NonNull
public String getId() {
return id;
}
@Nullable
public String getLast_visit() {
return last_visit;
}
@NonNull
public String getName() {
return name;
}
@Nullable
public String getPhotoUrl() {
return photoUrl;
}
@Nullable
public String getPosition() {
return position;
}
@NonNull
public String getLogin() {
return login;
}
public UserEntity(
@NonNull String id, @NonNull String login,
@NonNull String name,
@Nullable String last_visit,
@Nullable String photoUrl,
@Nullable String position) {
this.id = id;
this.login = login;
this.name = name;
this.last_visit = last_visit;
this.photoUrl = photoUrl;
this.position = position;
}
}
data class UserEntity(
val id: String,
val name: String,
val lastVisit: String,
val photoUrl: String,
val position: String,
)

View File

@ -1,29 +1,11 @@
package ru.myitschool.work.domain.login;
package ru.myitschool.work.domain.login
import androidx.annotation.NonNull;
import android.util.Log
import kotlin.math.log
import java.util.function.Consumer;
class IsUserExistUseCase(
private val repository: LoginRepository
) {
import ru.myitschool.work.domain.entities.Status;
public class IsUserExistUseCase {
private final LoginRepository repository;
public IsUserExistUseCase(LoginRepository repository) {
this.repository = repository;
}
public void execute(@NonNull String login, Consumer<Status<Boolean>> callback) {
repository.isUserExist(login, status -> {
boolean isAvailable = status.getStatusCode() == 200 || status.getStatusCode() == 401;
callback.accept(
new Status<>(
status.getStatusCode(),
isAvailable ? status.getStatusCode() == 200 : null,
status.getErrors()
)
);
});
}
}
suspend operator fun invoke(login: String) = repository.isUserExist(login)
}

View File

@ -1,13 +1,8 @@
package ru.myitschool.work.domain.login;
package ru.myitschool.work.domain.login
import androidx.annotation.NonNull;
interface LoginRepository {
import java.util.function.Consumer;
import ru.myitschool.work.domain.entities.Status;
public interface LoginRepository {
void isUserExist(@NonNull String login, Consumer<Status<Void>> callback);
void logoutUser();
}
suspend fun login(login: String, password: String) : Result<Unit>
suspend fun logout()
suspend fun isUserExist(login: String): Result<Boolean>
}

View File

@ -0,0 +1,9 @@
package ru.myitschool.work.domain.login
class LoginUseCase(
private val repository: LoginRepository
) {
operator fun invoke(login: String, password: String) =
repository.loginUser(login, password)
}

View File

@ -1,28 +1,10 @@
package ru.myitschool.work.domain.qr;
package ru.myitschool.work.domain.qr
import androidx.annotation.NonNull;
import ru.myitschool.work.domain.entities.QrEntity
import java.util.function.Consumer;
class PushQrUseCase(
private val repository: QrRepository
) {
import ru.myitschool.work.domain.entities.QrEntity;
import ru.myitschool.work.domain.entities.Status;
public class PushQrUseCase {
private final QrRepository repository;
public PushQrUseCase(QrRepository repository) {
this.repository = repository;
}
public void execute(@NonNull QrEntity qrEntity, Consumer<Status<Boolean>> callback) {
repository.pushQr(qrEntity, status -> {
boolean isOpened = status.getStatusCode() == 200 || status.getStatusCode() == 400 || status.getStatusCode() == 401;
callback.accept(
new Status<>(
status.getStatusCode(),
isOpened ? status.getStatusCode() == 200 : null,
status.getErrors()
)
);
});
}
}
suspend operator fun invoke(qrEntity: QrEntity) = repository.pushQr(qrEntity)
}

View File

@ -1,13 +1,8 @@
package ru.myitschool.work.domain.qr;
package ru.myitschool.work.domain.qr
import androidx.annotation.NonNull;
import ru.myitschool.work.domain.entities.QrEntity
import java.util.function.Consumer;
interface QrRepository {
import ru.myitschool.work.domain.entities.QrEntity;
import ru.myitschool.work.domain.entities.Status;
public interface QrRepository {
void pushQr(@NonNull QrEntity qrEntity, @NonNull Consumer<Status<Void>> callback);
}
suspend fun pushQr(qrEntity: QrEntity): Result<Unit>
}

View File

@ -5,7 +5,6 @@ import androidx.annotation.NonNull;
import java.util.function.Consumer;
import ru.myitschool.work.domain.entities.Status;
import ru.myitschool.work.domain.entities.UserEntity;
public class GetUserByLoginUseCase {
private final UserRepository repository;

View File

@ -1,14 +1,8 @@
package ru.myitschool.work.domain.user;
package ru.myitschool.work.domain.user
import androidx.annotation.NonNull;
import ru.myitschool.work.domain.entities.UserEntity
import java.util.function.Consumer;
interface UserRepository {
import ru.myitschool.work.domain.entities.Status;
import ru.myitschool.work.domain.entities.UserEntity;
public interface UserRepository {
void getUserByLogin(@NonNull String login, @NonNull Consumer<Status<UserEntity>> callback);
}
suspend fun getCurrentUser(): UserEntity
}

View File

@ -6,9 +6,6 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import ru.myitschool.work.data.UserRepositoryImplementation;
import ru.myitschool.work.domain.login.IsUserExistUseCase;
public class LoginViewModel extends ViewModel {
private final State INIT_STATE = new State(false);

View File

@ -0,0 +1,22 @@
package ru.myitschool.work.ui.login
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import ru.myitschool.work.R
import ru.myitschool.work.databinding.FragmentSplashBinding
class SplashFragment: Fragment(R.layout.fragment_splash) {
private var _binding: FragmentSplashBinding? = null
private val binding: FragmentSplashBinding get() = _binding!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentSplashBinding.bind(view)
}
override fun onDestroy() {
_binding = null
super.onDestroy()
}
}

View File

@ -8,7 +8,6 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentResultListener;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.Navigation;
@ -19,7 +18,6 @@ import java.text.MessageFormat;
import ru.myitschool.work.R;
import ru.myitschool.work.databinding.FragmentUserBinding;
import ru.myitschool.work.domain.entities.UserEntity;
import ru.myitschool.work.ui.qr.scan.QrScanDestination;
import ru.myitschool.work.utils.Utils;

View File

@ -6,8 +6,6 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import ru.myitschool.work.data.UserRepositoryImplementation;
import ru.myitschool.work.domain.entities.UserEntity;
import ru.myitschool.work.domain.user.GetUserByLoginUseCase;
public class UserViewModel extends ViewModel {

View File

@ -6,8 +6,6 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import ru.myitschool.work.data.UserRepositoryImplementation;
import ru.myitschool.work.domain.entities.QrEntity;
import ru.myitschool.work.domain.entities.Status;
import ru.myitschool.work.domain.qr.PushQrUseCase;
@ -16,7 +14,7 @@ public class QrResultViewModel extends ViewModel {
public final LiveData<State> stateLiveData = mutableStateLiveData;
public final PushQrUseCase pushQrUseCase = new PushQrUseCase(
UserRepositoryImplementation.getInstance()
QrRepositoryImplementation.getInstance()
);
public void update(@NonNull String login, @NonNull String qr) {

View File

@ -33,6 +33,18 @@
android:background="@drawable/input_field"
android:hint="@string/login_hint" />
<EditText
android:inputType="textPassword"
android:layout_marginTop="24dp"
android:paddingHorizontal="20dp"
android:fontFamily="@font/manrope_light"
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="50dp"
android:background="@drawable/input_field"
android:hint="@string/password_input_hint" />
<Button
android:foreground="?attr/selectableItemBackground"
android:layout_marginTop="8dp"

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -5,6 +5,7 @@
android:id="@+id/nav_graph"
app:startDestination="@id/loginFragment">
<fragment
android:id="@+id/userFragment"
android:name="ru.myitschool.work.ui.profile.UserFragment"

View File

@ -11,4 +11,6 @@
<string name="error">Что-то пошло не так</string>
<string name="logout_text">Выйти из аккаунта</string>
<string name="last_visit">Последний вход: </string>
<string name="password_input_hint">Введите пароль</string>
<string name="register_process_button_text">Зарегестрироваться</string>
</resources>

View File

@ -34,7 +34,7 @@ object Version {
object Android {
object Sdk {
const val min = 24
const val min = 28
const val compile = 34
const val target = 34
}