Android actual combat: mobile note app (I)
Android actual combat: mobile note app (II)
Follow the previous content
Create addeditnote module

Encapsulation of trigger events in addnote interface
Encapsulate the trigger events of input title, whether the title bar has input focus, input content, whether the content bar has input focus, change color and save into a kind of trigger event.
sealed class AddEditNoteEvent{
data class EnteredTitle(val value:String):AddEditNoteEvent()
data class ChangeTitleFocus(val focusState: FocusState):AddEditNoteEvent()
data class EnteredContent(val value: String):AddEditNoteEvent()
data class ChangeContentFocus(val focusState: FocusState):AddEditNoteEvent()
data class ChangeColor(val color:Int):AddEditNoteEvent()
object SaveNote:AddEditNoteEvent()
}
Encapsulation of text class
Ishintvisible: determines whether the hint is visible
data class NoteTextFieldState(
val text:String = "",
val hint:String = "",
val isHintVisible:Boolean = true
)
AddEditNoteViewModel
Savedstatehandle: the argument parameter passed from the navigation is acceptable
Uievent: the event fed back to the UI interface, as shown in the figure below. When saving fails, a prompt will pop up. When saving succeeds, it will navigate back to the previous page.
There is no need to introduce the rest, which is similar to the previous noteviewmodel.
Why Val_ Eventflow = mutablesharedflow < uievent > () use sharedflow here:Our requirement is that we only want the snackBar to pop up once. If we use state, the snackBar will pop up again when we rotate the screen.
Tips:Pay attention to the two attributes of hint and text in notetextfieldstate. Xiaobian accidentally knocked one of them into text, resulting in a bug later – the hint content in the title needs to be deleted manually every time you click + and will not disappear automatically after clicking, which gives me a headache!!! I looked for it for a long time and finally found it by accident.
image.png
@HiltViewModel
class AddEditNoteViewModel @Inject constructor(
private val noteUseCases: NoteUseCases,
savedStateHandle: SavedStateHandle
):ViewModel() {
private val _noteTitle = mutableStateOf(NoteTextFieldState(
hint = "Enter title..."
))
val noteTitle:State<NoteTextFieldState> = _noteTitle
private val _noteContent = mutableStateOf(NoteTextFieldState(
hint = "Enter some content"
))
val noteContent:State<NoteTextFieldState> = _noteContent
private val _noteColor = mutableStateOf(Note.noteColors.random().toArgb())
val noteColor:State<Int> = _noteColor
private val _eventFlow = MutableSharedFlow<UiEvent>()
val eventFlow = _eventFlow.asSharedFlow()
private var currentNoteId:Int ?= null
init {
savedStateHandle.get<Int>("noteId")?.let {noteId ->
if (noteId!=-1){
viewModelScope.launch {
noteUseCases.getNote(noteId)?.also {note ->
currentNoteId = note.id
_noteTitle.value = noteTitle.value.copy(
text = note.title,
isHintVisible = false
)
_noteContent.value = noteContent.value.copy(
text = note.content,
isHintVisible = false
)
_noteColor.value = note.color
}
}
}
}
}
fun onEvent(event:AddEditNoteEvent){
when(event){
is AddEditNoteEvent.EnteredTitle ->{
_noteTitle.value = noteTitle.value.copy(
text = event.value
)
}
is AddEditNoteEvent.ChangeTitleFocus ->{
_noteTitle.value = noteTitle.value.copy(
isHintVisible = !event.focusState.isFocused &&
noteTitle.value.text.isBlank()
)
}
is AddEditNoteEvent.EnteredContent ->{
_noteContent.value = noteContent.value.copy(
text = event.value
)
}
is AddEditNoteEvent.ChangeContentFocus -> {
_noteContent.value = noteContent.value.copy(
isHintVisible = !event.focusState.isFocused &&
noteContent.value.text.isBlank()
)
}
is AddEditNoteEvent.ChangeColor -> {
_noteColor.value = event.color
}
is AddEditNoteEvent.SaveNote ->{
viewModelScope.launch {
try {
noteUseCases.addNote(
Note(
title = noteTitle.value.text,
content = noteContent.value.text,
timestamp = System.currentTimeMillis(),
color = noteColor.value,
id = currentNoteId
)
)
_eventFlow.emit(UiEvent.SaveNote)
}catch (e:InvalidNoteException){
_eventFlow.emit(
UiEvent.ShowSnackBar(
message = e.message?:"Couldn't save note"
))
}
}
}
}
}
sealed class UiEvent{
data class ShowSnackBar(val message:String):UiEvent()
object SaveNote:UiEvent()
}
}
Create addnote page UI
Let’s analyze the components of the addnote page:
First, there are five circular color dials at the top of the screen. If you select that color, the text background will become that color, and the color dials will also have a black border to indicate the selected state. The implementation of this is very simple. You only need to traverse note Create a box () from the color Resources prepared by KT, and each click will trigger ViewModel Onevent (addeditnoteevent. Changecolor (colorint)), compared with the five color discs, the same one is selected.
Then there is the title and content input box, which are almost the same, but occupy different space. There will be hint when there is no content, and hint will disappear when you click the arc or there is content. The implementation is also simple: basictextfield and text. When there is no content, text is displayed, and the content in text is hint.
Finally, the save hover button in the lower right corner, so we need scaffold to execute ViewModel when clicking onEvent(AddEditNoteEvent.SaveNote)

Custom text style transparenthinttextfield (encapsulated input box)
When adding a new note, hint will be displayed and hidden when there is input. The text input content has been encapsulated earlier with ishintvisible as the parameter. If ishintvisible is true, text () will be displayed; otherwise, basictextfield () will be displayed.
@Composable
fun TransparentHintTextField(
text:String,
hint:String,
modifier: Modifier = Modifier,
isHintVisible:Boolean = true,
onValueChange:(String) ->Unit,
textStyle: TextStyle = TextStyle(),
singleLine:Boolean = false,
onFocusChange:(FocusState) -> Unit
) {
Box(
modifier = modifier
) {
BasicTextField(
value = text,
onValueChange = onValueChange,
singleLine = singleLine,
textStyle = textStyle,
modifier = Modifier
.fillMaxWidth()
.onFocusChanged {
onFocusChange(it)
}
)
if (isHintVisible){
Text(text = hint, style = textStyle, color = Color.DarkGray)
}
}
}
Create addeditnotescreen as the compose component collection of addnote
noteBackgroundAnimatable:Text background color, and there is animation effect when switching colors.
Row(…) Create a color wheel at the top and realize the animation switching effect.
/**
*@Description
*@Author PC
*@QQ 1578684787
*/
@Composable
fun AddEditNoteScreen(
navController: NavController,
noteColor:Int,
viewModel: AddEditNoteViewModel = hiltViewModel()
) {
val titleState = viewModel.noteTitle.value
val contentState = viewModel.noteContent.value
val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
//Background animation with switching effect
val noteBackgroundAnimatable = remember{
Animatable(
Color(if (noteColor != -1) noteColor else viewModel.noteColor.value)
)
}
LaunchedEffect(key1 = true){
viewModel.eventFlow.collectLatest { event ->
when(event){
is AddEditNoteViewModel.UiEvent.ShowSnackBar ->{
scaffoldState.snackbarHostState.showSnackbar(
message = event.message
)
}
is AddEditNoteViewModel.UiEvent.SaveNote ->{
navController.navigateUp()
}
}
}
}
Scaffold(
floatingActionButton ={
FloatingActionButton(
onClick = {
viewModel.onEvent(AddEditNoteEvent.SaveNote)
},
backgroundColor =MaterialTheme.colors.primary
) {
Icon(imageVector = Icons.Default.Save, contentDescription = "Save note")
}
},
scaffoldState = scaffoldState
) {
Column(
modifier = Modifier
.fillMaxSize()
.background(noteBackgroundAnimatable.value)
.padding(16.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
//Draw top color selection button
Note.noteColors.forEach {color ->
val colorInt = color.toArgb()
Box(
modifier = Modifier
.size(50.dp)
.shadow(15.dp, CircleShape)
.clip(CircleShape)
.background(color)
.border(
width = 3.dp,
color = if (viewModel.noteColor.value == colorInt) {
Color.Black
} else {
Color.Transparent
},
shape = CircleShape
)
.clickable {
scope.launch {
noteBackgroundAnimatable.animateTo(
targetValue = Color(colorInt),
animationSpec = tween(
500
)
)
}
//Change the text color of note
viewModel.onEvent(AddEditNoteEvent.ChangeColor(colorInt))
}
)
}
}
Spacer(modifier = Modifier.height(16.dp))
TransparentHintTextField(
text = titleState.text,
hint = titleState.hint,
isHintVisible = titleState.isHintVisible,
onValueChange = {
viewModel.onEvent(AddEditNoteEvent.EnteredTitle(it))
},
onFocusChange ={
viewModel.onEvent(AddEditNoteEvent.ChangeTitleFocus(it))
},
textStyle =MaterialTheme.typography.h5,
singleLine = true
)
Spacer(modifier = Modifier.height(16.dp))
TransparentHintTextField(
text = contentState.text,
hint = contentState.hint,
isHintVisible = contentState.isHintVisible,
onValueChange = {
viewModel.onEvent(AddEditNoteEvent.EnteredContent(it))
},
onFocusChange = {
viewModel.onEvent(AddEditNoteEvent.ChangeContentFocus(it))
},
textStyle = MaterialTheme.typography.body1,
modifier = Modifier.fillMaxHeight()
)
}
}
}
Add navigation
Encapsulate navigation path

Encapsulating the path of two pages with sealed classes not only enhances the readability of the code, but also enhances the reusability of the code.
sealed class Screen(val route:String){
object NotesScreen:Screen("notes_screen")
object AddEditNoteScreen:Screen("add_edit_note_screen")
}
Addeditnotescreen needs to receive two parameters, noteid and notecolor. The splicing method of compose path parameters is similar to that of network request parameters
“?noteId={noteId}¬eColor={noteColor}”
I believe you have seen a lot of – 1 in the previous code. Yes, it is the value of DefaultValue here, which is passed when there is no data. Of course, the value of DefaultValue is not unique. According to the type type defined by yourself, the int (navtype. Inttype) type used here can also define other types, such as string.
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NoteAppTheme {
Surface(
color = MaterialTheme.colors.background
) {
val navController = rememberNavController()
NavHost(
navController =navController,
startDestination = Screen.NotesScreen.route
){
composable(route = Screen.NotesScreen.route){
NotesScreen(navController = navController)
}
composable(route = Screen.AddEditNoteScreen.route +
"?noteId={noteId}¬eColor={noteColor}",
arguments = listOf(
navArgument(
name ="noteId"
){
type = NavType.IntType
defaultValue = -1
},
navArgument(
name = "noteColor"
){
type = NavType.IntType
defaultValue = -1
}
)
){
val color = it.arguments?.getInt("noteColor")?:-1
AddEditNoteScreen(
navController = navController,
noteColor = color
)
}
}
}
}
}
}
}
Navigation area

Navigation area
