Android Jetpack Compose——一个简单的笔记APP

news2024/9/23 9:30:00

一个简单的笔记APP

  • 简述
  • 效果视频
  • Hilt提供依赖对象
  • Room CRUD
    • 接口实现类
    • 内容封装
      • 查询所有
      • 查询
      • 删除
      • 插入
  • 笔记内容
    • 效果图
    • ViewModel
      • 依赖注入
      • 数据初始化
      • 数据处理
    • View
      • 标题栏
      • 排序组件
      • 笔记列表
  • 新建&编辑笔记
    • 效果图
    • ViewModel
      • 依赖注入
      • 初始化
      • 数据处理
    • View
      • 背景颜色条
      • 标题
      • 保存笔记
  • 路由导航
    • 建立导航结点
    • 绘制导航地图
    • 入口
  • 总结
  • Gitee链接

简述

此项目功能较为简单,基本就是使用Room数据库实现CRUD,但是此项目实现了一个干净的架构,项目使用MVVM架构进行设计,每一个模块的职责划分清晰,功能明确,没有冗余的代码。其中涉及了Hilt依赖注入,对于数据库的的操作,使用接口实现类进行获取,然后将实现类的CRUD操作封装在一个数据类中,最后通过Hilt自动注入依赖,供外部调用。
此项目原创来源于YouTube的一位创作者Philipp Lackner

效果视频

Hilt提供依赖对象

有关Hilt依赖注入的文章可以参考其他文章——Hilt依赖注入,此处就不在进行多余阐述,providerNoteDataBase提供了数据对象,providerNoteRepository提供了数据库接口实现类对象,providerNoteUseCase提供了数据库具体操作对象;这三个对象是一环扣一环,上一个为下一个提供对象,最后一个提供外部使用,这是Hilt依赖注入的一个便利,无需我们手动去一个个绑定,Hilt自动就帮我完成了这部分

/**
 * Module:用来管理所有需要提供的对象
 * Provides:用来提供对象
 * InstallIn:用来将模块装载到对应作用域饿,此处是单例
 * 自动绑定到"SingletonComponent::class"上*/
@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    /**
     * 提供数据库对象*/
    @Provides
    @Singleton
    fun providerNoteDataBase(application: Application):NoteDatabase{
        return Room.databaseBuilder(
            application,
            NoteDatabase::class.java,
            NoteDatabase.DATABASE_NAME
        ).build()
    }


    /**
     * 提供数据库Dao类操作对象*/
    @Provides
    @Singleton
    fun providerNoteRepository(db:NoteDatabase):NoteRepository{
        return NoteRepositoryImpl(db.noteDao)
    }

    /**
     * 提供数据库具体操作内容对象*/
    @Provides
    @Singleton
    fun providerNoteUseCase(repository: NoteRepository):NoteUseCase{
        return NoteUseCase(
            GetNotes(repository),
            GetNote(repository),
            DeleteNote(repository),
            InsertNote(repository)
        )
    }
}

Room CRUD

接口实现类

其中NoteRepository是一个接口类,提供了数据库的相关操作方法,然后NoteRepositoryImpl实现此接口,并通过数据库实例完成接口实现

class NoteRepositoryImpl(private val dao: NoteDao):NoteRepository {
    override fun getNotes(): Flow<List<NoteBean>> {
        return dao.queryAll()
    }

    override suspend fun getNote(id: Int): NoteBean? {
        return dao.queryById(id)
    }

    override suspend fun insertNote(bean: NoteBean) {
        dao.insertNote(bean)
    }

    override suspend fun deleteNote(bean: NoteBean) {
        dao.deleteNote(bean)
    }
}

内容封装

将数据库的CRUD操作封装在一个数据类中,最后外部通过调用此数据类完成对数据库的操作

data class NoteUseCase(
    val getNotes: GetNotes,
    val getNote: GetNote,
    val deleteNote: DeleteNote,
    val insertNote: InsertNote
)

查询所有

使用接口实现类提供的数据,并List数据进行排序处理,此处使用的是invoke函数,此函数的作用是,外部调用此函数就像类的构造函数一般,无需对类进行初始化,然后在调用此方法,可以直接GetNotes(param),就相当于调用了invoke函数

class GetNotes(private val repository:NoteRepository) {
    operator fun invoke(noteType: NoteType = NoteType.Date(NoteOrder.Descending)): Flow<List<NoteBean>> {
        return  repository.getNotes().map { notes ->
            when(noteType.noteOrder){
                is NoteOrder.Ascending->{
                    when(noteType){
                        is NoteType.Title-> notes.sortedBy { it.title.lowercase() }
                        is NoteType.Date-> notes.sortedBy { it.time }
                        is NoteType.Color-> notes.sortedBy { it.color }
                    }
                }
                is NoteOrder.Descending->{
                    when(noteType){
                        is NoteType.Title-> notes.sortedByDescending { it.title.lowercase() }
                        is NoteType.Date-> notes.sortedByDescending { it.time }
                        is NoteType.Color-> notes.sortedByDescending { it.color }
                    }
                }
            }
        }
    }
}

查询

数据库操作可以划分为耗时操作,所有使用suspend函数标记进行挂起,外部调用时就必须在协程中完成

class GetNote(private val repository: NoteRepository) {
    suspend operator fun invoke(id:Int):NoteBean?{
        return repository.getNote(id)
    }
}

删除

class DeleteNote(private val repository: NoteRepository) {
    suspend operator fun invoke(noteBean: NoteBean){
        repository.deleteNote(noteBean)
    }
}

插入

此处对数据库进行了插入操作,在此之前对插入的数据进行判空处理,如果为空,则通过自定义的一个异常类抛出此异常

class InsertNote(private val repository: NoteRepository) {

    @Throws(InvalidNoteException::class)
    suspend operator fun invoke(bean: NoteBean){
        if (bean.title.isBlank()){
            throw InvalidNoteException("标题不能为空!")
        }
        if (bean.content.isBlank()){
            throw InvalidNoteException("内容不能为空!")
        }
        repository.insertNote(bean)
    }
}

笔记内容

此界面完成的功能包括:显示所有笔记内容、删除笔记、撤回删除笔记、对笔记进行排序处理、跳转至创建笔记页面

效果图

ViewModel

开头已经介绍,此项目使用的是MVVM架构,所以VM类必不可少,VM类的职责为承接Model和View之间的桥梁作用,所有的交互或者数据处理放到VM类进行处理,View组件绑定VM中有状态的变量,一旦VM进行数据处理,外部相对应的组件就会进行重组

依赖注入

使用HiltViewModel注解标注此VM类,代表此类中要使用Hilt提供的依赖对象,然后@Inject注解,获取NoteUseCase对象,此对象是Hilt自动注入的,在ModuleProvider可以看到实际提供的对象

@HiltViewModel
class NotesViewModel @Inject constructor(private val noteUseCase: NoteUseCase):ViewModel(){...}

数据初始化

定义一个持有状态的变量,供外部View组件使用,其中NotesState数据类包括笔记List、笔记排序类型、是否显示排序组件三个成员变量;recentlyDeleteNote用于存储最近被删除的笔记内容,方便撤回删除的笔记时进行数据库插入操作;Job是用来进行协程操作的,它是CoroutineContext的一个子类

/**
     * 笔记内容状态管理
     * 所有笔记内容、排序方式、是否显示排序组件*/
    private val _state = mutableStateOf(NotesState())
    val state: State<NotesState> = _state

    /**
     * 存储最近被删除的笔记*/
    private var recentlyDeleteNote:NoteBean? = null

    private var getNotesJob: Job? = null

然后对数据进行初始化

 init {
        getNotes(NoteType.Date(NoteOrder.Descending))
    }

此处有一个重点,由于数据库接口实现类是用Flow<List<xxxBean>>包裹的流数据,并且Room数据库有一个特点,一旦数据库内容发生改变,就会重新派发通知给实现query的内容,此处通过Flow接收通知,并在重组作用域中重新给拥有状态的变量进行赋值,从而通知外部View绑定的列表数据进行重组,此处使用的Kotlin的高阶函数copy完成浅拷贝

 /**
     * 这是因为 SQLite 数据库的内容更新通知功能是以表 (Table) 数据为单位,而不是以行 (Row) 数据为单位,因此只要是表中的数据有更新,
     * 它就触发内容更新通知。Room 不知道表中有更新的数据是哪一个,因此它会重新触发 DAO 中定义的 query 操作。
     * 您可以使用 Flow 的操作符,比如 distinctUntilChanged 来确保只有在当您关心的数据有更新时才会收到通知
    */
    private fun getNotes(type: NoteType){
        getNotesJob?.cancel()
        getNotesJob = noteUseCase.getNotes(type).onEach {
            notes->
            /*room表中数据发生变化,此处会重新被执行*/
            _state.value = state.value.copy(
                notes = notes,
                noteType = type
            )
        }.launchIn(viewModelScope)
    }

数据处理

外部View组件的点击事件进行数据处理,通过调用VM的onEvent方法进行处理;NotesEvent是一个密封类,封装了几个操作类;下面实现了笔记排序处理笔记删除处理笔记撤回删除处理显示\隐藏排序组件;在下面我们直接使用Hilt自动注入的依赖对象进行处理,无需进行手动注入完成对象实例化

    fun onEvent(event: NotesEvent){
        when(event){
            /**
             * 对笔记内容进行排序,如果当前排序类型和方式一样则不进行任何操作
             * 否则重新根据排序方式进行排序*/
            is NotesEvent.Type ->{
                if (state.value.noteType == event.noteType &&
                    state.value.noteType.noteOrder == event.noteType.noteOrder){
                    return
                }
                getNotes(event.noteType)
            }
            /**
             * 删除笔记操作,然后将最近被删除的笔记赋值给一个临时变量进行暂时存储*/
            is NotesEvent.Delete ->{
                viewModelScope.launch {
                    noteUseCase.deleteNote(event.bean)
                    recentlyDeleteNote = event.bean
                }
            }
            /**
             * 撤回最近被删除的笔记,从临时变量中*/
            is NotesEvent.RestoreNote ->{
               viewModelScope.launch {
                   noteUseCase.insertNote(recentlyDeleteNote ?: return@launch)
                   recentlyDeleteNote = null
               }
            }
            /**
             * 显示/隐藏排序组件*/
            is NotesEvent.ToggleOrderSection ->{
                _state.value = state.value.copy(
                    isOrderSectionVisible = !state.value.isOrderSectionVisible
                )
            }
        }
    }

View

View的实现就较为简单,完成ViewModel类实例化,获取持有状态的变量的数据,然后绑定到相应组件上,并将需要通过交互处理的数据传递给VM进行处理

@Composable
fun ShowNotePage(navController: NavController,viewModel: NotesViewModel = hiltViewModel()){
    val state = viewModel.state.value
    val scaffoldState = rememberScaffoldState()
    val scope = rememberCoroutineScope()
    ...
    }

顶部一个标题栏,然后通过按钮对排序组件进行显示和隐藏操作;右下方有一个FAB按钮,然后删除笔记时会弹出SnackBar,最后就是笔记内容列表,我们使用Scaffold脚手架完成FABSnackBar的填充

标题栏

通过监听Icon的点击事件,在其中将需要执行的内容交给VM执行,在VM中改变组件显示的Boolean

     Row(
                verticalAlignment = Alignment.CenterVertically,
                modifier = Modifier.fillMaxWidth().padding(top = 10.dp)
            ) {
                Text(text = "NoteApp", style = MaterialTheme.typography.h4, color = NoteTheme.colors.primary)
                Spacer(modifier = Modifier.weight(1f))
                Icon(
                    imageVector = Icons.Default.Sort,
                    contentDescription = "排序",
                    tint = NoteTheme.colors.primary,
                    modifier = Modifier.clickable {
                        viewModel.onEvent(NotesEvent.ToggleOrderSection)
                    }
                )
            }

排序组件

排序组件使用AnimatedVisibility组件进行包裹,通过绑定VM显示/隐藏的Boolean值完成切换,具体的排序组件代码就不展示了,较为简单;通过状态提升,将排序组件的点击事件回调给外部,无需在内容在进行状态监听,然后在交托给VM类进行相应处理

   AnimatedVisibility(
                visible = state.isOrderSectionVisible,
                enter = fadeIn() + slideInVertically(),
                exit = fadeOut() + slideOutVertically()
            ) {
                OrderSelect(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(vertical = 16.dp),
                    noteType = state.noteType)
                {
                    viewModel.onEvent(NotesEvent.Type(it))
                }
            }

笔记列表

通过回调将笔记删除事件传递给父布局,然后在删除删除执行之后,弹出SnackBar,并对尾部添加撤回按钮,在撤回按钮中又进行笔记撤回删除操作,也就是重新插入

   NoteList(navController,notes = state.notes){
                //笔记删除事件
                viewModel.onEvent(NotesEvent.Delete(it))
                scope.launch {
                    val result = scaffoldState.snackbarHostState.showSnackbar(
                        message = "笔记已删除",
                        actionLabel = "撤回")
                    if (result == SnackbarResult.ActionPerformed){
                        viewModel.onEvent(NotesEvent.RestoreNote)
                    }
                }
            }

使用LazyColumn展示笔记列表,并在笔记点击事件中进行导航,因为是从已存在的笔记进行导航,所以需要传递一些参数

@Composable
fun NoteList(navController: NavController,notes:List<NoteBean>, onDeleteClick: (NoteBean) -> Unit){
    LazyColumn(modifier = Modifier.fillMaxSize()){
        items(notes.size){
            NoteItem(bean = notes[it], onDeleteClick = { onDeleteClick(notes[it])}, modifier = Modifier.fillMaxWidth().wrapContentHeight().clickable {
                ///跳转笔记编辑界面
                navController.navigate(NavigationItem.EditNote.route+"?noteId=${notes[it].id}&noteColor=${notes[it].color}")
            })
            if (it < notes.size - 1){
                Spacer(modifier = Modifier.height(16.dp))
            }
        }
    }
}

单个笔记Item的布局较为简单,在左上角对背景进行了一个折角处理,首先在画布上画出对应缺角路线,然后就缺角部分进行圆角和颜色处理;所以处理回调给外部,使其成为一个无状态组件

@Composable
fun NoteItem(
    bean: NoteBean,
    modifier: Modifier = Modifier,
    cornerRadius: Dp = 10.dp,
    cutCornerSize: Dp = 30.dp,
    onDeleteClick: () -> Unit)
{
    Box(modifier = modifier){
        Canvas(modifier = Modifier.matchParentSize()){
            /**
             * 绘制笔记路径*/
            val clipPath = Path().apply {
                lineTo(size.width - cutCornerSize.toPx(), 0f)//上
                lineTo(size.width, cutCornerSize.toPx())//右
                lineTo(size.width, size.height)//下
                lineTo(0f, size.height)//左
                close()
            }

            /**
             * 对右上角圆角进行折叠处理*/
            clipPath(clipPath) {
                drawRoundRect(
                    color = Color(bean.color),
                    size = size,
                    cornerRadius = CornerRadius(cornerRadius.toPx())
                )
                drawRoundRect(
                    color = Color(
                        ColorUtils.blendARGB(bean.color, 0x000000, 0.2f)
                    ),
                    topLeft = Offset(size.width - cutCornerSize.toPx(), -100f),
                    size = Size(cutCornerSize.toPx() + 100f, cutCornerSize.toPx() + 100f),
                    cornerRadius = CornerRadius(cornerRadius.toPx())
                )
            }
        }

        Column(
            modifier = Modifier
             .fillMaxSize()
             .padding(top = 16.dp, start = 16.dp, bottom = 16.dp,end = 32.dp),
            verticalArrangement = Arrangement.Center
        )
        {
            Text(
                text = bean.title,
                style = MaterialTheme.typography.h6,
                color = MaterialTheme.colors.onSurface,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis,
                modifier = Modifier.fillMaxWidth()
            )

            Spacer(modifier = Modifier.height(8.dp))

            Text(
                text = bean.content,
                style = MaterialTheme.typography.body1,
                color = MaterialTheme.colors.onSurface,
                maxLines = 10,
                overflow = TextOverflow.Ellipsis,
                modifier = Modifier.fillMaxWidth()
            )
        }

        Icon(
            imageVector = Icons.Default.Delete,
            contentDescription = "删除",
            tint = NoteTheme.colors.onSurface,
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .padding(8.dp)
                .clickable {
                    onDeleteClick()
                }
        )

    }
}

新建&编辑笔记

笔记编辑页面分为新建笔记和编辑笔记两种状态,从原有笔记页面进行跳转,则展示原有笔记内容;反之,显示空内容。

效果图

ViewModel

依赖注入

此处与上述的ViewModel依赖注入一致,多了一个SavedStateHandle对象,此类用于获取导航路由传递的参数,就不需要去通过函数传递和获取了

@HiltViewModel
class EditNoteViewModel @Inject constructor(private val noteUseCase: NoteUseCase,savedStateHandle: SavedStateHandle):ViewModel() {...}

初始化

定义三个持有状态的变量,分别对应编辑笔记页面的标题、内容、背景颜色

    /**
     * 对标题输入内容进行状态管理
     * text:标题输入框输入的内容
     * hint:标题输入框默认显示内容
     * isHintVisible:标题输入框是否显示hint内容*/
    private val _noteTitle = mutableStateOf(EditNoteTextFieldState(
        hint = "输入笔记标题..."
    ))
    val noteTitle: State<EditNoteTextFieldState> = _noteTitle

    private val _noteContent = mutableStateOf(EditNoteTextFieldState(
        hint = "输入笔记内容..."
    ))
    val noteContent: State<EditNoteTextFieldState> = _noteContent

    /**
     * 对当前笔记的背景颜色进行状态管理
     * 默认是从颜色列表中随机取一个颜色*/
    private val _noteColor = mutableStateOf(NoteBean.noteColor.random().toArgb())
    val noteColor: State<Int> = _noteColor

    /**
     * 对Ui界面的保存笔记事件和笔记内容是否为空事件进行管理
     * 然后将具体内容传递到Ui界面*/
    private val _eventFlow = MutableSharedFlow<EditNoteUiEvent>()
    val eventFlow = _eventFlow.asSharedFlow()

    /**
     * 当前的笔记的id,如果从指定笔记跳转,则此值不为空,若是创建一个新的笔记进行跳转,此值为-1*/
    private var currentId:Int? = null

在初始化中,使用savedStateHandle获取导航传递的参数值,-1为默认值,如果不等于-1则代表数据不为空,是从已经存在的笔记内容进行导航,从而将数据进行取出,并赋值给持有状态的变量

    /**
     * 对笔记内容进行初始化,从导航路由中获取"noteid"的值,然后在根据此值从数据库中进行查询
     * 若不为空,则刷新当前值(从指定笔记进行路由)
     * 否则,为默认值(创建一个新的笔记)*/
    init {
        savedStateHandle.get<Int>("noteId")?.let { noteId ->
            if (noteId != -1) {
                viewModelScope.launch {
                    noteUseCase.getNote(noteId)?.also { note->
                        currentId = noteId
                        _noteColor.value = note.color
                        _noteTitle.value = noteTitle.value.copy(
                            text = note.title,
                        )
                        _noteContent.value = noteContent.value.copy(
                            text = note.content,
                        )
                    }
                }
            }
        }
    }

数据处理

同样EditNoteEvent是一个密封类,包裹了下述几个类,当标题、内容、背景颜色改变时,在下述进行更改,然后在保存笔记处理中,读取当前VM中对应的值插入数据库中,在保存中如若触发异常通过Flow进行派发通知,外部界面通过接收通知,做出对应处理

    fun onEvent(event: EditNoteEvent){
        when(event){
            /**
             * 改变笔记标题的内容
             * 因为采用MVVM模式,笔记Ui界面的标题绑定VM的状态管理变量,然后输入框通过输入字符,并监听输入事件
             * 不断执行此事件,然后在此事件进行VM标题内容改变,笔记Ui界面的标题内容自动刷新*/
            is EditNoteEvent.EnterTitle -> {
                _noteTitle.value = noteTitle.value.copy(
                 text = event.title
                )
            }
            is EditNoteEvent.EnterContent -> {
                _noteContent.value = noteContent.value.copy(
                    text = event.content
                )
            }
            is EditNoteEvent.ChangeColor ->{
                _noteColor.value = event.color
            }
            /**
             * 保存当前笔记内容,将内容插入数据库中
             * 若某一内容为空,触发"InvalidNoteException"异常,则通过"eventFlow"传递到Ui界面,然后通过snack进行显示*/
            is EditNoteEvent.SaveNote ->{
                viewModelScope.launch {
                    try {
                        noteUseCase.insertNote(
                            NoteBean(
                                id = currentId,
                                color = noteColor.value,
                                title = noteTitle.value.text,
                                content = noteContent.value.text,
                                time = System.currentTimeMillis())
                        )
                        _eventFlow.emit(EditNoteUiEvent.SaveNoteUi)
                    }catch (e:InvalidNoteException){
                        _eventFlow.emit(EditNoteUiEvent.ShowSnackBar(e.message ?: "笔记保存失败!"))
                    }
                }
            }
        }
    }

View

新建&编辑笔记页面布局较为简单,顶部背景颜色条、笔记标题、笔记内容、FAB、SnackBar

@Composable
fun EditNotePage(
    navHostController: NavHostController,
    color:Int,
    viewModel: EditNoteViewModel = hiltViewModel()
){
    val title = viewModel.noteTitle.value//标题状态管理
    val content = viewModel.noteContent.value//内容状态管理
    val scope = rememberCoroutineScope()//协程
    val scaffoldState = rememberScaffoldState()//脚手架状态
    ...
    }

背景颜色条

对于初始化背景颜色,如果是编辑笔记从获取原本颜色,否则在VM中获取一个随机背景颜色

   val noteBackground = remember {
        Animatable(
            Color(
                if (color != -1)
                    color
                else
                    viewModel.noteColor.value
            )
        )
    }

颜色条具体布局代码就不展示了,一个LazyRow中展示颜色列表数据,然后每个颜色块Item裁剪成圆形即可,被选中颜色块有一个黑色圆形边框包裹,就通过上述获取的颜色与颜色列表进行比对,如果相等则边框显示一个颜色否则显示透明颜色即可,最后将点击事件暴露给外部;外部在协程中进行处理,颜色变化使用一个动画进行切换,随机通知VM进行对应处理

    ColorList(colors = NoteBean.noteColor,viewModel.noteColor.value){ color->
                scope.launch {
                    noteBackground.animateTo(
                        targetValue = color,
                        animationSpec = tween(500)
                    )
                    viewModel.onEvent(EditNoteEvent.ChangeColor(color.toArgb()))
                }
            }

标题

标题和内容一样,此处以标题为例,初始内容绑定VM的数据,使用placeholder展示Hint内容,并通过将部分颜色改为透明,以突出背景颜色为主,因为TextField组件默认带有边框、背景等颜色

       TextField(
                value = title.text,
                textStyle = MaterialTheme.typography.h5,
                singleLine = true,
                onValueChange = { viewModel.onEvent(EditNoteEvent.EnterTitle(it)) },
                placeholder = { Text(text = title.hint, color = NoteTheme.colors.textColor) },
                colors = TextFieldDefaults.textFieldColors(
                    backgroundColor = Color.Transparent,
                    disabledIndicatorColor = Color.Transparent,
                    unfocusedIndicatorColor = Color.Transparent,
                    focusedIndicatorColor = Color.Transparent,
                    errorIndicatorColor = Color.Transparent,
                    cursorColor = Color.Black,//光标颜色

                ),
                modifier = Modifier.fillMaxWidth()

            )

保存笔记

保存笔记通过FAB按钮完成,将保存笔记意图传递给VM层

     FloatingActionButton(
                backgroundColor = NoteTheme.colors.onBackground,
                onClick = { viewModel.onEvent(EditNoteEvent.SaveNote) }
            ) {
                Icon(
                    imageVector = Icons.Default.Save,
                    contentDescription = "保存",
                    tint = NoteTheme.colors.textColor
                )
            }

在ViewModel层中的保存笔记方法中,对保存状态进行一个事件流监听,然后将对应状态进行派发;外部通过LaunchedEffect在协程中进行处理,并进行Flow流收集,并根据内容做出对应处理,如果有异常,则通过SnackBar进行显示;反之正常,则返回导航上一级

    LaunchedEffect(key1 = true){
        viewModel.eventFlow.collectLatest {
            when(it){
                is EditNoteUiEvent.ShowSnackBar -> {
                    scaffoldState.snackbarHostState.showSnackbar(it.message)
                }
                is EditNoteUiEvent.SaveNoteUi -> {
                    navHostController.navigateUp()
                }
            }
        }
    }

路由导航

建立导航结点

使用密封类建立两个页面结点

sealed class NavigationItem(val route:String){
    object ShowNote:NavigationItem("ShowNote")
    object EditNote:NavigationItem("EditNote")
}

绘制导航地图

通过使用NavHostController完成导航路由,其中笔记编辑界面需要传递参数,直接在结点之后添加对应参数格式,然后通过navArgument进行参数定义,最后通过NavBackStackEntry去除对应参数值,并传递到具体Compose组件中

fun NavigationGraph(navHostController: NavHostController){
    NavHost(navController = navHostController , startDestination = NavigationItem.ShowNote.route){
        composable(NavigationItem.ShowNote.route){
            ShowNotePage(navController = navHostController)
        }
        composable(
            NavigationItem.EditNote.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
            EditNotePage(navHostController = navHostController, color = color)
        }
    }
}

入口

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        WindowCompat.setDecorFitsSystemWindows(window,false)
        installSplashScreen()
        super.onCreate(savedInstanceState)
        setContent {
            NoteAppTheme {
                ProvideWindowInsets() {
                    val systemUiController = rememberSystemUiController()
                    SideEffect {
                        systemUiController.setStatusBarColor(Color.Transparent, darkIcons = false)
                    }
                    Surface(
                        color = NoteTheme.colors.background,
                        modifier = Modifier.fillMaxSize().navigationBarsPadding()
                    ) {
                        val navHostController = rememberNavController()
                        NavigationGraph(navHostController = navHostController)
                    }
                }
            }
        }
    }
}

总结

整个项目功能不多,但整个项目架构职责明了,对于学习Compose入门的同志而言,我认为是一个好的项目;在自己在学习compose时没有养成不必要的编码坏习惯之前,先参考一定具有参考性的开源代码,养成自己编码思想、风格,我认为有一定必要

Gitee链接

EasyNote

https://gitee.com/FranzLiszt1847/easy-note

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/141169.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

动态规划 0-1背包问题(滚动数组思想优化)

目录 125 背包问题&#xff08;二&#xff09;LintCode 炼码 0-1背包滚动数组优化 0-1背包问题&#xff08;一&#xff09;LintCode 炼码 【解法一】二维数组 【解法二】滚动数组 125 背包问题&#xff08;二&#xff09;LintCode 炼码 class Solution { public:/*** para…

HADOOP-3.2.2安装

HADOOP-3.2.2安装一. 准备工作二.安装阶段1. 创建安装目录并安装解压包2.修改配置文件core-site.xml3. 修改hdfs-site.xml4. 修改修改yarn-site.xml5.修改workers文件6.修改hadoop-env.sh7.修改mapred-site.xml8.递归创建目录9.分发文件三.运行阶段1.启动hdfs2.启动yarn3.启动j…

F280049C Crossbar X-BAR

文章目录X-BAR9.1 输入X-BAR9.2 ePWM、CLB和GPIO输出X-BAR9.2.1 ePWM X-BAR9.2.1.1 ePWM X-BAR架构9.2.2 CLB X-BAR9.2.2.1 CLB X-BAR架构9.2.3 GPIO输出X-BAR9.2.3.1 GPIO输出X-BAR架构9.2.4 X-BAR标志总结X-BAR 交叉开关&#xff08;在本章中称为X-BAR&#xff09;提供了以各…

一年风雨几度寒,一杯浊酒敬虎年

我是谁大家好&#xff0c;我是凡夫贩夫&#xff0c;真实姓名不值一提&#xff0c;我的履历也很不值一提&#xff0c;非名校非大厂非专家&#xff0c;一名三非野生java开发者&#xff0c;现居住地河南郑州&#xff0c;就职于一家外包公司。的确&#xff0c;我是一个普通人&#…

(02)Cartographer源码无死角解析-(46) 2D栅格地图→CastRay()函数与贝汉明(Bresenham)算法

讲解关于slam一系列文章汇总链接:史上最全slam从零开始&#xff0c;针对于本栏目讲解(02)Cartographer源码无死角解析-链接如下: (02)Cartographer源码无死角解析- (00)目录_最新无死角讲解&#xff1a;https://blog.csdn.net/weixin_43013761/article/details/127350885 文末…

PCB设计完成后,为什么经常要拼版及拼版注意事项

通常我们在完成PCB设计的时候&#xff0c;有一些板子我们通常是需要进行拼版的&#xff0c;那么我们为什么要拼版&#xff0c;哪种情况下需要拼版呢&#xff1f;不拼是否可以呢&#xff1f;1、PCB生产制作尺寸要求 一般来说面积比较小的板子我们是需要进行拼版&#xff0c;一般…

MATLAB APP 设计实践(一)UART通信(下篇)

引言上篇介绍了 MATLAB App 的基本内容&#xff0c;本篇就结合UART发送数据的具体案例介绍开发过程。文末给出设计源文件、设计的可执行文件的下载链接&#xff0c;以及App的实际使用视频&#xff08;与FPGA开发板进行调试验证&#xff09;。前文链接&#xff1a;MATLAB APP 设…

MySQL 分区(innode引擎的讲解)

目录 一.InnoDB逻辑存储结构 段 区 页 二.分区概述 分区 三.分区类型 一.InnoDB逻辑存储结构 首先要先介绍一下InnoDB逻辑存储结构和区的概念&#xff0c;它的所有数据都被逻辑地存放在表空间&#xff0c;表空间又由段&#xff0c;区&#xff0c;页组成。 段 段就是…

【Python】sklearn机器学习之层次聚类算法AgglomerativeClustering

文章目录基本原理绘图层次定义距离基本原理 和Birch聚类相似&#xff0c;层次聚类也是一种依赖树结构实现的聚类方法&#xff0c;其核心概念是相似度。根据相似度&#xff0c;可以将所有样本组织起来&#xff0c;从而构建一棵层次聚类树。 其中Birch算法的核心&#xff0c;叫…

MyBatis【多表查询与动态SQL使用】

MyBatis【多表查询与动态SQL使用】&#x1f34e;一.MyBatis多表查询&#x1f352;1.1 一对一查询&#x1f352;1.2 一对多查询&#x1f34e;二.动态SQL使用&#x1f352;2.1 if 标签使用&#x1f352;2.2 trim 标签使用&#x1f352;2.3 where 标签使用&#x1f352;2.4 set 标…

ARX测试_绘制道路横断面

本文迁移自本人网易博客&#xff0c;写于2011年1月12日&#xff0c;ARX测试_绘制道路横断面 - lysygyy的日志 - 网易博客 (163.com)1、已提供道路的图形&#xff0c;获取用户输入的两点&#xff0c;并在两点间画一条虚线。计算这条直线与多少条直线相交&#xff0c;若数量不等于…

振弦采集模块多通道专用寄存器

振弦采集模块多通道专用寄存器 多通道频率、温度值寄存器 51~58&#xff08; 0x33~0x3A&#xff09; 位 符号 值 描述 默认值 bit15:0 频率/温度值 0 单通道模块时&#xff0c;寄存器 51 内为频率值&#xff0c;寄存器 55 内为温度值 4 通道模块时&#xff0c;寄存器 51~54 内…

如何去学习PMP考试的《PMBOK》

首先&#xff0c;是PMP考试的核心教材&#xff1a;《PMBOK指南》&#xff0c;目前已经出道第七版了&#xff0c;大家如果有备考的需要要赶紧买一本来学习。 其次&#xff0c;是《汪博士解读PMP》&#xff0c;目前出到第6版&#xff0c;这本书是对PMBOK中各领域知识点的深入浅出…

机械臂速成小指南(二十一):几何雅可比矩阵

&#x1f468;‍&#x1f3eb;&#x1f970;&#x1f973;需要机械臂相关资源的同学可以在评论区中留言哦&#x1f916;&#x1f63d;&#x1f984;指南目录&#x1f4d6;&#xff1a;&#x1f389;&#x1f389;机械臂速成小指南&#xff08;零点五&#xff09;&#xff1a;机…

煤矿皮带跑偏撕裂智能检测算法 opencv

煤矿皮带跑偏撕裂智能检测算法能够通过pythonopencv深度学习技术实时监测运输皮带的状况&#xff0c;当监测到皮带出现撕裂跑偏时&#xff0c;立刻抓拍告警并中止皮带的运输。OpenCV基于C实现&#xff0c;同时提供python, Ruby, Matlab等语言的接口。OpenCV-Python是OpenCV的Py…

动态博客系统

Halo 是我折腾过的众多博客系统里面&#xff0c;最好、最容易上手的动态博客系统之一&#xff08; solo 也是&#xff09;&#xff0c;轻快&#xff0c;简洁&#xff0c;功能强大。 正文 上周末正在募集团队一起写算法题&#xff0c;群里讨论需要一个网站来存放文章&#xff…

C++ 语法基础课 习题6 —— 函数

文章目录例题1. 804.n的阶乘2. 805.x和y的最大值3. 808.最大公约数4. 811.交换数值5. 812.打印数字6. 813.打印矩阵7. 819.递归求阶乘8. 820.递归求斐波那契数列例题 1. 804.n的阶乘 Acwing 804.n的阶乘 #include<iostream> using namespace std;int fact(int n) {if(…

MFC高级控件RichEdit2.0的使用

MFC高级控件RichEdit的使用MFC高级控件RichEdit的使用MFC控件设置焦点&#xff0c;实现回车后编辑框内容清空&#xff0c;焦点停留在该编辑框内MFC高级控件RichEdit的使用 RichEdit控件&#xff0c;可以设置编辑框内不同的行、不同的段落有不同的字体、颜色。 效果如下&#xf…

Python--文件基本操作

文件的存储方式 在计算机中&#xff0c;文件是以 二进制的方式保存在磁盘上的 文本文件和二进制文件 文本文件 可以使用文本编辑软件查看本质上还是二进制文件二进制文件 保存的内容 不是给人直接阅读的&#xff0c;而是提供给其它软件使用的二进制文件不能使用 文件编辑软件…

NTN(一) 基本架构

R17将NTN纳入了3GPP规范&#xff0c;NTN是 non-terrestrial networks非地面网络缩写&#xff0c;通过卫星或无人机平台实现NR通信&#xff0c;在地面网络设备无法普及的地方&#xff0c;采用NTN覆盖&#xff0c;进一步提升覆盖范围。例如在沙漠、海洋等极限区域&#xff0c;采用…