Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ android {
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
viewBinding true
}
dataBinding {
enabled = true
}
}

dependencies {
Expand All @@ -38,6 +44,9 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.8.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.annotation:annotation:1.6.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
android:supportsRtl="true"
android:theme="@style/Theme.MyApplication"
tools:targetApi="31">
<activity
android:name=".ui.login.LoginActivity"
android:exported="false"
android:label="@string/title_activity_login" />
<activity
android:name=".MainActivity"
android:exported="true">
Expand Down
13 changes: 12 additions & 1 deletion app/src/main/java/com/example/myapplication/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
package com.example.myapplication

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.LayoutInflater
import androidx.databinding.DataBindingUtil
import com.example.myapplication.databinding.ActivityMainBinding
import com.example.myapplication.ui.login.LoginActivity

class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
setContentView(binding.root)
binding.loginButton.setOnClickListener {
startActivity(Intent(this, LoginActivity::class.java))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.example.myapplication.data

import com.example.myapplication.data.model.LoggedInUser
import java.io.IOException

/**
* Class that handles authentication w/ login credentials and retrieves user information.
*/
class LoginDataSource {

fun login(username: String, password: String): Result<LoggedInUser> {
return try {
// TODO: handle loggedInUser authentication
val fakeUser = LoggedInUser(java.util.UUID.randomUUID().toString(), "Jane Doe")
Result.Success(fakeUser)
} catch (e: Throwable) {
Result.Error(IOException("Error logging in", e))
}
}

fun logout() {
// TODO: revoke authentication
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AIReviewRadar

The code looks good. Please add a newline at the end of the file to follow best practices.

@@ -0,0 +1,25 @@
+package com.example.myapplication.data
+
+import com.example.myapplication.data.model.LoggedInUser
+import java.io.IOException
+
+/**
+ * Class that handles authentication w/ login credentials and retrieves user information.
+ */
+class LoginDataSource {
+
+    fun login(username: String, password: String): Result<LoggedInUser> {
+        return try {
+            // TODO: handle loggedInUser authentication
+            val fakeUser = LoggedInUser(java.util.UUID.randomUUID().toString(), "Jane Doe")
+            Result.Success(fakeUser)
+        } catch (e: Throwable) {
+            Result.Error(IOException("Error logging in", e))
+        }
+    }
+
+    fun logout() {
+        // TODO: revoke authentication
+    }
+    
+}

Please make this change and reply with "OK" to confirm.

Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.example.myapplication.data

import com.example.myapplication.data.model.LoggedInUser

/**
* Class that requests authentication and user information from the remote data source and
* maintains an in-memory cache of login status and user credentials information.
*/

class LoginRepository(val dataSource: LoginDataSource) {

// in-memory cache of the loggedInUser object
var user: LoggedInUser? = null
private set

val isLoggedIn: Boolean
get() = user != null

init {
// If user credentials will be cached in local storage, it is recommended it be encrypted
// @see https://developer.android.com/training/articles/keystore
user = null
}

fun logout() {
user = null
dataSource.logout()
}

fun login(username: String, password: String): Result<LoggedInUser> {
// handle login
val result = dataSource.login(username, password)

if (result is Result.Success) {
setLoggedInUser(result.data)
}

return result
}

private fun setLoggedInUser(loggedInUser: LoggedInUser) {
this.user = loggedInUser
// If user credentials will be cached in local storage, it is recommended it be encrypted
// @see https://developer.android.com/training/articles/keystore
}
}
18 changes: 18 additions & 0 deletions app/src/main/java/com/example/myapplication/data/Result.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.myapplication.data

/**
* A generic class that holds a value with its loading status.
* @param <T>
*/
sealed class Result<out T : Any> {

data class Success<out T : Any>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()

override fun toString(): String {
return when (this) {
is Success<*> -> "Success[data=$data]"
is Error -> "Error[exception=$exception]"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.myapplication.data.model

/**
* Data class that captures user information for logged in users retrieved from LoginRepository
*/
data class LoggedInUser(
val userId: String,
val displayName: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.myapplication.ui.login

/**
* User details post authentication that is exposed to the UI
*/
data class LoggedInUserView(
val displayName: String
//... other data fields that may be accessible to the UI
)
130 changes: 130 additions & 0 deletions app/src/main/java/com/example/myapplication/ui/login/LoginActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package com.example.myapplication.ui.login

import android.app.Activity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import android.os.Bundle
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.Toast
import com.example.myapplication.databinding.ActivityLoginBinding

import com.example.myapplication.R

class LoginActivity : AppCompatActivity() {

private lateinit var loginViewModel: LoginViewModel
private lateinit var binding: ActivityLoginBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)

val username = binding.username
val password = binding.password
val login = binding.login
val loading = binding.loading

loginViewModel = ViewModelProvider(this, LoginViewModelFactory())
.get(LoginViewModel::class.java)

loginViewModel.loginFormState.observe(this@LoginActivity, Observer {
val loginState = it ?: return@Observer

// disable login button unless both username / password is valid
login.isEnabled = loginState.isDataValid

if (loginState.usernameError != null) {
username.error = getString(loginState.usernameError)
}
if (loginState.passwordError != null) {
password.error = getString(loginState.passwordError)
}
})

loginViewModel.loginResult.observe(this@LoginActivity, Observer {
val loginResult = it ?: return@Observer

loading.visibility = View.GONE
if (loginResult.error != null) {
showLoginFailed(loginResult.error)
}
if (loginResult.success != null) {
updateUiWithUser(loginResult.success)
}
setResult(Activity.RESULT_OK)

//Complete and destroy login activity once successful
finish()
})

username.afterTextChanged {
loginViewModel.loginDataChanged(
username.text.toString(),
password.text.toString()
)
}

password.apply {
afterTextChanged {
loginViewModel.loginDataChanged(
username.text.toString(),
password.text.toString()
)
}

setOnEditorActionListener { _, actionId, _ ->
when (actionId) {
EditorInfo.IME_ACTION_DONE ->
loginViewModel.login(
username.text.toString(),
password.text.toString()
)
}
false
}

login.setOnClickListener {
loading.visibility = View.VISIBLE
loginViewModel.login(username.text.toString(), password.text.toString())
}
}
}

private fun updateUiWithUser(model: LoggedInUserView) {
val welcome = getString(R.string.welcome)
val displayName = model.displayName
// TODO : initiate successful logged in experience
Toast.makeText(
applicationContext,
"$welcome $displayName",
Toast.LENGTH_LONG
).show()
}

private fun showLoginFailed(@StringRes errorString: Int) {
Toast.makeText(applicationContext, errorString, Toast.LENGTH_SHORT).show()
}
}

/**
* Extension function to simplify setting an afterTextChanged action to EditText components.
*/
fun EditText.afterTextChanged(afterTextChanged: (String) -> Unit) {
this.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(editable: Editable?) {
afterTextChanged.invoke(editable.toString())
}

override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}

override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.myapplication.ui.login

/**
* Data validation state of the login form.
*/
data class LoginFormState(
val usernameError: Int? = null,
val passwordError: Int? = null,
val isDataValid: Boolean = false
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.myapplication.ui.login

/**
* Authentication result : success (user details) or error message.
*/
data class LoginResult(
val success: LoggedInUserView? = null,
val error: Int? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.example.myapplication.ui.login

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import android.util.Patterns
import com.example.myapplication.data.LoginRepository
import com.example.myapplication.data.Result

import com.example.myapplication.R

class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() {

private val _loginForm = MutableLiveData<LoginFormState>()
val loginFormState: LiveData<LoginFormState> = _loginForm

private val _loginResult = MutableLiveData<LoginResult>()
val loginResult: LiveData<LoginResult> = _loginResult

fun login(username: String, password: String) {
// can be launched in a separate asynchronous job
val result = loginRepository.login(username, password)

if (result is Result.Success) {
_loginResult.value =
LoginResult(success = LoggedInUserView(displayName = result.data.displayName))
} else {
_loginResult.value = LoginResult(error = R.string.login_failed)
}
}

fun loginDataChanged(username: String, password: String) {
if (!isUserNameValid(username)) {
_loginForm.value = LoginFormState(usernameError = R.string.invalid_username)
} else if (!isPasswordValid(password)) {
_loginForm.value = LoginFormState(passwordError = R.string.invalid_password)
} else {
_loginForm.value = LoginFormState(isDataValid = true)
}
}

// A placeholder username validation check
private fun isUserNameValid(username: String): Boolean {
return if (username.contains('@')) {
Patterns.EMAIL_ADDRESS.matcher(username).matches()
} else {
username.isNotBlank()
}
}

// A placeholder password validation check
private fun isPasswordValid(password: String): Boolean {
return password.length > 5
}
}
Loading