Android actual combat: mobile note app (III)

Time:2022-5-8

Android actual combat: mobile note app (I)

Android actual combat: mobile note app (II)

Follow the previous content


Create addeditnote module

Android actual combat: mobile note app (III)

image.png

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.

Android actual combat: mobile note app (III)

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)

Android actual combat: mobile note app (III)

image.png

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

Android actual combat: mobile note app (III)

image.png

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}&noteColor={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}&noteColor={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

Android actual combat: mobile note app (III)

image.png

Navigation area

Android actual combat: mobile note app (III)

image.png

Full source address:https://github.com/gun-ctrl/NoteApp

Recommended Today

3. How to design the check-in system of ten million days?

Suppose there is a million check-in system, which records the user‘s check-in record, signed record 1 and not signed record 0. If we use redis string storage, 1000000 * 365 keys will be saved a year, which will occupy a lot of memory. In order to solve this problem, redis provides a bitmap data structure, […]