一个简单的笔记APP
- 简述
- 效果视频
- Hilt提供依赖对象
- Room CRUD
- 接口实现类
- 内容封装
- 查询所有
- 查询
- 删除
- 插入
- 笔记内容
- 效果图
- ViewModel
- 依赖注入
- 数据初始化
- 数据处理
- View
- 标题栏
- 排序组件
- 笔记列表
- 新建&编辑笔记
- 效果图
- ViewModel
- 依赖注入
- 初始化
- 数据处理
- View
- 背景颜色条
- 标题
- 保存笔记
- 路由导航
- 建立导航结点
- 绘制导航地图
- 入口
- 总结
- Gitee链接
简述
此项目功能较为简单,基本就是使用Room数据库实现CRUD,但是此项目实现了一个干净的架构,项目使用MVVM架构进行设计,每一个模块的职责划分清晰,功能明确,没有冗余的代码。其中涉及了Hilt依赖注入,对于数据库的的操作,使用接口实现类进行获取,然后将实现类的CRUD操作封装在一个数据类中,最后通过Hilt自动注入依赖,供外部调用。
此项目原创来源于YouTube的一位创作者Philipp Lackner
效果视频
Hilt提供依赖对象
有关Hilt依赖注入的文章可以参考其他文章——Hilt依赖注入,此处就不在进行多余阐述,providerNoteDataBase
提供了数据对象,providerNoteRepository
提供了数据库接口实现类对象,providerNoteUseCase
提供了数据库具体操作对象;这三个对象是一环扣一环,上一个为下一个提供对象,最后一个提供外部使用,这是Hilt依赖注入的一个便利,无需我们手动去一个个绑定,Hilt自动就帮我完成了这部分
/**
* Module:用来管理所有需要提供的对象
* Provides:用来提供对象
* InstallIn:用来将模块装载到对应作用域饿,此处是单例
* 自动绑定到"SingletonComponent::class"上*/
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
/**
* 提供数据库对象*/
@Provides
@Singleton
fun providerNoteDataBase(application: Application):NoteDatabase{
return Room.databaseBuilder(
application,
NoteDatabase::class.java,
NoteDatabase.DATABASE_NAME
).build()
}
/**
* 提供数据库Dao类操作对象*/
@Provides
@Singleton
fun providerNoteRepository(db:NoteDatabase):NoteRepository{
return NoteRepositoryImpl(db.noteDao)
}
/**
* 提供数据库具体操作内容对象*/
@Provides
@Singleton
fun providerNoteUseCase(repository: NoteRepository):NoteUseCase{
return NoteUseCase(
GetNotes(repository),
GetNote(repository),
DeleteNote(repository),
InsertNote(repository)
)
}
}
Room CRUD
接口实现类
其中NoteRepository
是一个接口类,提供了数据库的相关操作方法,然后NoteRepositoryImpl
实现此接口,并通过数据库实例完成接口实现
class NoteRepositoryImpl(private val dao: NoteDao):NoteRepository {
override fun getNotes(): Flow<List<NoteBean>> {
return dao.queryAll()
}
override suspend fun getNote(id: Int): NoteBean? {
return dao.queryById(id)
}
override suspend fun insertNote(bean: NoteBean) {
dao.insertNote(bean)
}
override suspend fun deleteNote(bean: NoteBean) {
dao.deleteNote(bean)
}
}
内容封装
将数据库的CRUD操作封装在一个数据类中,最后外部通过调用此数据类完成对数据库的操作
data class NoteUseCase(
val getNotes: GetNotes,
val getNote: GetNote,
val deleteNote: DeleteNote,
val insertNote: InsertNote
)
查询所有
使用接口实现类提供的数据,并List数据进行排序处理,此处使用的是invoke
函数,此函数的作用是,外部调用此函数就像类的构造函数一般,无需对类进行初始化,然后在调用此方法,可以直接GetNotes(param)
,就相当于调用了invoke
函数
class GetNotes(private val repository:NoteRepository) {
operator fun invoke(noteType: NoteType = NoteType.Date(NoteOrder.Descending)): Flow<List<NoteBean>> {
return repository.getNotes().map { notes ->
when(noteType.noteOrder){
is NoteOrder.Ascending->{
when(noteType){
is NoteType.Title-> notes.sortedBy { it.title.lowercase() }
is NoteType.Date-> notes.sortedBy { it.time }
is NoteType.Color-> notes.sortedBy { it.color }
}
}
is NoteOrder.Descending->{
when(noteType){
is NoteType.Title-> notes.sortedByDescending { it.title.lowercase() }
is NoteType.Date-> notes.sortedByDescending { it.time }
is NoteType.Color-> notes.sortedByDescending { it.color }
}
}
}
}
}
}
查询
数据库操作可以划分为耗时操作,所有使用suspend
函数标记进行挂起,外部调用时就必须在协程中完成
class GetNote(private val repository: NoteRepository) {
suspend operator fun invoke(id:Int):NoteBean?{
return repository.getNote(id)
}
}
删除
class DeleteNote(private val repository: NoteRepository) {
suspend operator fun invoke(noteBean: NoteBean){
repository.deleteNote(noteBean)
}
}
插入
此处对数据库进行了插入操作,在此之前对插入的数据进行判空处理,如果为空,则通过自定义的一个异常类抛出此异常
class InsertNote(private val repository: NoteRepository) {
@Throws(InvalidNoteException::class)
suspend operator fun invoke(bean: NoteBean){
if (bean.title.isBlank()){
throw InvalidNoteException("标题不能为空!")
}
if (bean.content.isBlank()){
throw InvalidNoteException("内容不能为空!")
}
repository.insertNote(bean)
}
}
笔记内容
此界面完成的功能包括:显示所有笔记内容、删除笔记、撤回删除笔记、对笔记进行排序处理、跳转至创建笔记页面
效果图
ViewModel
开头已经介绍,此项目使用的是MVVM架构,所以VM类必不可少,VM类的职责为承接Model和View之间的桥梁作用,所有的交互或者数据处理放到VM类进行处理,View组件绑定VM中有状态的变量,一旦VM进行数据处理,外部相对应的组件就会进行重组
依赖注入
使用HiltViewModel
注解标注此VM类,代表此类中要使用Hilt提供的依赖对象,然后@Inject
注解,获取NoteUseCase
对象,此对象是Hilt自动注入的,在Module
中Provider
可以看到实际提供的对象
@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
脚手架完成FAB
和SnackBar
的填充
标题栏
通过监听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}¬eColor=${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}¬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)
}
}
}
入口
@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