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

简述

此项目功能较为简单,基本就是使用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

猜你喜欢

转载自blog.csdn.net/News53231323/article/details/128561955