I am developing another bigger project, I want to try a simple DatingApp and currently I try to develop a Login and Registration Form. There is no special register / login button, the code will later decide wether email is registered (-> login) or not (-> register). But even though it's working I am not sure if I make things to complex:
First I want so show a very simple overview of the logic I try to develop:
The App launches with the LogoActivity as a simple SplashScreen. In here the Activity decides which Activity and Fragment is about to be displayed next. It has a sharedElementTransition connected to the AuthOptionActivity.
The AuthOptionActivity only contains the Logo and a fragment Container.
Depending on the AuthStatus it will show the AuthOptionFragment, containing a Layout with two Buttons. Here you can decide which way you want to use for your registration.
In case there is for example an email-registered user but he is not verified yet the AuthOptionActivity will show the MailAuthFragment instead of the AuthOptionFragment.
The MailAuthOptionFragment itself contains another two ChildFragments.
The EnterMailSubFragment contains two EditText-Views and the ConfirmMailSubFragment is just a Text containing the Email Adress and a clickable resend textview (not implemented yet).
The code so far implemented is working but it is not as clear as I want it to be.
Logo Activity
public class LogoActivity extends AppCompatActivity {
private ImageView mLogo;
private Handler mHandler;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_logo);
this.init();
}
private void init(){
mLogo = findViewById(R.id.activity_logo_logo);
AuthHandler.performStartUpCheck(new AuthHandler.StartupAuthStatus() {
@Override
public void noUser() {
Toast.makeText(LogoActivity.this, "No User", Toast.LENGTH_SHORT).show();
performSharedElementTransactionToAuthOption();
}
@Override
public void onError(String e) {
Toast.makeText(LogoActivity.this, e, Toast.LENGTH_SHORT).show();
performSharedElementTransactionToAuthOption();
}
@Override
public void onPhone() {
Toast.makeText(LogoActivity.this, "Phone", Toast.LENGTH_SHORT).show();
//direct to loadingActivity
}
@Override
public void onEmail(boolean isVerified) {
Toast.makeText(LogoActivity.this, "Email" + String.valueOf(isVerified), Toast.LENGTH_SHORT).show();
if(isVerified){
//direct to LoadingActivity
}else{
performSharedElementTransactionToAuthOption();
}
}
});
//this.performSharedElementTransaction();
}
private void performSharedElementTransactionToAuthOption(){
//check for auth and proceed to Loading
Intent pIntent = new Intent(LogoActivity.this, AuthOptionActivity.class);
ActivityOptions mOptions = ActivityOptions.makeSceneTransitionAnimation(LogoActivity.this, mLogo, mLogo.getTransitionName());
startActivity(pIntent, mOptions.toBundle());
}
private void performTransactionToLoadingScreen(){
/**dummy**/
}
@Override
protected void onStop() {
//Call finish here to avoid flickering
this.finish();
super.onStop();
}
}
Nothing so special here, I am quite happy with that class. Disregard some comments, they are just for me as a reminder.
AuthHandler
public class AuthHandler {
public interface StartupAuthStatus{
void noUser();
void onError(String e);
void onPhone();
void onEmail(boolean isVerified);
}
public interface SignInStatus{
void onEmail(boolean isVerified);
}
private FirebaseAuth mAuth;
private FirebaseFirestore mFirestore;
/**STARTUP PROCEDURE**/
public static void performStartUpCheck(final StartupAuthStatus mCallback){
final FirebaseAuth mAuth = FirebaseAuth.getInstance();
checkStartupAuthStatus(mAuth, mCallback);
}
private static void checkStartupAuthStatus(FirebaseAuth mAuth, StartupAuthStatus mCallback){
mAuth = FirebaseAuth.getInstance();
checkForCurrentUser(mAuth,mCallback);
}
private static void checkForCurrentUser(FirebaseAuth mAuth, StartupAuthStatus mCallback){
if(mAuth.getCurrentUser()!=null){
checkRegistrationMethod(mAuth, mCallback);
}else{
mCallback.noUser();
}
}
private static void checkRegistrationMethod(final FirebaseAuth mAuth, final StartupAuthStatus mCallback){
if(mAuth.getUid() == null){
mCallback.onError("NullPointerInUID");
return;
}
FirebaseFirestore.getInstance().collection("Registration").document(mAuth.getUid()).get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
@Override
public void onComplete(@NonNull Task<DocumentSnapshot> task) {
if(task.getResult() != null && task.getResult().exists()){
Map<String, Object> mData = task.getResult().getData();
String mRegistrationForm = (String) mData.get("reg");
if(mRegistrationForm.equals("mail")){
checkMailVerificationStatus(mAuth, mCallback);
}else if(mRegistrationForm.equals("phone")){
mCallback.onPhone();
}
}
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
mCallback.onError(e.getMessage());
mAuth.signOut();
}
});
}
private static void checkMailVerificationStatus(FirebaseAuth mAuth, StartupAuthStatus mCallback){
if(mAuth.getCurrentUser().isEmailVerified()){
mCallback.onEmail(true);
}else{
mCallback.onEmail(false);
}
}
/**LOGIN PROCEDURE**/
public static void enterUserWithEmailAndPassword(String mEmail, String mPassword){
FirebaseAuth mAuth = FirebaseAuth.getInstance();
createUserWithEmailAndPassword(mAuth, mEmail, mPassword);
}
private static void createUserWithEmailAndPassword(FirebaseAuth mAuth, String mEmail, String mPassword){
}
private static void signUserWithEmailAndPassword(FirebaseAuth mAuth, String mEmail, String mPassword){
}
private static void sendVerificationEmail(FirebaseAuth mAuth){
}
private static void setupRegistrationModel(String mModelType){
}
/**DELETE PROCEDURE**/
}
This class is made to "outsource" the code mess. Here I am not sure if it's a common or even usefull way to handle this. The Database contains Users (with a lot of Data later) and a Pool of Registrations, here I only store the choosen registration form (field reg(String): "phone" or "mail").
I need to seperate it somehow as the email-registrated-user needs to verify the email.
AuthOptionActivity
public class AuthOptionActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_authoption);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
getWindow().setSharedElementEnterTransition(TransitionInflater.from(this).inflateTransition(android.R.transition.move));
}
this.checkAuthStatus();
}
@Override
public void onBackPressed() {
findViewById(R.id.activity_authoption_logo).setTransitionName(null);
super.onBackPressed();
/** Problem occured: SharedViewElement will be visible after App was closed.
* Reason: The View will try to perform the ExitTransition
* Solution 1: Delete super.onBackPressed(); and override with finish();
* Solution 2: Set Transitionname to Null**/
}
/**The SlideEnter Transition is variable, on First Startup we want a smooth slide in,
* for phoneauth we want a Left-Side Slide and for Email a Right-Side Slide.
* The Exit Animation is always slide-out-bottom-smooth.
* The Reenter Animation is always slide-in-bottom-smooth and due to overlapping
* the ExitTransition of the View before BackPress will be hidden and is always 0**/
private void performFragmentTransaction(Fragment mFragment, String mTag, Boolean mAddToBackStack, int mEnterTransition){
FragmentTransaction mFragmentTransaction = getSupportFragmentManager().beginTransaction();
mFragmentTransaction.setCustomAnimations(mEnterTransition, 0, R.anim.slide_in_bottom_smooth, 0);
if(mAddToBackStack){
mFragmentTransaction.addToBackStack(mTag);
}
mFragmentTransaction.replace(R.id.activity_authoption_container, mFragment, mTag);
mFragmentTransaction.commit();
}
/**Check Auth Status: if CurrentUser != null but not Email verified -> Show MailAuth**/
private void checkAuthStatus(){
if(FirebaseAuth.getInstance().getCurrentUser()!=null && !FirebaseAuth.getInstance().getCurrentUser().isEmailVerified()){
//very bad way to force a backstack, still looking for a solution
this.showAuthOptionFragment();
this.showMailAuth();
}else{
this.showAuthOptionFragment();
}
}
/**Handle Fragment Transaction including Listener**/
private void showAuthOptionFragment(){
this.performFragmentTransaction(AuthOptionFragment.newInstance(new AuthOptionFragment.ClickListener() {
@Override
public void onMail() {
showMailAuth();
}
@Override
public void onPhone() {
showPhoneAuth();
}
}), "AuthOption", false, R.anim.slide_in_bottom);
}
private void showPhoneAuth(){
this.performFragmentTransaction(_PhoneAuthFragment.newInstance(new _PhoneAuthFragment.AuthStatusListener() {
@Override
public void onCancel() {
//PhoneAuthFragment contains a "back" Button it will fire "onCancel()"
onBackPressed();
}
@Override
public void onSuccess() {
//show LoadingScreen
}
}), "PhoneAuth", true, R.anim.slide_in_left_smooth);
}
private void showMailAuth(){
this.performFragmentTransaction(_MailAuthFragment.newInstance(new _MailAuthFragment.AuthStatusListener() {
@Override
public void onCancel() {
//MailAuthFragment contains a "back" Button it will fire "onCancel()"
onBackPressed();
}
@Override
public void onSuccess() {
//show LoadingScreen
}
}), "MailAuth", true, R.anim.slide_in_right_smooth);
}
}
For this activity I have some questions:
- I am checking the Auth Status & email again to decide which fragment to show. It's easy and simple, but repetitive (it will occur again in MailAuthFragment). Is it a common way to repetitive use FirebaseAuth instances?
- I have seen a couple of fragment -> activity communication solutions using onAttach etc. Is my solution to pass a CustomListener(interface) in the newInstance method okay? Is there any bug that could occur using it that way?
I pretty much like it as it makes the handling easier.
MailAuthOptionFragment
public class _MailAuthFragment extends Fragment {
public interface AuthStatusListener{
void onCancel();
void onSuccess();
}
private AuthStatusListener mCallback;
private String mActiveWindow;
private CheckCancelButton mCheckCancelButton;
private String mEmail;
private String mPassword;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View mView = inflater.inflate(R.layout.fragment_mailauth, container, false);
this.checkAuthStatus();
this.initButton(mView);
return mView;
}
/**PUBLIC METHODS**/
/**Set the Fragment Listener for AuthOptionActivity**/
public void setListener(AuthStatusListener mCallback){
this.mCallback = mCallback;
}
/**Replaces hard coded Instances as Listener will be passed directly**/
public static _MailAuthFragment newInstance(AuthStatusListener mCallback){
_MailAuthFragment mFragment = new _MailAuthFragment();
mFragment.setListener(mCallback);
return mFragment;
}
/**PRIVATE METHODS**/
/**Check AuthStatus, if CurrentUser!=null but not verified -> Show VerificationScreen**/
private void checkAuthStatus(){
if(FirebaseAuth.getInstance().getCurrentUser() != null && !FirebaseAuth.getInstance().getCurrentUser().isEmailVerified()){
this.showConfirmEmailSubFragment(R.anim.slide_in_right_smooth);
}else{
this.showEnterEmailSubFragment(R.anim.slide_in_right_smooth);
}
}
/**Handle Registration Process Fragments**/
private void performFragmentTransaction(Fragment mFragment, String mTag, int mEnterAnim){
mActiveWindow = mTag;
FragmentTransaction mFragmentTransaction = getChildFragmentManager().beginTransaction();
mFragmentTransaction.setCustomAnimations(mEnterAnim, 0);
mFragmentTransaction.replace(R.id.fragment_mailauth_container, mFragment, mTag);
mFragmentTransaction.commit();
}
/**MailAuth receives any valid TextChange from SubFragment**/
private void showEnterEmailSubFragment(int mEnterAnim){
this.performFragmentTransaction(EnterMailSubFragment.newInstance(new EnterMailSubFragment.EnterMailListener() {
@Override
public void onEmailChanged(String mEmail) {
_MailAuthFragment.this.mEmail = mEmail;
}
@Override
public void onPasswordChanged(String mPassword) {
_MailAuthFragment.this.mPassword = mPassword;
}
}), "EnterMail", mEnterAnim);
}
/**MailAuth receives resend VerificationEmailRequest from SubFragment**/
private void showConfirmEmailSubFragment(int mEnterAnim){
this.performFragmentTransaction(new ConfirmMailSubFragment(), "ConfirmMail", R.anim.slide_in_right_smooth);
}
private void initButton(final View mView){
/**Custom Button using AfterEffects, Bodymovin and LottieView, contains some commands like "transform to tick"**/
mCheckCancelButton = new CheckCancelButton((LottieAnimationView)mView.findViewById(R.id.fragment_mailauth_nextBtn));
mCheckCancelButton.setCustomClickListener(new CheckCancelButton.CheckCancelButtonClickListener() {
@Override
public void onClick() {
switch(mActiveWindow){
case "EnterMail":
// the received Email and Password will be passed to AuthHandlerClass - not implemented yet
break;
case "ConfirmMail":
// the AuthHandler will reloard the currentUser and check for email verification Status - not implemented yet
break;
}
}
});
}
}
Checking AuthStatus again and choosing whether to show the EnterEmailSubFragment or the ConfirmEmailSubFragment. This class contains a button handling both SubFragment pages. This leads to that switch-case method. Not sure if that is a proper way.
EnterMailSubFragment
public class EnterMailSubFragment extends Fragment {
public interface EnterMailListener{
void onEmailChanged(String mEmail);
void onPasswordChanged(String mPassword);
}
private EnterMailListener mCallback;
private EditText mEmail, mPassword;
private ImageView mEmailWarning, mPasswordWarning;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View mView = inflater.inflate(R.layout.subfragment_mailauth, container, false);
this.initUiWidgets(mView);
this.initEditText(mView);
return mView;
}
public void setEnterMailListener(EnterMailListener mMailListener){
this.mCallback = mMailListener;
}
public static EnterMailSubFragment newInstance(EnterMailListener mMailListener){
EnterMailSubFragment mFragment = new EnterMailSubFragment();
mFragment.setEnterMailListener(mMailListener);
return mFragment;
}
private void initUiWidgets(View mView){
mEmail = mView.findViewById(R.id.subfragment_mailauth_email_ET);
mPassword = mView.findViewById(R.id.subfragment_mailauth_password_et);
mEmailWarning = mView.findViewById(R.id.subfragment_mailauth_email_warning);
mPasswordWarning = mView.findViewById(R.id.subfragment_mailauth_password_warning);
}
/**Any Valid Entry will be passed to MailFragment**/
private void initEditText(final View mView){
mEmail.addTextChangedListener(new 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) {
if(mEmail.getText() != null && Patterns.EMAIL_ADDRESS.matcher(mEmail.getText().toString()).matches()){
mEmailWarning.setVisibility(View.GONE);
mCallback.onEmailChanged(mEmail.getText().toString());
}else{
mEmailWarning.setVisibility(View.VISIBLE);
}
}
});
mPassword.addTextChangedListener(new 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) {
if(mPassword.getText() != null && mPassword.getText().length() >= 6){
mPasswordWarning.setVisibility(View.GONE);
mCallback.onPasswordChanged(mPassword.getText().toString());
}else{
mPasswordWarning.setVisibility(View.VISIBLE);
}
}
});
}
}
The Textwatcher checks every entry and transfers it to the MailAuthFragment as MailAuthFragment handels the button event (this is for design puposes, I want the content to move and the button is meant to stay in its position).
ConfirmMailAuthSubFragment
public class ConfirmMailSubFragment extends Fragment {
public interface ConfirmMailListener{
void onResendEmail();
}
private ConfirmMailListener mCallback;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View mView = inflater.inflate(R.layout.subfragment_confirmmail, container, false);
return mView;
}
public void setConfirmMailListener(ConfirmMailListener mCallback){
this.mCallback = mCallback;
}
public static ConfirmMailSubFragment newInstance(ConfirmMailListener mCallback){
ConfirmMailSubFragment mFragment = new ConfirmMailSubFragment();
mFragment.setConfirmMailListener(mCallback);
return mFragment;
}
}
Nothing special here yet. It will handle the Button Event "Resend Verification Mail" and transfer it to MailAuthFragment.
I know it's a very early stage of my code but as beautiful as it looks with that slide in animations I am not sure if I am running in the wrong direction or making stuff unnecessary complex.
Update
I decided to handle the onBackPress manually (no more backstack) as the ChildFragmentManager does not allow to get a step back using the backpress button. Every fragment and childfragment now has a public method "onBackPress" called from AuthOptionActivity.
AuthOptionActivity - onBackPressed()
@Override
public void onBackPressed() {
/**Checking for Fragments and then checking of which instance so you can acess methods**/
if(getSupportFragmentManager().findFragmentById(R.id.activity_authoption_container) != null)
{
Fragment mCurrent = getSupportFragmentManager().findFragmentById(R.id.activity_authoption_container);
if(mCurrent instanceof AuthOptionFragment){
this.finish();
}
if(mCurrent instanceof _MailAuthFragment){
((_MailAuthFragment) mCurrent).onBackPress();
}
if(mCurrent instanceof _PhoneAuthFragment){
((_PhoneAuthFragment) mCurrent).onBackPress();
}
}
findViewById(R.id.activity_authoption_logo).setTransitionName(null);
//super.onBackPressed();
}