Skip to content
This repository was archived by the owner on May 19, 2022. It is now read-only.

Commit 1e2caa8

Browse files
author
Rory Kelly
committed
initial android project
1 parent d9a6420 commit 1e2caa8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1113
-0
lines changed

.gitignore

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
*.iml
2+
.gradle
3+
/local.properties
4+
/.idea/caches
5+
/.idea/libraries
6+
/.idea/modules.xml
7+
/.idea/workspace.xml
8+
/.idea/navEditor.xml
9+
/.idea/assetWizardSettings.xml
10+
.DS_Store
11+
/build
12+
/captures
13+
.externalNativeBuild
14+
.cxx
15+
/.idea

app/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

app/build.gradle

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
apply plugin: 'com.android.application'
2+
3+
apply plugin: 'kotlin-android'
4+
5+
apply plugin: 'kotlin-android-extensions'
6+
7+
android {
8+
compileSdkVersion 29
9+
buildToolsVersion "29.0.2"
10+
defaultConfig {
11+
applicationId "com.puddlealley.splash"
12+
minSdkVersion 23
13+
targetSdkVersion 29
14+
versionCode 1
15+
versionName "1.0"
16+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17+
}
18+
buildTypes {
19+
release {
20+
minifyEnabled false
21+
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
22+
}
23+
}
24+
}
25+
26+
dependencies {
27+
implementation fileTree(dir: 'libs', include: ['*.jar'])
28+
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
29+
implementation 'androidx.appcompat:appcompat:1.1.0'
30+
implementation 'androidx.core:core-ktx:1.1.0'
31+
implementation 'com.google.android.material:material:1.0.0'
32+
implementation 'androidx.annotation:annotation:1.1.0'
33+
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
34+
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
35+
testImplementation 'junit:junit:4.12'
36+
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
37+
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
38+
}

app/proguard-rules.pro

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Add project specific ProGuard rules here.
2+
# You can control the set of applied configuration files using the
3+
# proguardFiles setting in build.gradle.
4+
#
5+
# For more details, see
6+
# http://developer.android.com/guide/developing/tools/proguard.html
7+
8+
# If your project uses WebView with JS, uncomment the following
9+
# and specify the fully qualified class name to the JavaScript interface
10+
# class:
11+
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12+
# public *;
13+
#}
14+
15+
# Uncomment this to preserve the line number information for
16+
# debugging stack traces.
17+
#-keepattributes SourceFile,LineNumberTable
18+
19+
# If you keep the line number information, uncomment this to
20+
# hide the original source file name.
21+
#-renamesourcefileattribute SourceFile
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.puddlealley.splash
2+
3+
import androidx.test.platform.app.InstrumentationRegistry
4+
import androidx.test.ext.junit.runners.AndroidJUnit4
5+
6+
import org.junit.Test
7+
import org.junit.runner.RunWith
8+
9+
import org.junit.Assert.*
10+
11+
/**
12+
* Instrumented test, which will execute on an Android device.
13+
*
14+
* See [testing documentation](http://d.android.com/tools/testing).
15+
*/
16+
@RunWith(AndroidJUnit4::class)
17+
class ExampleInstrumentedTest {
18+
@Test
19+
fun useAppContext() {
20+
// Context of the app under test.
21+
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22+
assertEquals("com.puddlealley.splash", appContext.packageName)
23+
}
24+
}

app/src/main/AndroidManifest.xml

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
package="com.puddlealley.splash">
4+
5+
<application
6+
android:allowBackup="true"
7+
android:icon="@mipmap/ic_launcher"
8+
android:label="@string/app_name"
9+
android:roundIcon="@mipmap/ic_launcher_round"
10+
android:supportsRtl="true"
11+
android:theme="@style/AppTheme">
12+
<activity
13+
android:name=".ui.login.LoginActivity"
14+
android:label="@string/app_name">
15+
<intent-filter>
16+
<action android:name="android.intent.action.MAIN" />
17+
18+
<category android:name="android.intent.category.LAUNCHER" />
19+
</intent-filter>
20+
</activity>
21+
</application>
22+
23+
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.puddlealley.splash.data
2+
3+
import com.puddlealley.splash.data.model.LoggedInUser
4+
import java.io.IOException
5+
6+
/**
7+
* Class that handles authentication w/ login credentials and retrieves user information.
8+
*/
9+
class LoginDataSource {
10+
11+
fun login(username: String, password: String): Result<LoggedInUser> {
12+
try {
13+
// TODO: handle loggedInUser authentication
14+
val fakeUser = LoggedInUser(java.util.UUID.randomUUID().toString(), "Jane Doe")
15+
return Result.Success(fakeUser)
16+
} catch (e: Throwable) {
17+
return Result.Error(IOException("Error logging in", e))
18+
}
19+
}
20+
21+
fun logout() {
22+
// TODO: revoke authentication
23+
}
24+
}
25+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.puddlealley.splash.data
2+
3+
import com.puddlealley.splash.data.model.LoggedInUser
4+
5+
/**
6+
* Class that requests authentication and user information from the remote data source and
7+
* maintains an in-memory cache of login status and user credentials information.
8+
*/
9+
10+
class LoginRepository(val dataSource: LoginDataSource) {
11+
12+
// in-memory cache of the loggedInUser object
13+
var user: LoggedInUser? = null
14+
private set
15+
16+
val isLoggedIn: Boolean
17+
get() = user != null
18+
19+
init {
20+
// If user credentials will be cached in local storage, it is recommended it be encrypted
21+
// @see https://developer.android.com/training/articles/keystore
22+
user = null
23+
}
24+
25+
fun logout() {
26+
user = null
27+
dataSource.logout()
28+
}
29+
30+
fun login(username: String, password: String): Result<LoggedInUser> {
31+
// handle login
32+
val result = dataSource.login(username, password)
33+
34+
if (result is Result.Success) {
35+
setLoggedInUser(result.data)
36+
}
37+
38+
return result
39+
}
40+
41+
private fun setLoggedInUser(loggedInUser: LoggedInUser) {
42+
this.user = loggedInUser
43+
// If user credentials will be cached in local storage, it is recommended it be encrypted
44+
// @see https://developer.android.com/training/articles/keystore
45+
}
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.puddlealley.splash.data
2+
3+
/**
4+
* A generic class that holds a value with its loading status.
5+
* @param <T>
6+
*/
7+
sealed class Result<out T : Any> {
8+
9+
data class Success<out T : Any>(val data: T) : Result<T>()
10+
data class Error(val exception: Exception) : Result<Nothing>()
11+
12+
override fun toString(): String {
13+
return when (this) {
14+
is Success<*> -> "Success[data=$data]"
15+
is Error -> "Error[exception=$exception]"
16+
}
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.puddlealley.splash.data.model
2+
3+
/**
4+
* Data class that captures user information for logged in users retrieved from LoginRepository
5+
*/
6+
data class LoggedInUser(
7+
val userId: String,
8+
val displayName: String
9+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.puddlealley.splash.ui.login
2+
3+
/**
4+
* User details post authentication that is exposed to the UI
5+
*/
6+
data class LoggedInUserView(
7+
val displayName: String
8+
//... other data fields that may be accessible to the UI
9+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package com.puddlealley.splash.ui.login
2+
3+
import android.app.Activity
4+
import androidx.lifecycle.Observer
5+
import androidx.lifecycle.ViewModelProviders
6+
import android.os.Bundle
7+
import androidx.annotation.StringRes
8+
import androidx.appcompat.app.AppCompatActivity
9+
import android.text.Editable
10+
import android.text.TextWatcher
11+
import android.view.View
12+
import android.view.inputmethod.EditorInfo
13+
import android.widget.Button
14+
import android.widget.EditText
15+
import android.widget.ProgressBar
16+
import android.widget.Toast
17+
18+
import com.puddlealley.splash.R
19+
20+
class LoginActivity : AppCompatActivity() {
21+
22+
private lateinit var loginViewModel: LoginViewModel
23+
24+
override fun onCreate(savedInstanceState: Bundle?) {
25+
super.onCreate(savedInstanceState)
26+
27+
setContentView(R.layout.activity_login)
28+
29+
val username = findViewById<EditText>(R.id.username)
30+
val password = findViewById<EditText>(R.id.password)
31+
val login = findViewById<Button>(R.id.login)
32+
val loading = findViewById<ProgressBar>(R.id.loading)
33+
34+
loginViewModel = ViewModelProviders.of(this, LoginViewModelFactory())
35+
.get(LoginViewModel::class.java)
36+
37+
loginViewModel.loginFormState.observe(this@LoginActivity, Observer {
38+
val loginState = it ?: return@Observer
39+
40+
// disable login button unless both username / password is valid
41+
login.isEnabled = loginState.isDataValid
42+
43+
if (loginState.usernameError != null) {
44+
username.error = getString(loginState.usernameError)
45+
}
46+
if (loginState.passwordError != null) {
47+
password.error = getString(loginState.passwordError)
48+
}
49+
})
50+
51+
loginViewModel.loginResult.observe(this@LoginActivity, Observer {
52+
val loginResult = it ?: return@Observer
53+
54+
loading.visibility = View.GONE
55+
if (loginResult.error != null) {
56+
showLoginFailed(loginResult.error)
57+
}
58+
if (loginResult.success != null) {
59+
updateUiWithUser(loginResult.success)
60+
}
61+
setResult(Activity.RESULT_OK)
62+
63+
//Complete and destroy login activity once successful
64+
finish()
65+
})
66+
67+
username.afterTextChanged {
68+
loginViewModel.loginDataChanged(
69+
username.text.toString(),
70+
password.text.toString()
71+
)
72+
}
73+
74+
password.apply {
75+
afterTextChanged {
76+
loginViewModel.loginDataChanged(
77+
username.text.toString(),
78+
password.text.toString()
79+
)
80+
}
81+
82+
setOnEditorActionListener { _, actionId, _ ->
83+
when (actionId) {
84+
EditorInfo.IME_ACTION_DONE ->
85+
loginViewModel.login(
86+
username.text.toString(),
87+
password.text.toString()
88+
)
89+
}
90+
false
91+
}
92+
93+
login.setOnClickListener {
94+
loading.visibility = View.VISIBLE
95+
loginViewModel.login(username.text.toString(), password.text.toString())
96+
}
97+
}
98+
}
99+
100+
private fun updateUiWithUser(model: LoggedInUserView) {
101+
val welcome = getString(R.string.welcome)
102+
val displayName = model.displayName
103+
// TODO : initiate successful logged in experience
104+
Toast.makeText(
105+
applicationContext,
106+
"$welcome $displayName",
107+
Toast.LENGTH_LONG
108+
).show()
109+
}
110+
111+
private fun showLoginFailed(@StringRes errorString: Int) {
112+
Toast.makeText(applicationContext, errorString, Toast.LENGTH_SHORT).show()
113+
}
114+
}
115+
116+
/**
117+
* Extension function to simplify setting an afterTextChanged action to EditText components.
118+
*/
119+
fun EditText.afterTextChanged(afterTextChanged: (String) -> Unit) {
120+
this.addTextChangedListener(object : TextWatcher {
121+
override fun afterTextChanged(editable: Editable?) {
122+
afterTextChanged.invoke(editable.toString())
123+
}
124+
125+
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
126+
127+
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
128+
})
129+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.puddlealley.splash.ui.login
2+
3+
/**
4+
* Data validation state of the login form.
5+
*/
6+
data class LoginFormState(
7+
val usernameError: Int? = null,
8+
val passwordError: Int? = null,
9+
val isDataValid: Boolean = false
10+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.puddlealley.splash.ui.login
2+
3+
/**
4+
* Authentication result : success (user details) or error message.
5+
*/
6+
data class LoginResult(
7+
val success: LoggedInUserView? = null,
8+
val error: Int? = null
9+
)

0 commit comments

Comments
 (0)