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月前
|
XML Java 数据库
安卓项目:app注册/登录界面设计
本文介绍了如何设计一个Android应用的注册/登录界面,包括布局文件的创建、登录和注册逻辑的实现,以及运行效果的展示。
139 0
安卓项目:app注册/登录界面设计
|
2月前
|
Java 数据库 Android开发
一个Android App最少有几个线程?实现多线程的方式有哪些?
本文介绍了Android多线程编程的重要性及其实现方法,涵盖了基本概念、常见线程类型(如主线程、工作线程)以及多种多线程实现方式(如`Thread`、`HandlerThread`、`Executors`、Kotlin协程等)。通过合理的多线程管理,可大幅提升应用性能和用户体验。
120 15
一个Android App最少有几个线程?实现多线程的方式有哪些?
|
2月前
|
存储 开发工具 Android开发
使用.NET MAUI开发第一个安卓APP
【9月更文挑战第24天】使用.NET MAUI开发首个安卓APP需完成以下步骤:首先,安装Visual Studio 2022并勾选“.NET Multi-platform App UI development”工作负载;接着,安装Android SDK。然后,创建新项目时选择“.NET Multi-platform App (MAUI)”模板,并仅针对Android平台进行配置。了解项目结构,包括`.csproj`配置文件、`Properties`配置文件夹、平台特定代码及共享代码等。
156 2
|
2月前
|
XML Android开发 数据格式
🌐Android国际化与本地化全攻略!让你的App走遍全球无障碍!🌍
在全球化背景下,实现Android应用的国际化与本地化至关重要。本文以一款旅游指南App为例,详细介绍如何通过资源文件拆分与命名、适配布局与方向、处理日期时间及货币格式、考虑文化习俗等步骤,完成多语言支持和本地化调整。通过邀请用户测试并收集反馈,确保应用能无缝融入不同市场,提升用户体验与满意度。
102 3
|
2月前
|
Java 数据库 Android开发
一个Android App最少有几个线程?实现多线程的方式有哪些?
本文介绍了Android应用开发中的多线程编程,涵盖基本概念、常见实现方式及最佳实践。主要内容包括主线程与工作线程的作用、多线程的多种实现方法(如 `Thread`、`HandlerThread`、`Executors` 和 Kotlin 协程),以及如何避免内存泄漏和合理使用线程池。通过有效的多线程管理,可以显著提升应用性能和用户体验。
69 10
|
1月前
|
安全 网络安全 Android开发
深度解析:利用Universal Links与Android App Links实现无缝网页至应用跳转的安全考量
【10月更文挑战第2天】在移动互联网时代,用户经常需要从网页无缝跳转到移动应用中。这种跳转不仅需要提供流畅的用户体验,还要确保安全性。本文将深入探讨如何利用Universal Links(仅限于iOS)和Android App Links技术实现这一目标,并分析其安全性。
216 0
|
2月前
|
安全 Java Android开发
探索安卓应用开发的新趋势:Kotlin和Jetpack Compose
在安卓应用开发领域,随着技术的不断进步,新的编程语言和框架层出不穷。Kotlin作为一种现代的编程语言,因其简洁性和高效性正逐渐取代Java成为安卓开发的首选语言。同时,Jetpack Compose作为一个新的UI工具包,提供了一种声明式的UI设计方法,使得界面编写更加直观和灵活。本文将深入探讨Kotlin和Jetpack Compose的特点、优势以及如何结合使用它们来构建现代化的安卓应用。
61 4
|
2月前
|
XML 数据库 Android开发
10分钟手把手教你用Android手撸一个简易的个人记账App
该文章提供了使用Android Studio从零开始创建一个简单的个人记账应用的详细步骤,包括项目搭建、界面设计、数据库处理及各功能模块的实现方法。
|
3月前
|
API Android开发
Android P 性能优化:创建APP进程白名单,杀死白名单之外的进程
本文介绍了在Android P系统中通过创建应用进程白名单并杀死白名单之外的进程来优化性能的方法,包括设置权限、获取运行中的APP列表、配置白名单以及在应用启动时杀死非白名单进程的代码实现。
62 1
|
3月前
|
IDE Java 开发工具
探索安卓开发之旅:打造你的第一款App
【8月更文挑战第24天】在这篇文章中,我们将一起踏上激动人心的安卓开发之旅。不论你是编程新手还是希望扩展技能的老手,本文将为你提供一份详尽指南,帮助你理解安卓开发的基础知识并实现你的第一个应用程序。从搭建开发环境到编写“Hello World”,每一步都将用浅显易懂的语言进行解释。那么,让我们开始吧!