feat: moved fully to Kotlin and ktor
This commit is contained in:
parent
f4e53d6e89
commit
a15f1fb00e
@ -37,8 +37,10 @@ android {
|
||||
dependencies {
|
||||
defaultLibrary()
|
||||
|
||||
val ktorClientCore = "3.0.3"
|
||||
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||
|
||||
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}")
|
||||
@ -56,7 +58,7 @@ dependencies {
|
||||
implementation(Dependencies.Retrofit.gsonConverter)
|
||||
|
||||
implementation("com.squareup.picasso:picasso:2.8")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
|
||||
implementation("androidx.datastore:datastore-preferences:1.1.2")
|
||||
implementation("com.google.mlkit:barcode-scanning:17.3.0")
|
||||
|
||||
|
@ -2,4 +2,5 @@ package ru.myitschool.work.core
|
||||
// БЕРИТЕ И ИЗМЕНЯЙТЕ ХОСТ ТОЛЬКО ЗДЕСЬ И НЕ БЕРИТЕ ИЗ ДРУГИХ МЕСТ. ФАЙЛ ПЕРЕМЕЩАТЬ НЕЛЬЗЯ
|
||||
object Constants {
|
||||
const val SERVER_ADDRESS = "http://192.168.1.103:8080"
|
||||
const val TOKEN_KEY = "token"
|
||||
}
|
@ -10,6 +10,6 @@ class QrRepositoryImpl(
|
||||
) : QrRepository {
|
||||
|
||||
override suspend fun pushQr(qrEntity: QrEntity): Result<Unit> {
|
||||
return networkDataSource.pushQr(qrEntity, credentialsLocalDataSource.authData!!)
|
||||
return networkDataSource.pushQr(qrEntity, credentialsLocalDataSource.getToken())
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
package ru.myitschool.work.data
|
||||
|
||||
import android.util.Log
|
||||
import ru.myitschool.work.data.dto.UserDto
|
||||
import ru.myitschool.work.data.local.CredentialsLocalDataSource
|
||||
import ru.myitschool.work.data.local.UserLocalDataSource
|
||||
import ru.myitschool.work.domain.entities.UserEntity
|
||||
@ -9,31 +11,30 @@ import ru.myitschool.work.domain.user.UserRepository
|
||||
|
||||
class UserRepositoryImpl(
|
||||
private val credentialsLocalDataSource: CredentialsLocalDataSource,
|
||||
private val userLocalDatSource: UserLocalDataSource,
|
||||
private val userLocalDataSource: 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))
|
||||
networkDataSource.login(credentialsLocalDataSource.updateToken(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")
|
||||
)
|
||||
)
|
||||
map(dto).onSuccess { userLocalDataSource.cacheData(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun authorize(token: String): Result<Unit> {
|
||||
return networkDataSource.login(token).fold(
|
||||
onSuccess = { Result.success(Unit) },
|
||||
onFailure = { error -> Result.failure(error) }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun logout() {
|
||||
credentialsLocalDataSource.clear()
|
||||
userLocalDatSource.clear()
|
||||
userLocalDataSource.clear()
|
||||
}
|
||||
|
||||
override suspend fun isUserExist(login: String): Result<Boolean> {
|
||||
@ -41,6 +42,18 @@ class UserRepositoryImpl(
|
||||
}
|
||||
|
||||
override suspend fun getCurrentUser(): UserEntity {
|
||||
return userLocalDatSource.getUser()!!
|
||||
return userLocalDataSource.getUser()!!
|
||||
}
|
||||
|
||||
private fun map(userDto: UserDto): Result<UserEntity> {
|
||||
return runCatching {
|
||||
UserEntity(
|
||||
id = userDto.id ?: error("Null user id"),
|
||||
name = userDto.name ?: error("Null user name"),
|
||||
lastVisit = userDto.lastVisit ?: error("Null user lastVisit"),
|
||||
photoUrl = userDto.photoUrl ?: error("Null user photoUrl"),
|
||||
position = userDto.position ?: error("Null user position")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,35 +1,51 @@
|
||||
package ru.myitschool.work.data.local;
|
||||
package ru.myitschool.work.data.local
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.content.SharedPreferences
|
||||
import okhttp3.Credentials
|
||||
import ru.myitschool.work.core.Constants.TOKEN_KEY
|
||||
|
||||
public class CredentialsLocalDataSource {
|
||||
class CredentialsLocalDataSource private constructor(private val preferences: SharedPreferences) {
|
||||
companion object {
|
||||
|
||||
private static CredentialsLocalDataSource INSTANCE;
|
||||
@Volatile
|
||||
private var INSTANCE: CredentialsLocalDataSource? = null
|
||||
|
||||
private CredentialsLocalDataSource() {}
|
||||
|
||||
public static synchronized CredentialsLocalDataSource getInstance() {
|
||||
if (INSTANCE == null) {
|
||||
INSTANCE = new CredentialsLocalDataSource();
|
||||
fun getInstance(): CredentialsLocalDataSource {
|
||||
return INSTANCE!!
|
||||
}
|
||||
|
||||
fun buildSource(sharedPreferences: SharedPreferences) {
|
||||
INSTANCE = CredentialsLocalDataSource(sharedPreferences)
|
||||
}
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String authData = null;
|
||||
private var savedToken: String? = preferences.getString(TOKEN_KEY, null)
|
||||
|
||||
public String setAuthData(@NonNull String login, @NonNull String password) {
|
||||
this.authData = okhttp3.Credentials.basic(login, password);
|
||||
return this.authData;
|
||||
fun updateToken(login: String, password: String): String {
|
||||
val updatedToken = Credentials.basic(login, password)
|
||||
savedToken = updatedToken
|
||||
cacheData()
|
||||
return updatedToken
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
this.authData = null;
|
||||
private fun cacheData() {
|
||||
with(preferences.edit()) {
|
||||
putString(TOKEN_KEY, savedToken)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getAuthData() {
|
||||
return authData;
|
||||
fun getToken(): String {
|
||||
return savedToken!!
|
||||
}
|
||||
}
|
||||
|
||||
fun getTokenForAuth(): Result<String> {
|
||||
return if (savedToken != null) Result.success(savedToken!!) else
|
||||
Result.failure(IllegalStateException("User was not authorized"))
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
preferences.edit().clear().apply()
|
||||
savedToken = null
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package ru.myitschool.work.domain.entities;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class Status<T> {
|
||||
|
||||
private final int statusCode;
|
||||
|
||||
@Nullable
|
||||
private final T value;
|
||||
|
||||
@Nullable
|
||||
private final Throwable errors;
|
||||
|
||||
public Status(int statusCode, @Nullable T value, @Nullable Throwable errors) {
|
||||
this.errors = errors;
|
||||
this.statusCode = statusCode;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Throwable getErrors() {
|
||||
return errors;
|
||||
}
|
||||
|
||||
public int getStatusCode() {
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public T getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package ru.myitschool.work.domain.login
|
||||
|
||||
class AuthorizeUseCase(
|
||||
private val repository: LoginRepository
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(token: String) : Result<Unit> = repository.authorize(token)
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
package ru.myitschool.work.domain.login
|
||||
|
||||
import android.util.Log
|
||||
import kotlin.math.log
|
||||
|
||||
class IsUserExistUseCase(
|
||||
private val repository: LoginRepository
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(login: String) = repository.isUserExist(login)
|
||||
suspend operator fun invoke(login: String): Result<Boolean> {
|
||||
return repository.isUserExist(login)
|
||||
}
|
||||
}
|
@ -5,4 +5,5 @@ interface LoginRepository {
|
||||
suspend fun login(login: String, password: String) : Result<Unit>
|
||||
suspend fun logout()
|
||||
suspend fun isUserExist(login: String): Result<Boolean>
|
||||
suspend fun authorize(token: String): Result<Unit>
|
||||
}
|
@ -4,6 +4,6 @@ class LoginUseCase(
|
||||
private val repository: LoginRepository
|
||||
) {
|
||||
|
||||
operator fun invoke(login: String, password: String) =
|
||||
repository.loginUser(login, password)
|
||||
suspend operator fun invoke(login: String, password: String) =
|
||||
repository.login(login, password)
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package ru.myitschool.work.domain.login
|
||||
|
||||
class LogoutUseCase(
|
||||
private val repository: LoginRepository
|
||||
) {
|
||||
|
||||
suspend operator fun invoke() = repository.logout()
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package ru.myitschool.work.domain.user
|
||||
|
||||
|
||||
class GetCurrentUserUseCase(
|
||||
private val repository: UserRepository
|
||||
) {
|
||||
|
||||
suspend operator fun invoke() = repository.getCurrentUser()
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package ru.myitschool.work.domain.user;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import ru.myitschool.work.domain.entities.Status;
|
||||
|
||||
public class GetUserByLoginUseCase {
|
||||
private final UserRepository repository;
|
||||
|
||||
public GetUserByLoginUseCase(UserRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public void execute(@NonNull String login, @NonNull Consumer<Status<UserEntity>> callback) {
|
||||
repository.getUserByLogin(login, callback);
|
||||
}
|
||||
}
|
@ -1,81 +1,64 @@
|
||||
package ru.myitschool.work.ui.login;
|
||||
package ru.myitschool.work.ui.login
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.view.View;
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import ru.myitschool.work.R
|
||||
import ru.myitschool.work.databinding.FragmentLoginBinding
|
||||
import ru.myitschool.work.utils.collectWithLifecycle
|
||||
import ru.myitschool.work.utils.visibleOrGone
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.Navigation;
|
||||
class LoginFragment : Fragment(R.layout.fragment_login) {
|
||||
|
||||
import ru.myitschool.work.R;
|
||||
import ru.myitschool.work.databinding.FragmentLoginBinding;
|
||||
import ru.myitschool.work.utils.OnChangeText;
|
||||
import ru.myitschool.work.utils.Utils;
|
||||
private var _binding: FragmentLoginBinding? = null
|
||||
private val binding: FragmentLoginBinding get() = _binding!!
|
||||
|
||||
public class LoginFragment extends Fragment {
|
||||
private FragmentLoginBinding binding;
|
||||
private LoginViewModel viewModel;
|
||||
private val viewModel by viewModels<LoginViewModel> { LoginViewModel.Factory }
|
||||
|
||||
public LoginFragment() {
|
||||
super(R.layout.fragment_login);
|
||||
}
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
_binding = FragmentLoginBinding.bind(view)
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
if (getContext() != null) {
|
||||
if (Utils.getLogin(getContext()) != null && getView() != null)
|
||||
Navigation.findNavController(getView()).navigate(
|
||||
R.id.action_loginFragment_to_userFragment);
|
||||
binding.username.doAfterTextChanged { login ->
|
||||
viewModel.changeLogin(login = login.toString())
|
||||
}
|
||||
|
||||
binding = FragmentLoginBinding.bind(view);
|
||||
viewModel = new ViewModelProvider(this).get(LoginViewModel.class);
|
||||
binding.username.addTextChangedListener(new OnChangeText() {
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
super.afterTextChanged(s);
|
||||
viewModel.changeLogin(s.toString());
|
||||
viewModel.state.collectWithLifecycle(this) { state ->
|
||||
binding.username.isEnabled = state !is LoginViewModel.State.Loading
|
||||
binding.password.isEnabled = state !is LoginViewModel.State.Loading
|
||||
binding.error.visibleOrGone(state is LoginViewModel.State.Error)
|
||||
|
||||
when (state) {
|
||||
is LoginViewModel.State.Error -> binding.error.text = state.errorMessage
|
||||
is LoginViewModel.State.Loading -> Unit
|
||||
is LoginViewModel.State.Waiting -> Unit
|
||||
is LoginViewModel.State.LoginCheckCompleted -> binding.login.isEnabled =
|
||||
state.isCompleted
|
||||
}
|
||||
});
|
||||
binding.login.setOnClickListener(v -> viewModel.confirm());
|
||||
subscribe(viewModel);
|
||||
}
|
||||
|
||||
viewModel.action.collectWithLifecycle(this) { action ->
|
||||
when (action) {
|
||||
is LoginViewModel.Action.OpenApp ->
|
||||
findNavController().navigate(R.id.action_loginFragment_to_userFragment)
|
||||
|
||||
is LoginViewModel.Action.GoToLogin -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
binding.login.setOnClickListener {
|
||||
viewModel.onProcessClick(
|
||||
login = binding.username.text.toString(),
|
||||
password = binding.password.text.toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void subscribe(LoginViewModel viewModel) {
|
||||
viewModel.errorLiveData.observe(getViewLifecycleOwner(), error ->
|
||||
binding.error.setVisibility(Utils.visibleOrGone(error != null)));
|
||||
viewModel.stateLiveData.observe(getViewLifecycleOwner(), state -> {
|
||||
binding.login.setEnabled(state.isButtonActive());
|
||||
if (state.isButtonActive()) {
|
||||
binding.login.setBackground(ResourcesCompat.getDrawable(getResources(), R.drawable.main_button, getContext().getTheme()));
|
||||
binding.login.setTextColor(ResourcesCompat.getColor(getResources(), R.color.white, getContext().getTheme()));
|
||||
} else {
|
||||
binding.login.setBackground(ResourcesCompat.getDrawable(getResources(), R.drawable.inactive_button, getContext().getTheme()));
|
||||
binding.login.setTextColor(ResourcesCompat.getColor(getResources(), R.color.black, getContext().getTheme()));
|
||||
}
|
||||
});
|
||||
viewModel.openProfileLiveData.observe(getViewLifecycleOwner(), (unused) -> {
|
||||
|
||||
if (getContext() != null) {
|
||||
Utils.saveLogin(binding.username.getText().toString(), getContext());
|
||||
}
|
||||
|
||||
if (getView() == null) return;
|
||||
Navigation.findNavController(getView()).navigate(
|
||||
R.id.action_loginFragment_to_userFragment);
|
||||
});
|
||||
override fun onDestroy() {
|
||||
_binding = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
binding = null;
|
||||
super.onDestroyView();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,73 +1,126 @@
|
||||
package ru.myitschool.work.ui.login;
|
||||
package ru.myitschool.work.ui.login
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.CreationExtras
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.myitschool.work.data.UserRepositoryImpl
|
||||
import ru.myitschool.work.data.local.CredentialsLocalDataSource
|
||||
import ru.myitschool.work.data.local.UserLocalDataSource
|
||||
import ru.myitschool.work.data.network.UserNetworkDataSource
|
||||
import ru.myitschool.work.domain.login.AuthorizeUseCase
|
||||
import ru.myitschool.work.domain.login.IsUserExistUseCase
|
||||
import ru.myitschool.work.domain.login.LoginUseCase
|
||||
|
||||
public class LoginViewModel extends ViewModel {
|
||||
class LoginViewModel(
|
||||
private val loginUseCase: LoginUseCase,
|
||||
private val isUserExistUseCase: IsUserExistUseCase,
|
||||
private val authorizeUseCase: AuthorizeUseCase,
|
||||
private val application: Application
|
||||
) : AndroidViewModel(application) {
|
||||
|
||||
private final State INIT_STATE = new State(false);
|
||||
private final MutableLiveData<State> mutableStateLiveData = new MutableLiveData<State>(INIT_STATE);
|
||||
public final LiveData<State> stateLiveData = mutableStateLiveData;
|
||||
private final MutableLiveData<String> mutableErrorLiveData = new MutableLiveData<String>();
|
||||
public final LiveData<String> errorLiveData = mutableErrorLiveData;
|
||||
private final MutableLiveData<Void> mutableOpenProfileLiveData = new MutableLiveData<Void>();
|
||||
public final LiveData<Void> openProfileLiveData = mutableOpenProfileLiveData;
|
||||
private val _state = MutableStateFlow<State>(State.Waiting)
|
||||
private val _action = Channel<Action>(
|
||||
capacity = Channel.BUFFERED,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
val state = _state.asStateFlow()
|
||||
val action = _action.receiveAsFlow()
|
||||
private var userCheckCompleted = false
|
||||
private var currentLogin: String? = null
|
||||
|
||||
private final IsUserExistUseCase isUserExistUseCase = new IsUserExistUseCase(
|
||||
UserRepositoryImplementation.getInstance()
|
||||
);
|
||||
|
||||
@Nullable
|
||||
private String login = null;
|
||||
private boolean userCheckCompleted = false;
|
||||
public void changeLogin(@NonNull String login) {
|
||||
this.login = login;
|
||||
userCheckCompleted = !login.isBlank() &&
|
||||
login.length() >= 3 &&
|
||||
!Character.isDigit(login.charAt(0)) &&
|
||||
login.matches("^[a-zA-Z0-9]+$");
|
||||
|
||||
mutableStateLiveData.postValue(new State(userCheckCompleted));
|
||||
}
|
||||
|
||||
public void confirm() {
|
||||
checkUserExist();
|
||||
}
|
||||
|
||||
private void checkUserExist() {
|
||||
final String currentLogin = login;
|
||||
if (currentLogin == null || currentLogin.isEmpty()) {
|
||||
mutableErrorLiveData.postValue("Login cannot be null");
|
||||
return;
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
CredentialsLocalDataSource.getInstance().getTokenForAuth().fold(
|
||||
onSuccess = { token ->
|
||||
authorizeUseCase(token).fold(
|
||||
onSuccess = { _action.send(Action.OpenApp) },
|
||||
onFailure = { _action.send(Action.GoToLogin) })
|
||||
},
|
||||
onFailure = { _action.send(Action.GoToLogin) }
|
||||
)
|
||||
}
|
||||
isUserExistUseCase.execute(currentLogin, status -> {
|
||||
if (status.getValue() == null || status.getErrors() != null) {
|
||||
mutableErrorLiveData.postValue("Something went wrong. Try again later");
|
||||
return;
|
||||
}
|
||||
|
||||
fun changeLogin(login: String) {
|
||||
viewModelScope.launch {
|
||||
currentLogin = login
|
||||
userCheckCompleted = login.isNotBlank() && login.length >= 3 &&
|
||||
!Character.isDigit(login[0]) &&
|
||||
login.matches("^[a-zA-Z0-9]+$".toRegex())
|
||||
|
||||
_state.emit(State.LoginCheckCompleted(userCheckCompleted))
|
||||
}
|
||||
}
|
||||
|
||||
fun onProcessClick(login: String, password: String) {
|
||||
viewModelScope.launch {
|
||||
_state.emit(State.Loading)
|
||||
isUserExistUseCase.invoke(login).fold(
|
||||
onSuccess = { response ->
|
||||
if (response) {
|
||||
loginUseCase.invoke(login, password).fold(
|
||||
onSuccess = { openApp() },
|
||||
onFailure = { error ->
|
||||
_state.emit(State.Error(error.message.toString()))
|
||||
}
|
||||
)
|
||||
} else {
|
||||
_state.emit(State.Error("No such user or incorrect"))
|
||||
}
|
||||
},
|
||||
onFailure = { error -> _state.emit(State.Error(error.message.toString())) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openApp() {
|
||||
viewModelScope.launch {
|
||||
_action.send(Action.OpenApp)
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface State {
|
||||
data object Waiting : State
|
||||
data object Loading : State
|
||||
data class Error(val errorMessage: String) : State
|
||||
data class LoginCheckCompleted(val isCompleted: Boolean) : State
|
||||
}
|
||||
|
||||
sealed interface Action {
|
||||
data object OpenApp : Action
|
||||
data object GoToLogin : Action
|
||||
}
|
||||
|
||||
companion object {
|
||||
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
|
||||
val application =
|
||||
extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]!!
|
||||
|
||||
val repository = UserRepositoryImpl(
|
||||
userLocalDataSource = UserLocalDataSource,
|
||||
credentialsLocalDataSource = CredentialsLocalDataSource.getInstance(),
|
||||
networkDataSource = UserNetworkDataSource
|
||||
)
|
||||
|
||||
return LoginViewModel(
|
||||
loginUseCase = LoginUseCase(repository),
|
||||
isUserExistUseCase = IsUserExistUseCase(repository),
|
||||
authorizeUseCase = AuthorizeUseCase(repository),
|
||||
application = application
|
||||
) as T
|
||||
}
|
||||
if (status.getStatusCode() == 401) {
|
||||
mutableErrorLiveData.postValue("There is no such login or incorrect");
|
||||
} else if (status.getStatusCode() == 200) {
|
||||
mutableOpenProfileLiveData.postValue(null);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public class State {
|
||||
private final boolean isButtonActive;
|
||||
|
||||
public boolean isButtonActive() {
|
||||
return isButtonActive;
|
||||
}
|
||||
|
||||
public State(boolean isButtonActive) {
|
||||
this.isButtonActive = isButtonActive;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -1,18 +1,46 @@
|
||||
package ru.myitschool.work.ui.login
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import ru.myitschool.work.R
|
||||
import ru.myitschool.work.core.Constants
|
||||
import ru.myitschool.work.data.local.CredentialsLocalDataSource
|
||||
import ru.myitschool.work.databinding.FragmentSplashBinding
|
||||
import ru.myitschool.work.utils.collectWithLifecycle
|
||||
|
||||
class SplashFragment: Fragment(R.layout.fragment_splash) {
|
||||
class SplashFragment : Fragment(R.layout.fragment_splash) {
|
||||
|
||||
private var _binding: FragmentSplashBinding? = null
|
||||
private val binding: FragmentSplashBinding get() = _binding!!
|
||||
|
||||
private val viewModel by viewModels<LoginViewModel> { LoginViewModel.Factory }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
CredentialsLocalDataSource.buildSource(
|
||||
requireActivity().getSharedPreferences(
|
||||
Constants.TOKEN_KEY,
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
_binding = FragmentSplashBinding.bind(view)
|
||||
|
||||
viewModel.action.collectWithLifecycle(this) { action ->
|
||||
val navController = findNavController()
|
||||
when (action) {
|
||||
is LoginViewModel.Action.GoToLogin -> navController.navigate(R.id.loginFragment)
|
||||
is LoginViewModel.Action.OpenApp ->
|
||||
navController.navigate(R.id.action_loginFragment_to_userFragment)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
@ -1,109 +1,55 @@
|
||||
package ru.myitschool.work.ui.profile;
|
||||
package ru.myitschool.work.ui.profile
|
||||
|
||||
import static ru.myitschool.work.ui.qr.result.QrResultFragment.RESPONSE_KEY;
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.squareup.picasso.Picasso
|
||||
import ru.myitschool.work.R
|
||||
import ru.myitschool.work.databinding.FragmentUserBinding
|
||||
import ru.myitschool.work.utils.collectWithLifecycle
|
||||
import ru.myitschool.work.utils.visibleOrGone
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
class UserFragment : Fragment(R.layout.fragment_user) {
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentResultListener;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.Navigation;
|
||||
private var _binding: FragmentUserBinding? = null
|
||||
private val binding: FragmentUserBinding get() = _binding!!
|
||||
|
||||
import com.squareup.picasso.Picasso;
|
||||
private val viewModel by viewModels<UserViewModel> { UserViewModel.Factory }
|
||||
|
||||
import java.text.MessageFormat;
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
_binding = FragmentUserBinding.bind(view)
|
||||
|
||||
import ru.myitschool.work.R;
|
||||
import ru.myitschool.work.databinding.FragmentUserBinding;
|
||||
import ru.myitschool.work.ui.qr.scan.QrScanDestination;
|
||||
import ru.myitschool.work.utils.Utils;
|
||||
|
||||
public class UserFragment extends Fragment {
|
||||
|
||||
private FragmentUserBinding binding;
|
||||
private UserViewModel viewModel;
|
||||
|
||||
public UserFragment() {
|
||||
super(R.layout.fragment_user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
binding = FragmentUserBinding.bind(view);
|
||||
|
||||
viewModel = new ViewModelProvider(this).get(UserViewModel.class);
|
||||
viewModel.stateLiveData.observe(getViewLifecycleOwner(), state -> {
|
||||
UserEntity entity = state.getItem();
|
||||
if (state.getErrorMessage() != null) {
|
||||
binding.error.setVisibility(View.VISIBLE);
|
||||
binding.error.setText(state.getErrorMessage());
|
||||
|
||||
binding.logout.setVisibility(View.GONE);
|
||||
binding.scan.setVisibility(View.GONE);
|
||||
} else if (entity != null) {
|
||||
binding.photo.setVisibility(Utils.visibleOrGone(entity.getPhotoUrl() != null));
|
||||
binding.position.setVisibility(Utils.visibleOrGone(entity.getPosition() != null));
|
||||
binding.lastEntry.setVisibility(Utils.visibleOrGone(entity.getLast_visit() != null));
|
||||
|
||||
binding.fullname.setText(entity.getName());
|
||||
binding.position.setText(entity.getPosition());
|
||||
String lastVisit = entity.getLast_visit();
|
||||
binding.lastEntry.setText(MessageFormat.format("{0} {1}",
|
||||
lastVisit.substring(0, 10), lastVisit.substring(11, 16)));
|
||||
|
||||
if (entity.getPhotoUrl() != null) {
|
||||
Picasso.get().load(entity.getPhotoUrl()).into(binding.photo);
|
||||
viewModel.state.collectWithLifecycle(this) { state ->
|
||||
(binding.refresh as SwipeRefreshLayout).isRefreshing =
|
||||
state is UserViewModel.State.Loading
|
||||
binding.content?.visibleOrGone(state is UserViewModel.State.Show)
|
||||
when (state) {
|
||||
is UserViewModel.State.Loading -> Unit
|
||||
is UserViewModel.State.Show -> {
|
||||
val user = state.userEntity
|
||||
binding.fullname.text = user.name
|
||||
binding.position.text = user.position
|
||||
binding.lastEntry.text = user.lastVisit
|
||||
Picasso.get().load(user.photoUrl).into(binding.photo)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (getContext() != null && Utils.getLogin(getContext()) != null) {
|
||||
viewModel.update(Utils.getLogin(getContext()));
|
||||
(binding.refresh as SwipeRefreshLayout).setOnRefreshListener {
|
||||
viewModel.onRefresh()
|
||||
}
|
||||
|
||||
binding.logout.setOnClickListener {
|
||||
viewModel.onLogout()
|
||||
findNavController().navigate(R.id.action_userFragment_to_loginFragment)
|
||||
}
|
||||
}
|
||||
|
||||
binding.refresh.setOnClickListener(v -> {
|
||||
if (getContext() != null && Utils.getLogin(getContext()) != null) {
|
||||
viewModel.update(Utils.getLogin(getContext()));
|
||||
}
|
||||
});
|
||||
|
||||
binding.scan.setOnClickListener(v -> {
|
||||
if (getView() != null)
|
||||
Navigation.findNavController(getView()).navigate(
|
||||
R.id.action_userFragment_to_qrScanFragment);
|
||||
});
|
||||
|
||||
binding.logout.setOnClickListener(v -> {
|
||||
if (getContext() != null) {
|
||||
Utils.deleteLogin(getContext());
|
||||
}
|
||||
if (getView() != null) {
|
||||
Navigation.findNavController(getView()).navigate(
|
||||
R.id.action_userFragment_to_loginFragment);
|
||||
}
|
||||
});
|
||||
|
||||
getParentFragmentManager().setFragmentResultListener(QrScanDestination.REQUEST_KEY, this, new FragmentResultListener() {
|
||||
@Override
|
||||
public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle result) {
|
||||
getParentFragmentManager().setFragmentResult(RESPONSE_KEY, result);
|
||||
if (getView() != null)
|
||||
Navigation.findNavController(getView()).navigate(
|
||||
R.id.action_userFragment_to_qrResultFragment);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
binding = null;
|
||||
super.onDestroyView();
|
||||
override fun onDestroy() {
|
||||
_binding = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,60 +1,65 @@
|
||||
package ru.myitschool.work.ui.profile;
|
||||
package ru.myitschool.work.ui.profile
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.CreationExtras
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.myitschool.work.data.UserRepositoryImpl
|
||||
import ru.myitschool.work.data.local.CredentialsLocalDataSource
|
||||
import ru.myitschool.work.data.local.UserLocalDataSource
|
||||
import ru.myitschool.work.data.network.UserNetworkDataSource
|
||||
import ru.myitschool.work.domain.entities.UserEntity
|
||||
import ru.myitschool.work.domain.login.LogoutUseCase
|
||||
import ru.myitschool.work.domain.user.GetCurrentUserUseCase
|
||||
|
||||
import ru.myitschool.work.domain.user.GetUserByLoginUseCase;
|
||||
class UserViewModel(
|
||||
private val getCurrentUserUseCase: GetCurrentUserUseCase,
|
||||
private val logoutUseCase: LogoutUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
public class UserViewModel extends ViewModel {
|
||||
private val _state = MutableStateFlow<State>(State.Loading)
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
private final MutableLiveData<State> mutableStateLiveData = new MutableLiveData<>();
|
||||
public final LiveData<State> stateLiveData = mutableStateLiveData;
|
||||
|
||||
public final GetUserByLoginUseCase getUserByLoginUseCase = new GetUserByLoginUseCase(
|
||||
UserRepositoryImplementation.getInstance()
|
||||
);
|
||||
|
||||
public void update(@NonNull String login) {
|
||||
mutableStateLiveData.setValue(new State(null, null, true));
|
||||
getUserByLoginUseCase.execute(login, status -> {
|
||||
mutableStateLiveData.postValue(new State(
|
||||
status.getErrors() != null ? status.getErrors().getLocalizedMessage() : null,
|
||||
status.getValue(),
|
||||
false
|
||||
));
|
||||
});
|
||||
init {
|
||||
updateState()
|
||||
}
|
||||
|
||||
public class State {
|
||||
@Nullable
|
||||
private final String errorMessage;
|
||||
|
||||
@Nullable
|
||||
private final UserEntity item;
|
||||
|
||||
private final boolean isLoading;
|
||||
|
||||
@Nullable
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public UserEntity getItem() {
|
||||
return item;
|
||||
}
|
||||
|
||||
public boolean isLoading() {
|
||||
return isLoading;
|
||||
}
|
||||
|
||||
public State(@Nullable String errorMessage, @Nullable UserEntity item, boolean isLoading) {
|
||||
this.errorMessage = errorMessage;
|
||||
this.item = item;
|
||||
this.isLoading = isLoading;
|
||||
private fun updateState() {
|
||||
viewModelScope.launch {
|
||||
State.Show(getCurrentUserUseCase())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onRefresh() {
|
||||
updateState()
|
||||
}
|
||||
|
||||
fun onLogout() {
|
||||
viewModelScope.launch { logoutUseCase() }
|
||||
}
|
||||
|
||||
sealed interface State {
|
||||
data object Loading : State
|
||||
data class Show(val userEntity: UserEntity) : State
|
||||
}
|
||||
|
||||
companion object {
|
||||
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
|
||||
val repository = UserRepositoryImpl(
|
||||
credentialsLocalDataSource = CredentialsLocalDataSource.getInstance(),
|
||||
userLocalDataSource = UserLocalDataSource,
|
||||
networkDataSource = UserNetworkDataSource
|
||||
)
|
||||
return UserViewModel(
|
||||
getCurrentUserUseCase = GetCurrentUserUseCase(repository = repository),
|
||||
logoutUseCase = LogoutUseCase(repository = repository)
|
||||
) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,79 +1,78 @@
|
||||
package ru.myitschool.work.ui.qr.result;
|
||||
package ru.myitschool.work.ui.qr.result
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import ru.myitschool.work.R
|
||||
import ru.myitschool.work.databinding.FragmentQrResultBinding
|
||||
import ru.myitschool.work.ui.qr.scan.QrScanDestination
|
||||
import ru.myitschool.work.ui.qr.scan.QrScanDestination.getDataIfExist
|
||||
import ru.myitschool.work.utils.collectWithLifecycle
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentResultListener;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import ru.myitschool.work.R;
|
||||
import ru.myitschool.work.databinding.FragmentQrResultBinding;
|
||||
import ru.myitschool.work.ui.qr.scan.QrScanDestination;
|
||||
import ru.myitschool.work.utils.Utils;
|
||||
const val RESPONSE_KEY: String = "response_qr"
|
||||
|
||||
public class QrResultFragment extends Fragment {
|
||||
class QrResultFragment : Fragment(R.layout.fragment_qr_result) {
|
||||
|
||||
public static final String RESPONSE_KEY = "response_qr";
|
||||
private FragmentQrResultBinding binding;
|
||||
private String resultQr;
|
||||
private QrResultViewModel viewModel;
|
||||
private var _binding: FragmentQrResultBinding? = null
|
||||
private val binding: FragmentQrResultBinding get() = _binding!!
|
||||
|
||||
public QrResultFragment() {
|
||||
super(R.layout.fragment_qr_result);
|
||||
}
|
||||
private var _resultQr: String? = null
|
||||
private val resultQr: String = _resultQr!!
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
if (savedInstanceState != null)
|
||||
resultQr = savedInstanceState.getString(QrScanDestination.REQUEST_KEY);
|
||||
binding = FragmentQrResultBinding.bind(view);
|
||||
viewModel = new ViewModelProvider(this).get(QrResultViewModel.class);
|
||||
private val viewModel by viewModels<QrResultViewModel> { QrResultViewModel.Factory }
|
||||
|
||||
getParentFragmentManager().setFragmentResultListener(RESPONSE_KEY, this, new FragmentResultListener() {
|
||||
@Override
|
||||
public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle result) {
|
||||
resultQr = QrScanDestination.INSTANCE.getDataIfExist(result);
|
||||
viewModel.update(Utils.getLogin(getContext()), resultQr);
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
_binding = FragmentQrResultBinding.bind(view)
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
_resultQr = savedInstanceState.getString(QrScanDestination.REQUEST_KEY)
|
||||
}
|
||||
|
||||
parentFragmentManager.setFragmentResultListener(RESPONSE_KEY, this) { _, result ->
|
||||
_resultQr = getDataIfExist(result)
|
||||
viewModel.update(resultQr)
|
||||
}
|
||||
|
||||
viewModel.state.collectWithLifecycle(this) { state ->
|
||||
if (_resultQr == null) {
|
||||
binding.result.setText(R.string.door_closed)
|
||||
binding.close.background =
|
||||
ContextCompat.getDrawable(requireContext(), R.drawable.warn_button)
|
||||
}
|
||||
});
|
||||
|
||||
viewModel.stateLiveData.observe(getViewLifecycleOwner(), state -> {
|
||||
if (resultQr == null) {
|
||||
binding.result.setText(R.string.door_closed);
|
||||
binding.close.setBackground(ResourcesCompat.getDrawable(getResources(), R.drawable.warn_button, getContext().getTheme()));
|
||||
} else if (state.isOpened()) {
|
||||
binding.result.setText(R.string.door_opened);
|
||||
binding.close.setBackground(ResourcesCompat.getDrawable(getResources(), R.drawable.main_button, getContext().getTheme()));
|
||||
} else {
|
||||
binding.result.setText(R.string.error);
|
||||
binding.close.setBackground(ResourcesCompat.getDrawable(getResources(), R.drawable.warn_button, getContext().getTheme()));
|
||||
when (state) {
|
||||
is QrResultViewModel.State.Error -> {
|
||||
binding.result.text = state.message
|
||||
binding.close.background =
|
||||
ContextCompat.getDrawable(requireContext(), R.drawable.warn_button)
|
||||
}
|
||||
|
||||
is QrResultViewModel.State.Loading -> Unit
|
||||
is QrResultViewModel.State.Show -> {
|
||||
binding.result.setText(R.string.door_opened)
|
||||
binding.close.background =
|
||||
ContextCompat.getDrawable(requireContext(), R.drawable.main_button)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
binding.close.setOnClickListener(v -> {
|
||||
if (getView() != null) {
|
||||
Navigation.findNavController(getView()).navigate(
|
||||
R.id.action_qrResultFragment_to_userFragment);
|
||||
}
|
||||
});
|
||||
binding.close.setOnClickListener {
|
||||
findNavController().navigate(R.id.action_qrResultFragment_to_userFragment)
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putString(QrScanDestination.REQUEST_KEY, resultQr);
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putString(QrScanDestination.REQUEST_KEY, resultQr)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
binding = null;
|
||||
super.onDestroy();
|
||||
override fun onDestroy() {
|
||||
_binding = null
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,52 +1,53 @@
|
||||
package ru.myitschool.work.ui.qr.result;
|
||||
package ru.myitschool.work.ui.qr.result
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.CreationExtras
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.myitschool.work.data.QrNetworkDataSource
|
||||
import ru.myitschool.work.data.QrRepositoryImpl
|
||||
import ru.myitschool.work.data.local.CredentialsLocalDataSource
|
||||
import ru.myitschool.work.domain.entities.QrEntity
|
||||
import ru.myitschool.work.domain.qr.PushQrUseCase
|
||||
|
||||
import ru.myitschool.work.domain.entities.Status;
|
||||
import ru.myitschool.work.domain.qr.PushQrUseCase;
|
||||
class QrResultViewModel(
|
||||
private val pushQrUseCase: PushQrUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
public class QrResultViewModel extends ViewModel {
|
||||
private final MutableLiveData<State> mutableStateLiveData = new MutableLiveData<State>();
|
||||
public final LiveData<State> stateLiveData = mutableStateLiveData;
|
||||
private val _state = MutableStateFlow<State>(State.Loading)
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
public final PushQrUseCase pushQrUseCase = new PushQrUseCase(
|
||||
QrRepositoryImplementation.getInstance()
|
||||
);
|
||||
|
||||
public void update(@NonNull String login, @NonNull String qr) {
|
||||
pushQrUseCase.execute(new QrEntity(login, qr), status -> mutableStateLiveData.postValue(fromStatus(status)));
|
||||
}
|
||||
|
||||
private State fromStatus(Status<Boolean> status) {
|
||||
return new State(
|
||||
status.getErrors() != null ? status.getErrors().getLocalizedMessage(): null,
|
||||
status.getValue() != null ? status.getValue() : false
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public class State {
|
||||
|
||||
@Nullable
|
||||
private final String errorMessage;
|
||||
private final boolean isOpened;
|
||||
|
||||
@Nullable
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public boolean isOpened() {
|
||||
return isOpened;
|
||||
}
|
||||
|
||||
public State(@Nullable String errorMessage, boolean isOpened) {
|
||||
this.errorMessage = errorMessage;
|
||||
this.isOpened = isOpened;
|
||||
fun update(qrValue: String) {
|
||||
viewModelScope.launch {
|
||||
pushQrUseCase(QrEntity(code = qrValue)).fold(
|
||||
onSuccess = { _state.emit(State.Show) },
|
||||
onFailure = { _state.emit(State.Error(it.message.toString())) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface State {
|
||||
data object Loading : State
|
||||
data object Show : State
|
||||
data class Error(val message: String) : State
|
||||
}
|
||||
|
||||
companion object {
|
||||
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
|
||||
return QrResultViewModel(
|
||||
pushQrUseCase = PushQrUseCase(
|
||||
repository = QrRepositoryImpl(
|
||||
networkDataSource = QrNetworkDataSource,
|
||||
credentialsLocalDataSource = CredentialsLocalDataSource.getInstance()
|
||||
)
|
||||
)
|
||||
) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -20,7 +20,7 @@ import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
import ru.myitschool.work.R
|
||||
import ru.myitschool.work.databinding.FragmentQrScanBinding
|
||||
import ru.myitschool.work.utils.collectWhenStarted
|
||||
import ru.myitschool.work.utils.collectWithLifecycle
|
||||
import ru.myitschool.work.utils.visibleOrGone
|
||||
|
||||
// НЕ ИЗМЕНЯЙТЕ ЭТОТ ФАЙЛ. В ТЕСТАХ ОН БУДЕМ ВОЗВРАЩЁН В ИСХОДНОЕ СОСТОЯНИЕ
|
||||
@ -49,7 +49,7 @@ class QrScanFragment : Fragment(R.layout.fragment_qr_scan) {
|
||||
}
|
||||
|
||||
private fun subscribe() {
|
||||
viewModel.state.collectWhenStarted(this) { state ->
|
||||
viewModel.state.collectWithLifecycle(this) { state ->
|
||||
binding.loading.visibleOrGone(state is QrScanViewModel.State.Loading)
|
||||
binding.viewFinder.visibleOrGone(state is QrScanViewModel.State.Scan)
|
||||
if (!isCameraInit && state is QrScanViewModel.State.Scan) {
|
||||
@ -58,7 +58,7 @@ class QrScanFragment : Fragment(R.layout.fragment_qr_scan) {
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.action.collectWhenStarted(this) { action ->
|
||||
viewModel.action.collectWithLifecycle(this) { action ->
|
||||
when (action) {
|
||||
is QrScanViewModel.Action.RequestPermission -> requestPermission(action.permission)
|
||||
is QrScanViewModel.Action.CloseWithCancel -> {
|
||||
|
@ -13,14 +13,14 @@ import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.myitschool.work.utils.MutablePublishFlow
|
||||
import ru.myitschool.work.utils.mutablePublishFlow
|
||||
|
||||
// НЕ ИЗМЕНЯЙТЕ ЭТОТ ФАЙЛ. В ТЕСТАХ ОН БУДЕМ ВОЗВРАЩЁН В ИСХОДНОЕ СОСТОЯНИЕ
|
||||
class QrScanViewModel(
|
||||
application: Application
|
||||
) : AndroidViewModel(application) {
|
||||
|
||||
private val _action = MutablePublishFlow<Action>()
|
||||
private val _action = mutablePublishFlow<Action>()
|
||||
val action = _action.asSharedFlow()
|
||||
|
||||
private val _state = MutableStateFlow<State>(initialState)
|
||||
|
@ -1,50 +0,0 @@
|
||||
package ru.myitschool.work.utils;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import ru.myitschool.work.domain.entities.Status;
|
||||
|
||||
public class CallToConsumer<SOURCE, DEST> implements Callback<SOURCE> {
|
||||
@NonNull
|
||||
private final Consumer<Status<DEST>> callback;
|
||||
@NonNull
|
||||
private final Mapper<SOURCE, DEST> mapper;
|
||||
|
||||
public CallToConsumer(@NonNull Consumer<Status<DEST>> callback,
|
||||
@NonNull Mapper<SOURCE, DEST> mapper) {
|
||||
this.callback = callback;
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<SOURCE> call, Response<SOURCE> response) {
|
||||
callback.accept(
|
||||
new Status<>(
|
||||
response.code(),
|
||||
mapper.map(response.body()),
|
||||
null
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<SOURCE> call, @NonNull Throwable t) {
|
||||
callback.accept(
|
||||
new Status<>(
|
||||
-1,
|
||||
null,
|
||||
t
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public interface Mapper<SOURCE, DEST> {
|
||||
DEST map(SOURCE source);
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ package ru.myitschool.work.utils
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
||||
fun <T> MutablePublishFlow() = MutableSharedFlow<T>(
|
||||
fun <T> mutablePublishFlow() = MutableSharedFlow<T>(
|
||||
replay = 0,
|
||||
extraBufferCapacity = 1,
|
||||
BufferOverflow.DROP_OLDEST
|
||||
|
@ -6,13 +6,13 @@ import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
inline fun <T> Flow<T>.collectWhenStarted(
|
||||
inline fun <T> Flow<T>.collectWithLifecycle(
|
||||
fragment: Fragment,
|
||||
crossinline collector: (T) -> Unit
|
||||
) {
|
||||
fragment.viewLifecycleOwner.lifecycleScope.launch {
|
||||
flowWithLifecycle(fragment.viewLifecycleOwner.lifecycle).collect { value ->
|
||||
collector.invoke(value)
|
||||
collector(value)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package ru.myitschool.work.utils;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
|
||||
public class OnChangeText implements TextWatcher {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package ru.myitschool.work.utils
|
||||
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
|
||||
open class TextChangedListener: TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
|
||||
|
||||
override fun afterTextChanged(s: Editable?) = Unit
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package ru.myitschool.work.utils;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
|
||||
public class Utils {
|
||||
|
||||
public static int visibleOrGone(boolean isVisible) {
|
||||
return isVisible ? VISIBLE : GONE;
|
||||
}
|
||||
public static void saveLogin(String login, Context context) {
|
||||
SharedPreferences sharedPreferences = context.getSharedPreferences("login", Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = sharedPreferences.edit();
|
||||
editor.putString("login", login);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
public static void deleteLogin(Context context) {
|
||||
SharedPreferences sharedPreferences = context.getSharedPreferences("login", Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = sharedPreferences.edit();
|
||||
editor.putString("login", null);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
public static String getLogin(Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(
|
||||
"login", Context.MODE_PRIVATE);
|
||||
return preferences.getString("login", null);
|
||||
}
|
||||
}
|
@ -1,112 +1,111 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_marginTop="131dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/refresh"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/photo"
|
||||
tools:src="@mipmap/ic_launcher"
|
||||
android:layout_width="130dp"
|
||||
android:layout_height="130dp" />
|
||||
|
||||
<TextView
|
||||
android:textAlignment="center"
|
||||
android:id="@+id/fullname"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:fontFamily="@font/manrope_bold"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:maxWidth="280dp"
|
||||
tools:text="Алексеев Виталий Александрович" />
|
||||
|
||||
<TextView
|
||||
android:layout_marginTop="4dp"
|
||||
android:textColor="@color/main_button_color"
|
||||
android:id="@+id/position"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Начальник" />
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_marginTop="130dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_marginTop="4dp"
|
||||
android:id="@+id/content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal">
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/last_visit" />
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:fontFamily="@font/manrope_light"
|
||||
android:id="@+id/lastEntry"
|
||||
android:layout_width="wrap_content"
|
||||
<ImageView
|
||||
android:id="@+id/photo"
|
||||
android:layout_width="130dp"
|
||||
android:layout_height="130dp"
|
||||
tools:src="@mipmap/ic_launcher" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/fullname"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:fontFamily="@font/manrope_bold"
|
||||
android:maxWidth="280dp"
|
||||
android:textAlignment="center"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="Алексеев Виталий Александрович" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/position"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textColor="@color/main_button_color"
|
||||
tools:text="Начальник" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:text="@string/last_visit" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/lastEntry"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/manrope_light"
|
||||
tools:text="19.09.24:10:00:01" />
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/scan"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:background="@drawable/main_button"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:text="@string/scan_qr_text"
|
||||
android:textColor="@color/white" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/error"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textColor="@color/warn_button_color"
|
||||
tools:text="Some error" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="19.09.24:10:00:01" />
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_marginTop="60dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<Button
|
||||
android:id="@+id/logout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/warn_button_outline"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:text="@string/logout_text"
|
||||
android:textColor="@color/warn_button_color" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:background="@drawable/main_button"
|
||||
android:textColor="@color/white"
|
||||
android:layout_marginTop="24dp"
|
||||
android:id="@+id/scan"
|
||||
android:text="@string/scan_qr_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:layout_marginTop="8dp"
|
||||
android:textColor="@color/warn_button_color"
|
||||
android:id="@+id/error"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Some error"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_marginBottom="37dp"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<Button
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:textColor="@color/warn_button_color"
|
||||
android:id="@+id/logout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/logout_text"
|
||||
android:background="@drawable/warn_button_outline"/>
|
||||
|
||||
<Button
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:textColor="@color/main_button_color"
|
||||
android:layout_marginTop="10dp"
|
||||
android:id="@+id/refresh"
|
||||
android:text="@string/refresh_data"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/main_button_outline"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -3,7 +3,7 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/nav_graph"
|
||||
app:startDestination="@id/loginFragment">
|
||||
app:startDestination="@id/splashFragment">
|
||||
|
||||
|
||||
<fragment
|
||||
@ -24,19 +24,19 @@
|
||||
app:destination="@id/qrScanFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
tools:layout="@layout/fragment_qr_scan"
|
||||
android:id="@+id/qrScanFragment"
|
||||
android:name="ru.myitschool.work.ui.qr.scan.QrScanFragment"
|
||||
android:label="QrScanFragment" >
|
||||
android:label="QrScanFragment"
|
||||
tools:layout="@layout/fragment_qr_scan">
|
||||
<action
|
||||
android:id="@+id/action_qrScanFragment_to_userFragment"
|
||||
app:destination="@id/userFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
tools:layout="@layout/fragment_login"
|
||||
android:id="@+id/loginFragment"
|
||||
android:name="ru.myitschool.work.ui.login.LoginFragment"
|
||||
android:label="LoginFragment" >
|
||||
android:label="LoginFragment"
|
||||
tools:layout="@layout/fragment_login">
|
||||
<action
|
||||
android:id="@+id/action_loginFragment_to_userFragment"
|
||||
app:destination="@id/userFragment"
|
||||
@ -44,14 +44,23 @@
|
||||
app:popUpToInclusive="true" />
|
||||
</fragment>
|
||||
<fragment
|
||||
tools:layout="@layout/fragment_qr_result"
|
||||
android:id="@+id/qrResultFragment"
|
||||
android:name="ru.myitschool.work.ui.qr.result.QrResultFragment"
|
||||
android:label="QrResultFragment" >
|
||||
android:label="QrResultFragment"
|
||||
tools:layout="@layout/fragment_qr_result">
|
||||
<action
|
||||
android:id="@+id/action_qrResultFragment_to_userFragment"
|
||||
app:destination="@id/userFragment"
|
||||
app:popUpTo="@id/nav_graph"
|
||||
app:popUpToInclusive="true" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/splashFragment"
|
||||
android:name="ru.myitschool.work.ui.login.SplashFragment"
|
||||
android:label="SplashFragment"
|
||||
tools:layout="@layout/fragment_splash">
|
||||
<action
|
||||
android:id="@+id/action_splashFragment_to_loginFragment"
|
||||
app:destination="@id/loginFragment" />
|
||||
</fragment>
|
||||
</navigation>
|
Loading…
x
Reference in New Issue
Block a user