Migrate GeneratePasswordActivity to compose. (#314)

* Move password generation logic to ViewModel.

* Store GeneratePassword state in GeneratePasswordViewState.

* Update GeneratePasswordState when form is updated.

* Remove password StateFlow nad use the one in the ViewState.

* Fix dependency version incompatibility issue and add missing dependencies.

* Add mdc3Theme adapter.

* Add GeneratePasswordContent form.

* Integrate GeneratePasswordContent.

* Fix spotless issues.

* Remove xml layout file.
This commit is contained in:
Abhishek Saxena
2023-01-31 22:41:53 +05:30
committed by GitHub
parent 1c76db717e
commit 9a2917f9a2
9 changed files with 431 additions and 139 deletions

View File

@@ -36,6 +36,10 @@ android {
kotlinOptions {
jvmTarget = '1.8'
useIR = true
freeCompilerArgs += [
'-Xopt-in=androidx.compose.material3.ExperimentalMaterial3Api'
]
}
buildFeatures {
compose true
@@ -84,13 +88,18 @@ dependencies {
api project(":common")
implementation "androidx.compose.ui:ui:1.3.3"
implementation "androidx.compose.material:material:1.3.1"
implementation "androidx.compose.material:material:1.4.0-alpha04"
implementation "androidx.compose.ui:ui-tooling-preview:1.3.3"
implementation 'androidx.activity:activity-compose:1.5.1'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.0-alpha03"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1"
implementation "androidx.compose.material:material-icons-extended:1.3.1"
androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.3.3"
debugImplementation "androidx.compose.ui:ui-tooling:1.3.3"
implementation 'androidx.compose.material3:material3:1.1.0-alpha04'
implementation "com.google.accompanist:accompanist-themeadapter-material3:0.28.0"
// XML Libraries

View File

@@ -1,47 +1,23 @@
package com.yogeshpaliyal.keypasscompose.ui.generate
import android.content.ClipData
import android.content.ClipboardManager
import android.os.Bundle
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.yogeshpaliyal.common.utils.PasswordGenerator
import com.yogeshpaliyal.keypasscompose.R
import com.yogeshpaliyal.keypasscompose.databinding.ActivityGeneratePasswordBinding
import com.google.accompanist.themeadapter.material3.Mdc3Theme
import com.yogeshpaliyal.keypasscompose.ui.generate.ui.GeneratePasswordScreen
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class GeneratePasswordActivity : AppCompatActivity() {
private lateinit var binding: ActivityGeneratePasswordBinding
private val viewModel: GeneratePasswordViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityGeneratePasswordBinding.inflate(layoutInflater)
setContentView(binding.root)
generatePassword()
binding.btnRefresh.setOnClickListener {
generatePassword()
setContent {
Mdc3Theme {
GeneratePasswordScreen(viewModel)
}
}
binding.tilPassword.setEndIconOnClickListener {
val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("random_password", binding.etPassword.text)
clipboard.setPrimaryClip(clip)
Toast.makeText(this, getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT).show()
}
}
private fun generatePassword() {
val password = PasswordGenerator(
length = binding.sliderPasswordLength.value.toInt(),
includeUpperCaseLetters = binding.cbCapAlphabets.isChecked,
includeLowerCaseLetters = binding.cbLowerAlphabets.isChecked,
includeSymbols = binding.cbSymbols.isChecked,
includeNumbers = binding.cbNumbers.isChecked
).generatePassword()
binding.etPassword.setText(password)
binding.etPassword.setSelection(password.length)
}
}

View File

@@ -0,0 +1,63 @@
package com.yogeshpaliyal.keypasscompose.ui.generate
import androidx.lifecycle.ViewModel
import com.yogeshpaliyal.common.utils.PasswordGenerator
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
@HiltViewModel
class GeneratePasswordViewModel @Inject constructor() : ViewModel() {
private val _viewState = MutableStateFlow(GeneratePasswordViewState.Initial)
val viewState = _viewState.asStateFlow()
fun generatePassword() {
val currentViewState = _viewState.value
val passwordGenerator = PasswordGenerator(
length = currentViewState.length,
includeUpperCaseLetters = currentViewState.includeUppercaseLetters,
includeLowerCaseLetters = currentViewState.includeLowercaseLetters,
includeSymbols = currentViewState.includeSymbols,
includeNumbers = currentViewState.includeNumbers,
)
_viewState.update {
val newPassword = passwordGenerator.generatePassword()
it.copy(password = newPassword)
}
}
fun onPasswordLengthSliderChange(value: Float) {
_viewState.update {
it.copy(length = value.toInt())
}
}
fun onUppercaseCheckedChange(checked: Boolean) {
_viewState.update {
it.copy(includeUppercaseLetters = checked)
}
}
fun onLowercaseCheckedChange(checked: Boolean) {
_viewState.update {
it.copy(includeLowercaseLetters = checked)
}
}
fun onNumbersCheckedChange(checked: Boolean) {
_viewState.update {
it.copy(includeNumbers = checked)
}
}
fun onSymbolsCheckedChange(checked: Boolean) {
_viewState.update {
it.copy(includeSymbols = checked)
}
}
}

View File

@@ -0,0 +1,21 @@
package com.yogeshpaliyal.keypasscompose.ui.generate
data class GeneratePasswordViewState(
val length: Int,
val includeUppercaseLetters: Boolean,
val includeLowercaseLetters: Boolean,
val includeSymbols: Boolean,
val includeNumbers: Boolean,
val password: String,
) {
companion object {
val Initial = GeneratePasswordViewState(
length = 10,
includeUppercaseLetters = true,
includeLowercaseLetters = true,
includeSymbols = true,
includeNumbers = true,
password = ""
)
}
}

View File

@@ -0,0 +1,235 @@
package com.yogeshpaliyal.keypasscompose.ui.generate.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.accompanist.themeadapter.material3.Mdc3Theme
import com.yogeshpaliyal.keypasscompose.ui.generate.GeneratePasswordViewState
import com.yogeshpaliyal.keypasscompose.ui.generate.ui.components.CheckboxWithLabel
@Composable
fun GeneratePasswordContent(
viewState: GeneratePasswordViewState,
onCopyPasswordClick: () -> Unit,
onGeneratePasswordClick: () -> Unit,
onPasswordLengthChange: (Float) -> Unit,
onUppercaseCheckedChange: (Boolean) -> Unit,
onLowercaseCheckedChange: (Boolean) -> Unit,
onNumbersCheckedChange: (Boolean) -> Unit,
onSymbolsCheckedChange: (Boolean) -> Unit,
) {
Scaffold(
floatingActionButton = { GeneratePasswordFab(onGeneratePasswordClick) }
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
.background(Color(0xFFEEF0F2))
.padding(16.dp)
) {
FormInputCard(
viewState = viewState,
onCopyPasswordClick = onCopyPasswordClick,
onPasswordLengthChange = onPasswordLengthChange,
onUppercaseCheckedChange = onUppercaseCheckedChange,
onLowercaseCheckedChange = onLowercaseCheckedChange,
onNumbersCheckedChange = onNumbersCheckedChange,
onSymbolsCheckedChange = onSymbolsCheckedChange
)
}
}
}
@Composable
private fun GeneratePasswordFab(onGeneratePasswordClick: () -> Unit) {
FloatingActionButton(
onClick = onGeneratePasswordClick,
shape = RoundedCornerShape(16.dp),
) {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = ""
)
}
}
@Composable
private fun FormInputCard(
viewState: GeneratePasswordViewState,
onCopyPasswordClick: () -> Unit,
onPasswordLengthChange: (Float) -> Unit,
onUppercaseCheckedChange: (Boolean) -> Unit,
onLowercaseCheckedChange: (Boolean) -> Unit,
onNumbersCheckedChange: (Boolean) -> Unit,
onSymbolsCheckedChange: (Boolean) -> Unit
) {
OutlinedCard(
colors = CardDefaults.outlinedCardColors(
containerColor = Color.White
),
shape = RoundedCornerShape(8.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
PasswordTextField(viewState.password, onCopyPasswordClick)
// temporary label until we put slider label on the thumb to display current value.
PasswordLengthInput(viewState.length, onPasswordLengthChange)
UppercaseAlphabetInput(viewState.includeUppercaseLetters, onUppercaseCheckedChange)
LowercaseAlphabetInput(viewState.includeLowercaseLetters, onLowercaseCheckedChange)
NumberInput(viewState.includeNumbers, onNumbersCheckedChange)
SymbolInput(viewState.includeSymbols, onSymbolsCheckedChange)
}
}
}
@Composable
private fun PasswordTextField(
password: String,
onCopyPasswordClick: () -> Unit
) {
OutlinedTextField(
value = password,
onValueChange = {},
modifier = Modifier.fillMaxWidth(),
readOnly = true,
trailingIcon = {
IconButton(
onClick = onCopyPasswordClick
) {
Icon(
imageVector = Icons.Default.ContentCopy,
contentDescription = "Copy password"
)
}
},
label = {
Text(text = "Password")
}
)
}
@Composable
private fun PasswordLengthInput(
length: Int,
onPasswordLengthChange: (Float) -> Unit
) {
Text(text = "Password Length: $length")
Slider(
value = length.toFloat(),
onValueChange = onPasswordLengthChange,
valueRange = 7f..50f,
steps = 43,
)
}
@Composable
private fun UppercaseAlphabetInput(
includeUppercaseLetters: Boolean,
onUppercaseCheckedChange: (Boolean) -> Unit
) {
CheckboxWithLabel(
label = "Uppercase Alphabets",
checked = includeUppercaseLetters,
onCheckedChange = onUppercaseCheckedChange,
)
}
@Composable
private fun LowercaseAlphabetInput(
includeLowercaseLetters: Boolean,
onLowercaseCheckedChange: (Boolean) -> Unit
) {
CheckboxWithLabel(
label = "Lowercase Alphabets",
checked = includeLowercaseLetters,
onCheckedChange = onLowercaseCheckedChange,
)
}
@Composable
private fun NumberInput(
includeNumbers: Boolean,
onNumbersCheckedChange: (Boolean) -> Unit
) {
CheckboxWithLabel(
label = "Numbers",
checked = includeNumbers,
onCheckedChange = onNumbersCheckedChange,
)
}
@Composable
private fun SymbolInput(
includeSymbols: Boolean,
onSymbolsCheckedChange: (Boolean) -> Unit
) {
CheckboxWithLabel(
label = "Symbols",
checked = includeSymbols,
onCheckedChange = onSymbolsCheckedChange,
)
}
@Preview(
name = "Night Mode",
uiMode = Configuration.UI_MODE_NIGHT_YES,
)
@Preview(
name = "Day Mode",
uiMode = Configuration.UI_MODE_NIGHT_NO,
)
@Composable
@Suppress("UnusedPrivateMember", "MagicNumber")
private fun GeneratePasswordContentPreview() {
val viewState = GeneratePasswordViewState.Initial
Mdc3Theme {
Surface {
GeneratePasswordContent(
viewState = viewState,
onGeneratePasswordClick = {},
onCopyPasswordClick = {},
onPasswordLengthChange = {},
onUppercaseCheckedChange = {},
onLowercaseCheckedChange = {},
onNumbersCheckedChange = {},
onSymbolsCheckedChange = {}
)
}
}
}

View File

@@ -0,0 +1,43 @@
package com.yogeshpaliyal.keypasscompose.ui.generate.ui
import android.content.Context
import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import com.yogeshpaliyal.keypasscompose.R
import com.yogeshpaliyal.keypasscompose.ui.generate.GeneratePasswordViewModel
import com.yogeshpaliyal.keypasscompose.ui.generate.ui.utils.copyTextToClipboard
@Composable
fun GeneratePasswordScreen(viewModel: GeneratePasswordViewModel) {
val context = LocalContext.current
// replace collectAsState() with collectAsStateWithLifecycle() when compose version and kotlin version are bumped up.
val viewState by viewModel.viewState.collectAsState()
LaunchedEffect(key1 = Unit) {
viewModel.generatePassword()
}
GeneratePasswordContent(
viewState = viewState,
onGeneratePasswordClick = viewModel::generatePassword,
onCopyPasswordClick = { onCopyPasswordClick(context, viewState.password) },
onPasswordLengthChange = viewModel::onPasswordLengthSliderChange,
onUppercaseCheckedChange = viewModel::onUppercaseCheckedChange,
onLowercaseCheckedChange = viewModel::onLowercaseCheckedChange,
onNumbersCheckedChange = viewModel::onNumbersCheckedChange,
onSymbolsCheckedChange = viewModel::onSymbolsCheckedChange
)
}
private fun onCopyPasswordClick(context: Context, text: String) {
copyTextToClipboard(context = context, text = text, label = "random_password")
Toast
.makeText(context, context.getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT)
.show()
}

View File

@@ -0,0 +1,32 @@
package com.yogeshpaliyal.keypasscompose.ui.generate.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@Composable
fun CheckboxWithLabel(
label: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.clickable { onCheckedChange(!checked) },
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = checked,
onCheckedChange = onCheckedChange,
)
Text(text = label)
}
}

View File

@@ -0,0 +1,17 @@
package com.yogeshpaliyal.keypasscompose.ui.generate.ui.utils
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import androidx.appcompat.app.AppCompatActivity
fun copyTextToClipboard(
context: Context,
text: String,
label: String
) {
val clipboard =
context.getSystemService(AppCompatActivity.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText(label, text)
clipboard.setPrimaryClip(clip)
}

View File

@@ -1,104 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.generate.GeneratePasswordActivity">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/grid_0_5"
android:paddingVertical="@dimen/grid_0_5">
<com.yogeshpaliyal.keypasscompose.custom_views.MaskedCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="@dimen/grid_1"
android:paddingVertical="@dimen/grid_1">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/password"
app:endIconDrawable="@drawable/ic_twotone_content_copy_24"
app:endIconMode="custom"
app:layout_constraintEnd_toEndOf="@id/endGuideline"
app:layout_constraintStart_toStartOf="@id/startGuideline"
app:layout_constraintTop_toTopOf="@id/topGuideline">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_marginTop="@dimen/grid_2"
android:layout_height="wrap_content"
android:text="@string/password_length"/>
<com.google.android.material.slider.Slider
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:valueFrom="7"
android:valueTo="50"
android:stepSize="1"
android:id="@+id/sliderPasswordLength"/>
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/cbCapAlphabets"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/uppercase_alphabets" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/cbLowerAlphabets"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/lowercase_alphabets" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/cbNumbers"
android:layout_width="match_parent"
android:checked="true"
android:layout_height="wrap_content"
android:text="@string/numbers" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/cbSymbols"
android:layout_width="match_parent"
android:checked="true"
android:layout_height="wrap_content"
android:text="@string/symbols" />
</LinearLayout>
</com.yogeshpaliyal.keypasscompose.custom_views.MaskedCardView>
</androidx.core.widget.NestedScrollView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/grid_3"
app:fabSize="normal"
android:src="@drawable/ic_baseline_refresh_24"
android:id="@+id/btnRefresh"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>