Free tutorials & notes in Hindi & English · Clean code examples 🧰 Free PDF & Image Tools →
📱 Android App (Kotlin + XML)  ·  Lesson 16

Login & Register Screens

The two screens

9:41▮▮ 85%🛒Welcome back!Login to continue shopping✉ Email🔒 Password 👁LoginUse demo accountNew to KartEasy? Create an account9:41▮▮ 85%Create account👤 Full name✉ riya@xEnter a valid email address📞 +91 Mobile number (optional)🔒 Password🔒 Confirm passwordI agree to the Terms of Use✓Sign up
Login, and Register showing a field error under the email

Step 1: AuthActivity + nav_auth

Login and Register are two fragments inside a small AuthActivity with its own navigation graph. After login we open MainActivity with CLEAR_TASK, so pressing Back from Home closes the app instead of returning to the login screen.

Kotlin – ui/auth/AuthActivity.kt
package com.codingeasily.karteasy.ui.auth

import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.codingeasily.karteasy.MainActivity
import com.codingeasily.karteasy.databinding.ActivityAuthBinding

/** Hosts LoginFragment and RegisterFragment (nav_auth.xml). */
class AuthActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(ActivityAuthBinding.inflate(layoutInflater).root)
    }

    /** Called by the fragments after a successful login or sign-up. */
    fun openHome() {
        startActivity(Intent(this, MainActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK))
        finish()
    }
}
XML – layout/activity_auth.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/auth_nav_host"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/surface"
    app:defaultNavHost="true"
    app:navGraph="@navigation/nav_auth" />
XML – navigation/nav_auth.xml
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/nav_auth"
    app:startDestination="@id/loginFragment">

    <fragment
        android:id="@+id/loginFragment"
        android:name="com.codingeasily.karteasy.ui.auth.LoginFragment"
        android:label="@string/login">
        <action
            android:id="@+id/action_login_to_register"
            app:destination="@id/registerFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right" />
    </fragment>

    <fragment
        android:id="@+id/registerFragment"
        android:name="com.codingeasily.karteasy.ui.auth.RegisterFragment"
        android:label="@string/create_account" />
</navigation>
XML – anim/slide_in_right.xml (one of four slide animations)
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="250"
    android:fromXDelta="100%"
    android:interpolator="@android:interpolator/fast_out_slow_in"
    android:toXDelta="0" />

Step 2: Validator

The same rules as the PHP API, written once and used by Login, Register, Address and Edit Profile:

Kotlin – util/Validator.kt
package com.codingeasily.karteasy.util

import android.util.Patterns

/** Same rules as the PHP API, so the user sees errors before a network call. */
object Validator {
    fun email(v: String) = Patterns.EMAIL_ADDRESS.matcher(v.trim()).matches()
    fun phone(v: String) = Regex("^[6-9][0-9]{9}$").matches(v.trim())
    fun pincode(v: String) = Regex("^[1-9][0-9]{5}$").matches(v.trim())
    fun password(v: String) = v.length >= 6
    fun name(v: String) = v.trim().length in 2..80
}

Step 3: Login layout

TextInputLayout gives the floating label, start icon, error text under the field and — with endIconMode="password_toggle" — the eye button to show the password. autofillHints lets password managers fill the form.

XML – layout/fragment_login.xml
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="24dp">

        <ImageView
            android:layout_width="88dp"
            android:layout_height="88dp"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="32dp"
            android:importantForAccessibility="no"
            android:src="@mipmap/ic_launcher" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="20dp"
            android:text="@string/welcome_back"
            android:textAppearance="?attr/textAppearanceHeadlineMedium"
            android:textColor="@color/text_primary"
            android:textStyle="bold" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_marginBottom="28dp"
            android:text="@string/login_subtitle"
            android:textColor="@color/text_secondary" />

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/email_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/email"
            app:startIconDrawable="@drawable/ic_email">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/email_input"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:autofillHints="emailAddress"
                android:imeOptions="actionNext"
                android:inputType="textEmailAddress" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/password_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp"
            android:hint="@string/password"
            app:endIconMode="password_toggle"
            app:startIconDrawable="@drawable/ic_lock">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/password_input"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:autofillHints="password"
                android:imeOptions="actionDone"
                android:inputType="textPassword" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.button.MaterialButton
            android:id="@+id/login_button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="24dp"
            android:text="@string/login" />

        <com.google.android.material.progressindicator.LinearProgressIndicator
            android:id="@+id/progress"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:indeterminate="true"
            android:visibility="gone" />

        <com.google.android.material.button.MaterialButton
            android:id="@+id/demo_button"
            style="@style/Widget.KartEasy.Button.Outlined"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp"
            android:text="@string/use_demo" />

        <TextView
            android:id="@+id/register_link"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="24dp"
            android:background="?attr/selectableItemBackground"
            android:padding="8dp"
            android:text="@string/no_account"
            android:textColor="@color/brand"
            android:textStyle="bold" />
    </LinearLayout>
</ScrollView>

Step 4: AuthViewModel

The ViewModel exposes one StateFlow. Because the state lives in the ViewModel, rotating the phone during login does not start a second request or lose the progress bar. A double tap is ignored while loading.

Kotlin – ui/auth/AuthViewModel.kt
package com.codingeasily.karteasy.ui.auth

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.codingeasily.karteasy.data.api.Result
import com.codingeasily.karteasy.util.kartApp
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class AuthViewModel(app: Application) : AndroidViewModel(app) {

    sealed class State {
        object Idle : State()
        object Loading : State()
        object LoggedIn : State()
        data class Failed(val message: String) : State()
    }

    private val repo = app.kartApp.repository
    private val _state = MutableStateFlow<State>(State.Idle)
    val state: StateFlow<State> = _state

    fun login(email: String, password: String) = submit { repo.login(email, password) }

    fun register(name: String, email: String, phone: String, password: String) =
        submit { repo.register(name, email, phone, password) }

    private fun submit(call: suspend () -> Result<*>) {
        if (_state.value == State.Loading) return   // ignore double taps
        _state.value = State.Loading
        viewModelScope.launch {
            _state.value = when (val r = call()) {
                is Result.Success -> State.LoggedIn
                is Result.Error -> State.Failed(r.message)
            }
        }
    }

    fun consumeError() {
        if (_state.value is State.Failed) _state.value = State.Idle
    }
}
Idleform visibleLoadingbutton off, progress onLoggedInopen HomeFailedSnackbar → back to Idle
The four states of the auth screens
Kotlin – ShopRepository.kt (login part)
// ShopRepository.kt – login saves the token as soon as it arrives
suspend fun login(email: String, password: String): Result<AuthData> {
    val r = safeCall { api.login(LoginRequest(email.trim(), password)) }
    if (r is Result.Success) session.saveLogin(r.data.token, r.data.user)
    return r
}

Step 5: LoginFragment

The fragment validates, calls vm.login() and draws whatever state arrives. collectWhenStarted (from Extensions.kt) collects the flow only while the screen is visible.

Kotlin – ui/auth/LoginFragment.kt
package com.codingeasily.karteasy.ui.auth

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.codingeasily.karteasy.R
import com.codingeasily.karteasy.databinding.FragmentLoginBinding
import com.codingeasily.karteasy.util.Validator
import com.codingeasily.karteasy.util.collectWhenStarted
import com.codingeasily.karteasy.util.snack

class LoginFragment : Fragment() {

    private var _binding: FragmentLoginBinding? = null
    private val binding get() = _binding!!
    private val vm: AuthViewModel by viewModels()

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        _binding = FragmentLoginBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        binding.loginButton.setOnClickListener { submit() }
        binding.passwordInput.setOnEditorActionListener { _, actionId, _ ->
            if (actionId == EditorInfo.IME_ACTION_DONE) { submit(); true } else false
        }
        binding.registerLink.setOnClickListener { findNavController().navigate(R.id.action_login_to_register) }
        binding.demoButton.setOnClickListener {
            binding.emailInput.setText(getString(R.string.demo_email))
            binding.passwordInput.setText(getString(R.string.demo_password))
        }

        collectWhenStarted(vm.state) { state ->
            val loading = state is AuthViewModel.State.Loading
            binding.loginButton.isEnabled = !loading
            binding.progress.visibility = if (loading) View.VISIBLE else View.GONE
            when (state) {
                is AuthViewModel.State.LoggedIn -> (requireActivity() as AuthActivity).openHome()
                is AuthViewModel.State.Failed -> { snack(state.message); vm.consumeError() }
                else -> Unit
            }
        }
    }

    private fun submit() {
        val email = binding.emailInput.text.toString()
        val pass = binding.passwordInput.text.toString()
        binding.emailLayout.error = if (Validator.email(email)) null else getString(R.string.err_email)
        binding.passwordLayout.error = if (Validator.password(pass)) null else getString(R.string.err_password)
        if (binding.emailLayout.error == null && binding.passwordLayout.error == null) vm.login(email, pass)
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

Step 6: Register screen

Same pattern with more fields. The phone field shows a +91 prefix and a 10-digit counter; the user must accept the terms.

XML – layout/fragment_register.xml
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="24dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="24dp"
            android:text="@string/create_account"
            android:textAppearance="?attr/textAppearanceHeadlineMedium"
            android:textColor="@color/text_primary"
            android:textStyle="bold" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="24dp"
            android:text="@string/register_subtitle"
            android:textColor="@color/text_secondary" />

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/name_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/full_name"
            app:startIconDrawable="@drawable/ic_person">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/name_input"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:autofillHints="name"
                android:inputType="textPersonName|textCapWords" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/email_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp"
            android:hint="@string/email"
            app:startIconDrawable="@drawable/ic_email">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/email_input"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:autofillHints="emailAddress"
                android:inputType="textEmailAddress" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/phone_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp"
            android:hint="@string/mobile_optional"
            app:counterEnabled="true"
            app:counterMaxLength="10"
            app:prefixText="+91 "
            app:startIconDrawable="@drawable/ic_phone">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/phone_input"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:autofillHints="phone"
                android:inputType="phone"
                android:maxLength="10" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/password_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp"
            android:hint="@string/password"
            app:endIconMode="password_toggle"
            app:startIconDrawable="@drawable/ic_lock">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/password_input"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:autofillHints="newPassword"
                android:inputType="textPassword" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/confirm_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp"
            android:hint="@string/confirm_password"
            app:endIconMode="password_toggle"
            app:startIconDrawable="@drawable/ic_lock">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/confirm_input"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:inputType="textPassword" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.checkbox.MaterialCheckBox
            android:id="@+id/terms_check"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:text="@string/accept_terms" />

        <com.google.android.material.button.MaterialButton
            android:id="@+id/register_button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="@string/sign_up" />

        <com.google.android.material.progressindicator.LinearProgressIndicator
            android:id="@+id/progress"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:indeterminate="true"
            android:visibility="gone" />

        <TextView
            android:id="@+id/login_link"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="20dp"
            android:background="?attr/selectableItemBackground"
            android:padding="8dp"
            android:text="@string/have_account"
            android:textColor="@color/brand"
            android:textStyle="bold" />
    </LinearLayout>
</ScrollView>
Kotlin – ui/auth/RegisterFragment.kt
package com.codingeasily.karteasy.ui.auth

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.codingeasily.karteasy.R
import com.codingeasily.karteasy.databinding.FragmentRegisterBinding
import com.codingeasily.karteasy.util.Validator
import com.codingeasily.karteasy.util.collectWhenStarted
import com.codingeasily.karteasy.util.snack

class RegisterFragment : Fragment() {

    private var _binding: FragmentRegisterBinding? = null
    private val binding get() = _binding!!
    private val vm: AuthViewModel by viewModels()

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        _binding = FragmentRegisterBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        binding.registerButton.setOnClickListener { submit() }
        binding.loginLink.setOnClickListener { findNavController().navigateUp() }

        collectWhenStarted(vm.state) { state ->
            val loading = state is AuthViewModel.State.Loading
            binding.registerButton.isEnabled = !loading
            binding.progress.visibility = if (loading) View.VISIBLE else View.GONE
            when (state) {
                is AuthViewModel.State.LoggedIn -> (requireActivity() as AuthActivity).openHome()
                is AuthViewModel.State.Failed -> { snack(state.message); vm.consumeError() }
                else -> Unit
            }
        }
    }

    private fun submit() {
        val name = binding.nameInput.text.toString()
        val email = binding.emailInput.text.toString()
        val phone = binding.phoneInput.text.toString()
        val pass = binding.passwordInput.text.toString()
        val confirm = binding.confirmInput.text.toString()

        binding.nameLayout.error = if (Validator.name(name)) null else getString(R.string.err_name)
        binding.emailLayout.error = if (Validator.email(email)) null else getString(R.string.err_email)
        binding.phoneLayout.error = if (phone.isEmpty() || Validator.phone(phone)) null else getString(R.string.err_phone)
        binding.passwordLayout.error = if (Validator.password(pass)) null else getString(R.string.err_password)
        binding.confirmLayout.error = if (pass == confirm) null else getString(R.string.err_password_match)
        val ok = listOf(binding.nameLayout, binding.emailLayout, binding.phoneLayout, binding.passwordLayout, binding.confirmLayout)
            .all { it.error == null }
        if (!ok) return
        if (!binding.termsCheck.isChecked) {
            snack(getString(R.string.err_terms))
            return
        }
        vm.register(name, email, phone, pass)
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

What happens after login

LoginFragmentAuthViewModelShopRepositorySessionManagerlogin(email, pass)repo.login()POST auth/login.phpsaveLogin(token, user)Result.SuccessState.LoggedInopenHome() → MainActivity
From the Login button to the Home screen

Test cases

  1. 1Empty email → "Enter a valid email address" under the field, no network call
  2. 2Wrong password → Snackbar "Wrong email or password." from the API
  3. 3Use demo account → Login → Home opens
  4. 4Turn on airplane mode → "No internet connection…"
  5. 5Register with an existing email → "This email is already registered. Please login."
  6. 6Passwords different → "Passwords do not match"
  7. 7Rotate the phone while loading → still one request, progress stays
Seven checks for the auth flow

Frequently Asked Questions

Why validate in the app if the API validates too?
App validation gives instant feedback without a network call and saves mobile data. API validation is the real protection. Both use the same rules (Validator.kt mirrors the PHP checks).
Why is the state a sealed class?
Idle, Loading, LoggedIn and Failed cover every situation the screen can be in, and the "when" in the fragment must handle all of them. The Kotlin compiler then reminds you if you add a new state and forget to show it.
Should I remove the "Use demo account" button?
Yes, before publishing on Play Store. It is only there so testers and students can log in quickly.
← E-commerce App Tutorial
🔗

Share this topic with a friend

यह topic किसी दोस्त को भेजें

Found it useful? Send it to a classmate learning the same thing.

अच्छा लगा? जो दोस्त यही सीख रहा है, उसे भेज दीजिए।

Next topicRetrofit, Token & Session Your progress: 16 / 28 (57%) · Lesson 17 / 28 Continue →

How was this lesson?

Your feedback helps us make the content better.

Write a review or suggestion (optional)

After approval, your name and city may appear publicly with your review. Please do not write your email or phone number.

▶ CodingEasily on YouTube

Learn with video lessons in easy Hindi

Featured video: RDBMS Part 3 – Class XI Commerce IT

▶ Subscribe – it’s free