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

```

相关文章
|
1月前
|
Web App开发 安全 程序员
FFmpeg开发笔记(五十五)寒冬里的安卓程序员可进阶修炼的几种姿势
多年的互联网寒冬在今年尤为凛冽,坚守安卓开发愈发不易。面对是否转行或学习新技术的迷茫,安卓程序员可从三个方向进阶:1)钻研谷歌新技术,如Kotlin、Flutter、Jetpack等;2)拓展新功能应用,掌握Socket、OpenGL、WebRTC等专业领域技能;3)结合其他行业,如汽车、游戏、安全等,拓宽职业道路。这三个方向各有学习难度和保饭碗指数,助你在安卓开发领域持续成长。
58 1
FFmpeg开发笔记(五十五)寒冬里的安卓程序员可进阶修炼的几种姿势
|
21天前
|
Linux API 开发工具
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
66 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
2月前
|
Web App开发 Java 视频直播
FFmpeg开发笔记(四十九)助您在毕业设计中脱颖而出的几个流行APP
对于软件、计算机等专业的毕业生,毕业设计需实现实用软件或APP。新颖的设计应结合最新技术,如5G时代的音视频技术。示例包括: 1. **短视频分享APP**: 集成FFmpeg实现视频剪辑功能,如添加字幕、转场特效等。 2. **电商购物APP**: 具备直播带货功能,使用RTMP/SRT协议支持流畅直播体验。 3. **同城生活APP**: 引入WebRTC技术实现可信的视频通话功能。这些应用不仅实用,还能展示开发者紧跟技术潮流的能力。
79 4
FFmpeg开发笔记(四十九)助您在毕业设计中脱颖而出的几个流行APP
|
3月前
|
JavaScript 前端开发 Java
FFmpeg开发笔记(四十七)寒冬下安卓程序员的几个技术转型发展方向
IT寒冬使APP开发门槛提升,安卓程序员需转型。选项包括:深化Android开发,跟进Google新技术如Kotlin、Jetpack、Flutter及Compose;研究Android底层框架,掌握AOSP;转型Java后端开发,学习Spring Boot等框架;拓展大前端技能,掌握JavaScript、Node.js、Vue.js及特定框架如微信小程序、HarmonyOS;或转向C/C++底层开发,通过音视频项目如FFmpeg积累经验。每条路径都有相应的书籍和技术栈推荐,助你顺利过渡。
58 3
FFmpeg开发笔记(四十七)寒冬下安卓程序员的几个技术转型发展方向
|
2月前
|
安全 Java Android开发
探索安卓应用开发的新趋势:Kotlin和Jetpack Compose
在安卓应用开发领域,随着技术的不断进步,新的编程语言和框架层出不穷。Kotlin作为一种现代的编程语言,因其简洁性和高效性正逐渐取代Java成为安卓开发的首选语言。同时,Jetpack Compose作为一个新的UI工具包,提供了一种声明式的UI设计方法,使得界面编写更加直观和灵活。本文将深入探讨Kotlin和Jetpack Compose的特点、优势以及如何结合使用它们来构建现代化的安卓应用。
57 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歌词字幕功能的视频剪辑工具。这些项目不仅技术含量高,也符合市场需求,是毕业设计的理想选择。
74 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`以缓存关键帧。
140 2
FFmpeg开发笔记(三十八)APP如何访问SRS推流的RTMP直播地址
|
3月前
|
编解码 安全 Ubuntu
Android Selinux 问题处理笔记
这篇文章是关于处理Android系统中SELinux权限问题的笔记,介绍了如何通过分析SELinux拒绝的日志、修改SELinux策略文件,并重新编译部署来解决权限问题,同时提供了一些SELinux的背景知识和实用工具。
59 0