Login done (75%)

This commit is contained in:
A1pha 2024-11-21 20:53:33 +03:00
parent 1194f4d2ea
commit 4fe6238503
22 changed files with 308 additions and 110 deletions

View File

@ -5,16 +5,19 @@ import androidx.annotation.NonNull;
import java.util.function.Consumer; import java.util.function.Consumer;
import ru.myitschool.work.data.network.RetrofitFactory; import ru.myitschool.work.data.network.RetrofitFactory;
import ru.myitschool.work.data.source.Credentials;
import ru.myitschool.work.data.source.UserApi; import ru.myitschool.work.data.source.UserApi;
import ru.myitschool.work.domain.entities.Status; import ru.myitschool.work.domain.entities.Status;
import ru.myitschool.work.domain.entities.UserEntity; import ru.myitschool.work.domain.entities.UserEntity;
import ru.myitschool.work.domain.login.LoginRepository;
import ru.myitschool.work.domain.user.UserRepository; import ru.myitschool.work.domain.user.UserRepository;
import ru.myitschool.work.utils.CallToConsumer; import ru.myitschool.work.utils.CallToConsumer;
public class UserRepositoryImplementation implements UserRepository { public class UserRepositoryImplementation implements UserRepository, LoginRepository {
private static UserRepositoryImplementation INSTANCE; private static UserRepositoryImplementation INSTANCE;
private final UserApi userApi = RetrofitFactory.getInstance().getUserApi(); private final UserApi userApi = RetrofitFactory.getInstance().getUserApi();
private final Credentials credentials = Credentials.getInstance();
private UserRepositoryImplementation() {} private UserRepositoryImplementation() {}
@ -25,16 +28,16 @@ public class UserRepositoryImplementation implements UserRepository {
return INSTANCE; return INSTANCE;
} }
@Override @Override
public void getUserById(@NonNull String id, @NonNull Consumer<Status<UserEntity>> callback) { public void getUserByLogin(@NonNull String login, @NonNull Consumer<Status<UserEntity>> callback) {
userApi.getById(id).enqueue(new CallToConsumer<>( userApi.getByLogin(login).enqueue(new CallToConsumer<>(
callback, callback,
userDto -> { userDto -> {
final String resultId = userDto.id; final String resultLogin = userDto.login;
final String name = userDto.name; final String name = userDto.name;
if (resultId != null && name != null) { if (resultLogin != null && name != null) {
return new UserEntity( return new UserEntity(
resultId, resultLogin,
name, name,
userDto.lastVisit, userDto.lastVisit,
userDto.photoUrl, userDto.photoUrl,
@ -46,4 +49,17 @@ public class UserRepositoryImplementation implements UserRepository {
} }
)); ));
} }
@Override
public void isUserExist(@NonNull String login, Consumer<Status<Void>> callback) {
userApi.isExist(login).enqueue(new CallToConsumer<>(
callback,
dto -> null
));
}
@Override
public void logoutUser() {
credentials.setAuthData(null);
}
} }

View File

@ -10,16 +10,16 @@ public class UserDto {
@Nullable @Nullable
@SerializedName("name") @SerializedName("name")
public String name; public String name;
@Nullable
@SerializedName("lastVisit") @SerializedName("lastVisit")
@Nullable
public String lastVisit; public String lastVisit;
@Nullable
@SerializedName("photoUrl") @SerializedName("photoUrl")
@Nullable
public String photoUrl; public String photoUrl;
@Nullable
@SerializedName("position") @SerializedName("position")
@Nullable
public String position; public String position;
@SerializedName("id")
@Nullable @Nullable
public String id; @SerializedName("login")
public String login;
} }

View File

@ -1,5 +1,7 @@
package ru.myitschool.work.data.network; package ru.myitschool.work.data.network;
import static ru.myitschool.work.core.Constants.SERVER_ADDRESS;
import retrofit2.Retrofit; import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory; import retrofit2.converter.gson.GsonConverterFactory;
import ru.myitschool.work.data.UserRepositoryImplementation; import ru.myitschool.work.data.UserRepositoryImplementation;
@ -16,8 +18,8 @@ public class RetrofitFactory {
return INSTANCE; return INSTANCE;
} }
private Retrofit retrofit = new Retrofit.Builder() private final Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://10.0.2.2:8080/") .baseUrl(SERVER_ADDRESS)
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.build(); .build();

View File

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

View File

@ -7,6 +7,8 @@ import ru.myitschool.work.data.dto.UserDto;
public interface UserApi { public interface UserApi {
@GET("user/{id}") @GET("user/{login}")
Call<UserDto> getById(@Path("id") String id); Call<UserDto> getByLogin(@Path("login") String login);
@GET("user/username/{username}")
Call<Void> isExist(@Path("username") String login);
} }

View File

@ -9,7 +9,7 @@ public class UserEntity {
@NonNull @NonNull
private final String name; private final String name;
@NonNull @NonNull
private final String id; private final String login;
@Nullable @Nullable
private final String last_visit; private final String last_visit;
@Nullable @Nullable
@ -19,7 +19,7 @@ public class UserEntity {
@NonNull @NonNull
public String getId() { public String getId() {
return id; return login;
} }
@Nullable @Nullable
@ -43,12 +43,12 @@ public class UserEntity {
} }
public UserEntity( public UserEntity(
@NonNull String id, @NonNull String login,
@NonNull String name, @NonNull String name,
@Nullable String last_visit, @Nullable String last_visit,
@Nullable String photoUrl, @Nullable String photoUrl,
@Nullable String position) { @Nullable String position) {
this.id = id; this.login = login;
this.name = name; this.name = name;
this.last_visit = last_visit; this.last_visit = last_visit;
this.photoUrl = photoUrl; this.photoUrl = photoUrl;

View File

@ -1,4 +0,0 @@
package ru.myitschool.work.domain.login;
public class CreateUserUseCase {
}

View File

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

View File

@ -1,4 +1,13 @@
package ru.myitschool.work.domain.login; package ru.myitschool.work.domain.login;
import androidx.annotation.NonNull;
import java.util.function.Consumer;
import ru.myitschool.work.domain.entities.Status;
public interface LoginRepository { public interface LoginRepository {
void isUserExist(@NonNull String login, Consumer<Status<Void>> callback);
void logoutUser();
} }

View File

@ -1,4 +0,0 @@
package ru.myitschool.work.domain.login;
public class LoginUserUseCase {
}

View File

@ -7,14 +7,14 @@ import java.util.function.Consumer;
import ru.myitschool.work.domain.entities.Status; import ru.myitschool.work.domain.entities.Status;
import ru.myitschool.work.domain.entities.UserEntity; import ru.myitschool.work.domain.entities.UserEntity;
public class GetUserByIdUseCase { public class GetUserByLoginUseCase {
private final UserRepository repository; private final UserRepository repository;
public GetUserByIdUseCase(UserRepository repository) { public GetUserByLoginUseCase(UserRepository repository) {
this.repository = repository; this.repository = repository;
} }
public void execute(@NonNull String id, @NonNull Consumer<Status<UserEntity>> callback) { public void execute(@NonNull String login, @NonNull Consumer<Status<UserEntity>> callback) {
repository.getUserById(id, callback); repository.getUserByLogin(login, callback);
} }
} }

View File

@ -10,5 +10,5 @@ import ru.myitschool.work.domain.entities.UserEntity;
public interface UserRepository { public interface UserRepository {
void getUserById(@NonNull String id, @NonNull Consumer<Status<UserEntity>> callback); void getUserByLogin(@NonNull String login, @NonNull Consumer<Status<UserEntity>> callback);
} }

View File

@ -1,19 +1,9 @@
package ru.myitschool.work.ui package ru.myitschool.work.ui
import android.os.Bundle import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.createGraph
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.fragment
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import ru.myitschool.work.R import ru.myitschool.work.R
import ru.myitschool.work.domain.user.GetUserByIdUseCase
import ru.myitschool.work.ui.login.LoginDestination
import ru.myitschool.work.ui.login.LoginFragment
import ru.myitschool.work.ui.qr.scan.QrScanDestination
import ru.myitschool.work.ui.qr.scan.QrScanFragment
// НЕ ИЗМЕНЯЙТЕ НАЗВАНИЕ КЛАССА! // НЕ ИЗМЕНЯЙТЕ НАЗВАНИЕ КЛАССА!
@AndroidEntryPoint @AndroidEntryPoint

View File

@ -1,6 +0,0 @@
package ru.myitschool.work.ui.login
import kotlinx.serialization.Serializable
@Serializable
data object LoginDestination

View File

@ -0,0 +1,63 @@
package ru.myitschool.work.ui.login;
import android.os.Bundle;
import android.text.Editable;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.Navigation;
import ru.myitschool.work.R;
import ru.myitschool.work.databinding.FragmentLoginBinding;
import ru.myitschool.work.utils.OnChangeText;
public class LoginFragment extends Fragment {
private FragmentLoginBinding binding;
private LoginViewModel viewModel;
public LoginFragment() {
super(R.layout.fragment_login);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
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());
}
});
binding.login.setOnClickListener(v -> viewModel.confirm());
subscribe(viewModel);
}
private void subscribe(LoginViewModel viewModel) {
viewModel.errorLiveData.observe(getViewLifecycleOwner(), error -> {
Toast.makeText(getContext(), error, Toast.LENGTH_SHORT).show();
});
viewModel.stateLiveData.observe(getViewLifecycleOwner(), state -> {
binding.login.setClickable(state.isButtonActive());
});
viewModel.openProfileLiveData.observe(getViewLifecycleOwner(), (unused) -> {
View view = getView();
if (view == null) return;
Navigation.findNavController(getView()).navigate(
R.id.action_loginFragment_to_userFragment);
});
}
@Override
public void onDestroyView() {
binding = null;
super.onDestroyView();
}
}

View File

@ -1,36 +0,0 @@
package ru.myitschool.work.ui.login
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import dagger.hilt.android.AndroidEntryPoint
import ru.myitschool.work.R
import ru.myitschool.work.databinding.FragmentLoginBinding
import ru.myitschool.work.utils.collectWhenStarted
import ru.myitschool.work.utils.visibleOrGone
@AndroidEntryPoint
class LoginFragment : Fragment(R.layout.fragment_login) {
private var _binding: FragmentLoginBinding? = null
private val binding: FragmentLoginBinding get() = _binding!!
private val viewModel: LoginViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentLoginBinding.bind(view)
subscribe()
}
private fun subscribe() {
viewModel.state.collectWhenStarted(this) { state ->
binding.loading.visibleOrGone(state)
}
}
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
}

View File

@ -0,0 +1,76 @@
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 ru.myitschool.work.data.UserRepositoryImplementation;
import ru.myitschool.work.domain.login.IsUserExistUseCase;
public class LoginViewModel extends ViewModel {
private final State INIT_STATE = new State(false);
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 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;
}
isUserExistUseCase.execute(currentLogin, status -> {
if (status.getValue() == null || status.getErrors() != null) {
mutableErrorLiveData.postValue("Something went wrong. Try again later");
return;
}
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,17 +0,0 @@
package ru.myitschool.work.ui.login
import android.content.Context
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
@HiltViewModel
class LoginViewModel @Inject constructor(
@ApplicationContext private val context: Context,
) : ViewModel() {
private val _state = MutableStateFlow(true)
val state = _state.asStateFlow()
}

View File

@ -9,14 +9,14 @@ import androidx.lifecycle.ViewModel;
import ru.myitschool.work.data.UserRepositoryImplementation; import ru.myitschool.work.data.UserRepositoryImplementation;
import ru.myitschool.work.domain.entities.Status; import ru.myitschool.work.domain.entities.Status;
import ru.myitschool.work.domain.entities.UserEntity; import ru.myitschool.work.domain.entities.UserEntity;
import ru.myitschool.work.domain.user.GetUserByIdUseCase; import ru.myitschool.work.domain.user.GetUserByLoginUseCase;
public class UserViewModel extends ViewModel { public class UserViewModel extends ViewModel {
private final MutableLiveData<State> mutableStateLiveData = new MutableLiveData<State>(); private final MutableLiveData<State> mutableStateLiveData = new MutableLiveData<State>();
public final LiveData<State> stateLiveData = mutableStateLiveData; public final LiveData<State> stateLiveData = mutableStateLiveData;
public final GetUserByIdUseCase getUserByIdUseCase = new GetUserByIdUseCase( public final GetUserByLoginUseCase getUserByLoginUseCase = new GetUserByLoginUseCase(
UserRepositoryImplementation.getInstance() UserRepositoryImplementation.getInstance()
); );
@ -28,7 +28,7 @@ public class UserViewModel extends ViewModel {
public void update(@NonNull String id) { public void update(@NonNull String id) {
mutableStateLiveData.setValue(new State(null, null, true)); mutableStateLiveData.setValue(new State(null, null, true));
getUserByIdUseCase.execute(id, status -> { getUserByLoginUseCase.execute(id, status -> {
mutableStateLiveData.postValue(fromStatus(status)); mutableStateLiveData.postValue(fromStatus(status));
}); });
} }

View File

@ -0,0 +1,21 @@
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,16 +1,44 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<ProgressBar <androidx.constraintlayout.helper.widget.Flow
android:id="@+id/loading" android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:flow_verticalStyle="packed"
app:flow_verticalGap="24dp"
app:layout_constraintBottom_toBottomOf="parent"
android:orientation="vertical"
app:constraint_referenced_ids="title, username, login" />
<TextView
android:textAppearance="@style/TextAppearance.AppCompat.Display1"
android:id="@+id/title"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent" android:text="@string/welcome_text" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" <EditText
app:layout_constraintBottom_toBottomOf="parent" /> android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="@string/login_hint" />
<Button
android:id="@+id/login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/login"
android:clickable="false" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -2,4 +2,7 @@
<string name="app_name">NTO Pass</string> <string name="app_name">NTO Pass</string>
<string name="logout_text">Выйти из аккаунта</string> <string name="logout_text">Выйти из аккаунта</string>
<string name="scan_qr_text">Сканировать QR-код</string> <string name="scan_qr_text">Сканировать QR-код</string>
<string name="login_hint">Введите логин</string>
<string name="welcome_text">Добро пожаловать</string>
<string name="login">Войти</string>
</resources> </resources>