Skip to content

Commit 4ee47d4

Browse files
authored
Merge pull request #38 from Kotlin/master-jetpack-compose
Update starter sample to Jetpack Compose, StateFlow, Material3
2 parents e5f67dc + ad775ec commit 4ee47d4

File tree

10 files changed

+226
-177
lines changed

10 files changed

+226
-177
lines changed

.idea/gradle.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/misc.xml

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/build.gradle.kts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11
33
plugins {
44
alias(libs.plugins.androidApplication)
55
alias(libs.plugins.kotlinAndroid)
6+
alias(libs.plugins.compose.compiler)
67
}
78

89
kotlin {
@@ -38,8 +39,9 @@ android {
3839
targetCompatibility = JavaVersion.VERSION_11
3940
}
4041
buildFeatures {
41-
viewBinding = true
42+
compose = true
4243
}
44+
4345
namespace = "com.jetbrains.simplelogin.androidapp"
4446
}
4547

@@ -48,9 +50,24 @@ dependencies {
4850
implementation(libs.androidx.appcompat)
4951
implementation(libs.androidx.material)
5052
implementation(libs.androidx.annotation)
51-
implementation(libs.androidx.constraintlayout)
52-
implementation(libs.androidx.lifecycle.livedata.ktx)
5353
implementation(libs.androidx.lifecycle.viewmodel.ktx)
54+
implementation(libs.androidx.lifecycle.runtime.ktx)
55+
implementation(libs.androidx.lifecycle.runtime.compose)
56+
implementation(libs.androidx.lifecycle.viewmodel.compose)
57+
58+
// Compose
59+
implementation(libs.androidx.activity.compose)
60+
implementation(libs.compose.ui)
61+
implementation(libs.compose.ui.tooling.preview)
62+
implementation(libs.compose.foundation)
63+
implementation(libs.compose.material3)
64+
implementation(libs.compose.runtime)
65+
debugImplementation(libs.compose.ui.tooling)
66+
67+
// Coroutines
68+
implementation(libs.kotlinx.coroutines.core)
69+
implementation(libs.kotlinx.coroutines.android)
70+
5471
testImplementation(libs.junit)
5572
androidTestImplementation(libs.androidx.test.junit)
5673
androidTestImplementation(libs.androidx.espresso.core)
Lines changed: 34 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,130 +1,51 @@
11
package com.jetbrains.simplelogin.androidapp.ui.login
22

33
import android.app.Activity
4-
import androidx.lifecycle.Observer
5-
import androidx.lifecycle.ViewModelProvider
64
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.EditText
145
import android.widget.Toast
15-
import com.jetbrains.simplelogin.androidapp.databinding.ActivityLoginBinding
16-
6+
import androidx.activity.compose.setContent
7+
import androidx.appcompat.app.AppCompatActivity
8+
import androidx.compose.material3.MaterialTheme
9+
import androidx.compose.material3.Surface
1710
import com.jetbrains.simplelogin.androidapp.R
11+
import com.jetbrains.simplelogin.androidapp.data.LoginDataSource
12+
import com.jetbrains.simplelogin.androidapp.data.LoginDataValidator
13+
import com.jetbrains.simplelogin.androidapp.data.LoginRepository
1814

1915
class LoginActivity : AppCompatActivity() {
2016

21-
private lateinit var loginViewModel: LoginViewModel
22-
private lateinit var binding: ActivityLoginBinding
23-
2417
override fun onCreate(savedInstanceState: Bundle?) {
2518
super.onCreate(savedInstanceState)
2619

27-
binding = ActivityLoginBinding.inflate(layoutInflater)
28-
setContentView(binding.root)
29-
30-
val username = binding.username
31-
val password = binding.password
32-
val login = binding.login
33-
val loading = binding.loading
34-
35-
loginViewModel = ViewModelProvider(this, LoginViewModelFactory())
36-
.get(LoginViewModel::class.java)
37-
38-
loginViewModel.loginFormState.observe(this@LoginActivity, Observer {
39-
val loginState = it ?: return@Observer
40-
41-
// disable login button unless both username / password is valid
42-
login.isEnabled = loginState.isDataValid
43-
44-
if (loginState.usernameError != null) {
45-
username.error = loginState.usernameError
46-
}
47-
if (loginState.passwordError != null) {
48-
password.error = loginState.passwordError
49-
}
50-
})
51-
52-
loginViewModel.loginResult.observe(this@LoginActivity, Observer {
53-
val loginResult = it ?: return@Observer
54-
55-
loading.visibility = View.GONE
56-
if (loginResult.error != null) {
57-
showLoginFailed(loginResult.error)
58-
}
59-
if (loginResult.success != null) {
60-
updateUiWithUser(loginResult.success)
61-
}
62-
setResult(Activity.RESULT_OK)
63-
64-
//Complete and destroy login activity once successful
65-
finish()
66-
})
67-
68-
username.afterTextChanged {
69-
loginViewModel.loginDataChanged(
70-
username.text.toString(),
71-
password.text.toString()
72-
)
73-
}
74-
75-
password.apply {
76-
afterTextChanged {
77-
loginViewModel.loginDataChanged(
78-
username.text.toString(),
79-
password.text.toString()
80-
)
81-
}
82-
83-
setOnEditorActionListener { _, actionId, _ ->
84-
when (actionId) {
85-
EditorInfo.IME_ACTION_DONE ->
86-
loginViewModel.login(
87-
username.text.toString(),
88-
password.text.toString()
89-
)
20+
setContent {
21+
MaterialTheme {
22+
Surface() {
23+
LoginScreen(
24+
viewModel = LoginViewModel(
25+
loginRepository = LoginRepository(
26+
dataSource = LoginDataSource()
27+
),
28+
dataValidator = LoginDataValidator()
29+
),
30+
onLoginSuccess = {
31+
// Show welcome message
32+
val successResult = it.success
33+
successResult?.let {
34+
val welcome = getString(R.string.welcome)
35+
Toast.makeText(
36+
applicationContext,
37+
"$welcome ${it.displayName}",
38+
Toast.LENGTH_LONG
39+
).show()
40+
}
41+
42+
// Complete the login process
43+
setResult(Activity.RESULT_OK)
44+
finish()
45+
}
46+
)
9047
}
91-
false
92-
}
93-
94-
login.setOnClickListener {
95-
loading.visibility = View.VISIBLE
96-
loginViewModel.login(username.text.toString(), password.text.toString())
9748
}
9849
}
9950
}
100-
101-
private fun updateUiWithUser(model: LoggedInUserView) {
102-
val welcome = getString(R.string.welcome)
103-
val displayName = model.displayName
104-
// TODO : initiate successful logged in experience
105-
Toast.makeText(
106-
applicationContext,
107-
"$welcome $displayName",
108-
Toast.LENGTH_LONG
109-
).show()
110-
}
111-
112-
private fun showLoginFailed(@StringRes errorString: Int) {
113-
Toast.makeText(applicationContext, errorString, Toast.LENGTH_SHORT).show()
114-
}
11551
}
116-
117-
/**
118-
* Extension function to simplify setting an afterTextChanged action to EditText components.
119-
*/
120-
fun EditText.afterTextChanged(afterTextChanged: (String) -> Unit) {
121-
this.addTextChangedListener(object : TextWatcher {
122-
override fun afterTextChanged(editable: Editable?) {
123-
afterTextChanged.invoke(editable.toString())
124-
}
125-
126-
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
127-
128-
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
129-
})
130-
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package com.jetbrains.simplelogin.androidapp.ui.login
2+
3+
import androidx.compose.foundation.layout.*
4+
import androidx.compose.foundation.text.KeyboardActions
5+
import androidx.compose.foundation.text.KeyboardOptions
6+
import androidx.compose.material3.*
7+
import androidx.compose.runtime.*
8+
import androidx.compose.ui.Alignment
9+
import androidx.compose.ui.Modifier
10+
import androidx.compose.ui.focus.FocusRequester
11+
import androidx.compose.ui.focus.focusRequester
12+
import androidx.compose.ui.platform.LocalContext
13+
import androidx.compose.ui.res.stringResource
14+
import androidx.compose.ui.text.input.ImeAction
15+
import androidx.compose.ui.text.input.KeyboardType
16+
import androidx.compose.ui.text.input.PasswordVisualTransformation
17+
import androidx.compose.ui.unit.dp
18+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
19+
import com.jetbrains.simplelogin.androidapp.R
20+
21+
@Composable
22+
fun LoginScreen(
23+
viewModel: LoginViewModel,
24+
onLoginSuccess: (LoginResult) -> Unit
25+
) {
26+
val loginFormState by viewModel.loginFormState.collectAsStateWithLifecycle()
27+
val loginResult by viewModel.loginResult.collectAsStateWithLifecycle()
28+
29+
var username by remember { mutableStateOf("") }
30+
var password by remember { mutableStateOf("") }
31+
val passwordFocusRequester = remember { FocusRequester() }
32+
33+
// Handle login result
34+
LaunchedEffect(loginResult) {
35+
loginResult?.let { result ->
36+
if (result.success != null) {
37+
// Show welcome message
38+
onLoginSuccess(result)
39+
}
40+
}
41+
}
42+
43+
Column(
44+
modifier = Modifier
45+
.fillMaxSize()
46+
.padding(16.dp),
47+
horizontalAlignment = Alignment.CenterHorizontally,
48+
verticalArrangement = Arrangement.Center
49+
) {
50+
// Username field
51+
OutlinedTextField(
52+
value = username,
53+
onValueChange = {
54+
username = it
55+
viewModel.loginDataChanged(username, password)
56+
},
57+
label = { Text(stringResource(R.string.prompt_email)) },
58+
isError = loginFormState.usernameError != null,
59+
supportingText = {
60+
loginFormState.usernameError?.let {
61+
Text(it)
62+
}
63+
},
64+
keyboardOptions = KeyboardOptions(
65+
keyboardType = KeyboardType.Email,
66+
imeAction = ImeAction.Next
67+
),
68+
keyboardActions = KeyboardActions(
69+
onNext = { passwordFocusRequester.requestFocus() }
70+
),
71+
singleLine = true,
72+
modifier = Modifier
73+
.fillMaxWidth()
74+
.padding(bottom = 8.dp)
75+
)
76+
77+
// Password field
78+
OutlinedTextField(
79+
value = password,
80+
onValueChange = {
81+
password = it
82+
viewModel.loginDataChanged(username, password)
83+
},
84+
label = { Text(stringResource(R.string.prompt_password)) },
85+
isError = loginFormState.passwordError != null,
86+
supportingText = {
87+
loginFormState.passwordError?.let {
88+
Text(it)
89+
}
90+
},
91+
visualTransformation = PasswordVisualTransformation(),
92+
keyboardOptions = KeyboardOptions(
93+
keyboardType = KeyboardType.Password,
94+
imeAction = ImeAction.Done
95+
),
96+
keyboardActions = KeyboardActions(
97+
onDone = {
98+
if (loginFormState.isDataValid) {
99+
viewModel.login(username, password)
100+
}
101+
}
102+
),
103+
singleLine = true,
104+
modifier = Modifier
105+
.fillMaxWidth()
106+
.padding(bottom = 16.dp)
107+
.focusRequester(passwordFocusRequester)
108+
)
109+
110+
// Login button
111+
Button(
112+
onClick = { viewModel.login(username, password) },
113+
enabled = loginFormState.isDataValid,
114+
modifier = Modifier
115+
.fillMaxWidth()
116+
.height(50.dp)
117+
) {
118+
Text(stringResource(R.string.action_sign_in))
119+
}
120+
121+
// Loading indicator
122+
if (loginResult != null && loginResult?.success == null && loginResult?.error == null) {
123+
CircularProgressIndicator(
124+
modifier = Modifier.padding(16.dp)
125+
)
126+
}
127+
128+
// Error message
129+
loginResult?.error?.let { errorId ->
130+
Text(
131+
text = stringResource(errorId),
132+
color = MaterialTheme.colorScheme.error,
133+
modifier = Modifier.padding(top = 16.dp)
134+
)
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)