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

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

@[TOC](一个简单的笔记APP)

# 简述

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

此项目原创来源于YouTube的一位创作者[Philipp Lackner](https://www.youtube.com/watch?v=8YPXv7xKh2w)

# 效果视频


# Hilt提供依赖对象

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

```kotlin

/**

* 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`实现此接口,并通过数据库实例完成接口实现

```bash

class NoteRepositoryImpl(private val dao: NoteDao):NoteRepository {

   override fun getNotes(): Flow> {

       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操作封装在一个数据类中,最后外部通过调用此数据类完成对数据库的操作

```bash

data class NoteUseCase(

   val getNotes: GetNotes,

   val getNote: GetNote,

   val deleteNote: DeleteNote,

   val insertNote: InsertNote

)

```


### 查询所有

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

```kotlin

class GetNotes(private val repository:NoteRepository) {

   operator fun invoke(noteType: NoteType = NoteType.Date(NoteOrder.Descending)): Flow> {

       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`函数标记进行挂起,外部调用时就必须在协程中完成

```bash

class GetNote(private val repository: NoteRepository) {

   suspend operator fun invoke(id:Int):NoteBean?{

       return repository.getNote(id)

   }

}

```


### 删除


```bash

class DeleteNote(private val repository: NoteRepository) {

   suspend operator fun invoke(noteBean: NoteBean){

       repository.deleteNote(noteBean)

   }

}

```


### 插入

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

```kotlin

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自动注入的,在`Module`中`Provider`可以看到实际提供的对象

```kotlin

@HiltViewModel

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

```


### 数据初始化

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

```kotlin

/**

    * 笔记内容状态管理

    * 所有笔记内容、排序方式、是否显示排序组件*/

   private val _state = mutableStateOf(NotesState())

   val state: State = _state


   /**

    * 存储最近被删除的笔记*/

   private var recentlyDeleteNote:NoteBean? = null


   private var getNotesJob: Job? = null

```

然后对数据进行初始化

```bash

init {

       getNotes(NoteType.Date(NoteOrder.Descending))

   }

```

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


```bash

/**

    * 这是因为 SQLite 数据库的内容更新通知功能是以表 (Table) 数据为单位,而不是以行 (Row) 数据为单位,因此只要是表中的数据有更新,

    * 它就触发内容更新通知。Room 不知道表中有更新的数据是哪一个,因此它会重新触发 DAO 中定义的 query 操作。

    * 您可以使用 Flow 的操作符,比如 distinctUntilChanged 来确保只有在当您关心的数据有更新时才会收到通知

   */

```


```kotlin

   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自动注入的依赖对象进行处理,无需进行手动注入完成对象实例化

```kotlin

   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进行处理


```kotlin

@Composable

fun ShowNotePage(navController: NavController,viewModel: NotesViewModel = hiltViewModel()){

   val state = viewModel.state.value

   val scaffoldState = rememberScaffoldState()

   val scope = rememberCoroutineScope()

   ...

   }

```

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


### 标题栏

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

```kotlin

    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类进行相应处理


```kotlin

  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`,并对尾部添加`撤回`按钮,在撤回按钮中又进行笔记撤回删除操作,也就是重新插入

```kotlin

  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`展示笔记列表,并在笔记点击事件中进行导航,因为是从已存在的笔记进行导航,所以需要传递一些参数

```kotlin

@Composable

fun NoteList(navController: NavController,notes:List, 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}¬eColor=${notes[it].color}")

           })

           if (it < notes.size - 1){

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

           }

       }

   }

}

```

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

```kotlin

@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`对象,此类用于获取导航路由传递的参数,就不需要去通过函数传递和获取了

```kotlin

@HiltViewModel

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

```


### 初始化

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

```kotlin

   /**

    * 对标题输入内容进行状态管理

    * text:标题输入框输入的内容

    * hint:标题输入框默认显示内容

    * isHintVisible:标题输入框是否显示hint内容*/

   private val _noteTitle = mutableStateOf(EditNoteTextFieldState(

       hint = "输入笔记标题..."

   ))

   val noteTitle: State = _noteTitle


   private val _noteContent = mutableStateOf(EditNoteTextFieldState(

       hint = "输入笔记内容..."

   ))

   val noteContent: State = _noteContent


   /**

    * 对当前笔记的背景颜色进行状态管理

    * 默认是从颜色列表中随机取一个颜色*/

   private val _noteColor = mutableStateOf(NoteBean.noteColor.random().toArgb())

   val noteColor: State = _noteColor


   /**

    * 对Ui界面的保存笔记事件和笔记内容是否为空事件进行管理

    * 然后将具体内容传递到Ui界面*/

   private val _eventFlow = MutableSharedFlow()

   val eventFlow = _eventFlow.asSharedFlow()


   /**

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

   private var currentId:Int? = null

```


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

```kotlin

   /**

    * 对笔记内容进行初始化,从导航路由中获取"noteid"的值,然后在根据此值从数据库中进行查询

    * 若不为空,则刷新当前值(从指定笔记进行路由)

    * 否则,为默认值(创建一个新的笔记)*/

   init {

       savedStateHandle.get("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`进行派发通知,外部界面通过接收通知,做出对应处理


```kotlin

   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


```kotlin

@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中获取一个随机背景颜色

```kotlin

  val noteBackground = remember {

       Animatable(

           Color(

               if (color != -1)

                   color

               else

                   viewModel.noteColor.value

           )

       )

   }

```

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

```kotlin

   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`组件默认带有边框、背景等颜色

```kotlin

      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层

```kotlin

    FloatingActionButton(

               backgroundColor = NoteTheme.colors.onBackground,

               onClick = { viewModel.onEvent(EditNoteEvent.SaveNote) }

           ) {

               Icon(

                   imageVector = Icons.Default.Save,

                   contentDescription = "保存",

                   tint = NoteTheme.colors.textColor

               )

           }

```

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

```kotlin

   LaunchedEffect(key1 = true){

       viewModel.eventFlow.collectLatest {

           when(it){

               is EditNoteUiEvent.ShowSnackBar -> {

                   scaffoldState.snackbarHostState.showSnackbar(it.message)

               }

               is EditNoteUiEvent.SaveNoteUi -> {

                   navHostController.navigateUp()

               }

           }

       }

   }

```


# 路由导航


## 建立导航结点

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

```kotlin

sealed class NavigationItem(val route:String){

   object ShowNote:NavigationItem("ShowNote")

   object EditNote:NavigationItem("EditNote")

}

```


## 绘制导航地图

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

```kotlin

fun NavigationGraph(navHostController: NavHostController){

   NavHost(navController = navHostController , startDestination = NavigationItem.ShowNote.route){

       composable(NavigationItem.ShowNote.route){

           ShowNotePage(navController = navHostController)

       }

       composable(

           NavigationItem.EditNote.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

           EditNotePage(navHostController = navHostController, color = color)

       }

   }

}

```


## 入口


```kotlin

@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)


```kotlin

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

```

相关文章
|
4月前
|
XML 自然语言处理 Android开发
🌐Android国际化与本地化全攻略!让你的App走遍全球无障碍!🌍
【7月更文挑战第28天】在全球化背景下,实现Android应用的国际化与本地化至关重要 for 用户基础扩展。本文通过旅游指南App案例,介绍全攻略。步骤包括资源文件拆分与命名、适配布局与方向、处理日期时间及货币格式、考虑文化习俗及进行详尽测试。采用Android Studio支持,创建如`res/values-en/strings.xml`等多语言资源文件夹,使用灵活布局解决文本长度差异问题,并通过用户反馈迭代优化。最终,打造一款能无缝融入全球各地文化的App。
185 3
|
2月前
|
Web App开发 Java 视频直播
FFmpeg开发笔记(四十九)助您在毕业设计中脱颖而出的几个流行APP
对于软件、计算机等专业的毕业生,毕业设计需实现实用软件或APP。新颖的设计应结合最新技术,如5G时代的音视频技术。示例包括: 1. **短视频分享APP**: 集成FFmpeg实现视频剪辑功能,如添加字幕、转场特效等。 2. **电商购物APP**: 具备直播带货功能,使用RTMP/SRT协议支持流畅直播体验。 3. **同城生活APP**: 引入WebRTC技术实现可信的视频通话功能。这些应用不仅实用,还能展示开发者紧跟技术潮流的能力。
79 4
FFmpeg开发笔记(四十九)助您在毕业设计中脱颖而出的几个流行APP
|
2月前
|
安全 Java Android开发
探索安卓应用开发的新趋势:Kotlin和Jetpack Compose
在安卓应用开发领域,随着技术的不断进步,新的编程语言和框架层出不穷。Kotlin作为一种现代的编程语言,因其简洁性和高效性正逐渐取代Java成为安卓开发的首选语言。同时,Jetpack Compose作为一个新的UI工具包,提供了一种声明式的UI设计方法,使得界面编写更加直观和灵活。本文将深入探讨Kotlin和Jetpack Compose的特点、优势以及如何结合使用它们来构建现代化的安卓应用。
54 4
|
3月前
|
Web App开发 Android开发
FFmpeg开发笔记(四十六)利用SRT协议构建手机APP的直播Demo
实时数据传输在互联网中至关重要,不仅支持即时通讯如QQ、微信的文字与图片传输,还包括音视频通信。一对一通信常采用WebRTC技术,如《Android Studio开发实战》中的App集成示例;而一对多的在线直播则需部署独立的流媒体服务器,使用如SRT等协议。SRT因其优越的直播质量正逐渐成为主流。本文档概述了SRT协议的使用,包括通过OBS Studio和SRT Streamer进行SRT直播推流的方法,并展示了推流与拉流的成功实例。更多细节参见《FFmpeg开发实战》一书。
55 1
FFmpeg开发笔记(四十六)利用SRT协议构建手机APP的直播Demo
|
3月前
|
Web App开发 5G Linux
FFmpeg开发笔记(四十四)毕业设计可做的几个拉满颜值的音视频APP
一年一度的毕业季来临,计算机专业的毕业设计尤为重要,不仅关乎学业评价还积累实战经验。选择紧跟5G技术趋势的音视频APP作为课题极具吸引力。这里推荐三类应用:一是融合WebRTC技术实现视频通话的即时通信APP;二是具备在线直播功能的短视频分享平台,涉及RTMP/SRT等直播技术;三是具有自定义动画特效及卡拉OK歌词字幕功能的视频剪辑工具。这些项目不仅技术含量高,也符合市场需求,是毕业设计的理想选择。
73 6
FFmpeg开发笔记(四十四)毕业设计可做的几个拉满颜值的音视频APP
|
3月前
|
编解码 Java Android开发
FFmpeg开发笔记(四十五)使用SRT Streamer开启APP直播推流
​SRT Streamer是一个安卓手机端的开源SRT协议直播推流框架,可用于RTMP直播和SRT直播。SRT Streamer支持的视频编码包括H264、H265等等,支持的音频编码包括AAC、OPUS等等,可谓功能强大的APP直播框架。另一款APP直播框架RTMP Streamer支持RTMP直播和RTSP直播,不支持SRT协议的直播。而本文讲述的SRT Streamer支持RTMP直播和SRT直播,不支持RTSP协议的直播。有关RTMP Streamer的说明参见之前的文章《使用RTMP Streamer开启APP直播推流》,下面介绍如何使用SRT Streamer开启手机直播。
66 4
FFmpeg开发笔记(四十五)使用SRT Streamer开启APP直播推流
|
4月前
|
Web App开发 缓存 编解码
FFmpeg开发笔记(三十八)APP如何访问SRS推流的RTMP直播地址
《FFmpeg开发实战》书中介绍了轻量级流媒体服务器MediaMTX,适合测试RTSP/RTMP协议,但不适用于复杂直播场景。SRS是一款强大的开源流媒体服务器,支持多种协议,起初为RTMP,现扩展至HLS、SRT等。在FFmpeg 6.1之前,推送给SRS的HEVC流不受支持。要播放RTMP流,Android应用可使用ExoPlayer,需在`build.gradle`导入ExoPlayer及RTMP扩展,并根据URL类型创建MediaSource。若SRS播放黑屏,需在配置文件中开启`gop_cache`以缓存关键帧。
139 2
FFmpeg开发笔记(三十八)APP如何访问SRS推流的RTMP直播地址
|
3月前
|
Kubernetes Linux Docker
【Azure 应用服务】使用Docker Compose创建App Service遇见"Linux Version is too long. It cannot be more than 4000 characters"错误
【Azure 应用服务】使用Docker Compose创建App Service遇见"Linux Version is too long. It cannot be more than 4000 characters"错误
|
4月前
|
消息中间件 Android开发 开发者
🔍深度剖析Android内存泄漏,让你的App远离崩溃边缘,稳如老狗!🐶
【7月更文挑战第28天】在 Android 开发中,内存管理至关重要。内存泄漏可悄无声息地累积,最终导致应用崩溃或性能下滑。它通常由不正确地持有 Activity 或 Fragment 的引用引起。常见原因包括静态变量持有组件引用、非静态内部类误用、Handler 使用不当、资源未关闭及集合对象未清理。使用 Android Studio Profiler 和 LeakCanary 可检测泄漏,修复方法涉及使用弱引用、改用静态内部类、妥善管理 Handler 和及时释放资源。良好的内存管理是保证应用稳定性的基石。
76 4
|
4月前
|
XML 缓存 Android开发
🎯解锁Android性能优化秘籍!让你的App流畅如飞,用户爱不释手!🚀
【7月更文挑战第28天】在移动应用竞争中,性能是关键。掌握Android性能优化技巧对开发者至关重要。
41 2