feat: moved fully to Kotlin and ktor

This commit is contained in:
a1pha 2025-02-19 12:01:50 +03:00
parent f4e53d6e89
commit a15f1fb00e
30 changed files with 626 additions and 716 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
package ru.myitschool.work.domain.login
class LogoutUseCase(
private val repository: LoginRepository
) {
suspend operator fun invoke() = repository.logout()
}

View File

@ -0,0 +1,9 @@
package ru.myitschool.work.domain.user
class GetCurrentUserUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke() = repository.getCurrentUser()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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