引入
其实这项目与我之前做的手机便签项目在功能上有点冲突,但是除了在UI方面不一样以外,其所使用的技术知识点也大有不同。此项目是小编学了一段时间Jetpack Compose之后在YouTube自学的一个项目,不再采用传统的View(命令式UI)而采用声明式UI技术。
image.png
简介:首页是已创建的笔记的列表展示,点击右上角的菜单可对创建的笔记按不同需求进行排序,再次点击菜单按钮可对其进行隐藏,点击某个笔记的item可进入查看详情或进行修改。点击首页的添加悬浮键可添加新的笔记文本,上方可对文本设置背景色。
主要技术点
1、Jetpack Compose(项目支撑,要有基础才能看懂)
2、MVVM设计模式
3、Hilt自动化注入技术
项目准备
创建Empty Compose Activity
image.png导入项目所需依赖项
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
dependencies {
// Compose dependencies
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0-beta01"
implementation "androidx.navigation:navigation-compose:2.4.0-alpha09"
implementation "androidx.compose.material:material-icons-extended:$compose_version"
implementation "androidx.hilt:hilt-navigation-compose:1.0.0-alpha03"
// Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1'
//Dagger - Hilt
implementation "com.google.dagger:hilt-android:2.38.1"
kapt "com.google.dagger:hilt-android-compiler:2.37"
implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03"
kapt "androidx.hilt:hilt-compiler:1.0.0"
// Room
implementation "androidx.room:room-runtime:2.3.0"
kapt "androidx.room:room-compiler:2.3.0"
// Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:2.3.0"
}
buildscript {
ext {
compose_version = '1.0.1'
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21"
classpath "com.google.dagger:hilt-android-gradle-plugin:2.38.1"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
创建分类文件
资源文件配置
Color.kt
val DarkGray = Color(0xFF202020)
val LightBlue = Color(0xFFD7E8DE)
val RedOrange = Color(0xffffab91)
val RedPink = Color(0xfff48fb1)
val BabyBlue = Color(0xff81deea)
val Violet = Color(0xffcf94da)
val LightGreen = Color(0xffe7ed9b)
Shape.kt
val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)
Theme.kt
private val DarkColorPalette = darkColors(
primary = Color.White,
background = DarkGray,
onBackground = Color.White,
surface = LightBlue,
onSurface = DarkGray
)
@Composable
fun NoteAppTheme(darkTheme: Boolean = true, content: @Composable () -> Unit) {
MaterialTheme(
colors = DarkColorPalette,
typography = Typography,
shapes = Shapes,
content = content
)
}
Type.kt
val Typography = Typography(
body1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
)
/* Other default text styles to override
button = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.W500,
fontSize = 14.sp
),
caption = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 12.sp
)
*/
)
创建Application类、并使用注解@HiltAndroidApp
这一步是使用Hilt的重要步骤
数据搭建
创建数据库列表
image.png@Entity(tableName = "note_table")
data class Note(
@PrimaryKey
val id:Int ?=null,
val title:String,
val content:String,
val timestamp:Long,
val color:Int,
){
//添加新Note时可选的背景颜色
companion object{
val noteColors = listOf(RedOrange, LightGreen, Violet, RedPink, BabyBlue)
}
}
//自定义Exception 用于保存内容为空时抛出异常 并提示用户
class InvalidNoteException(message:String):Exception(message)
创建Dao与RoomDatabase
image.png@Dao
interface NoteDao {
@Query("select * from note_table")
fun getNotes():Flow<List<Note>>
@Query("select * from note_table where id=:id")
suspend fun getNoteById(id:Int):Note?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertNote(note: Note)
@Delete
suspend fun deleteNote(note: Note)
}
@Database(
entities =[Note::class],
version = 1,
exportSchema = false
)
abstract class NoteDatabase:RoomDatabase() {
abstract val noteDao:NoteDao
companion object{
const val DATABASE_NAME = "notes_db"
}
}
创建Repository
image.pnginterface NoteRepository {
fun getNotes():Flow<List<Note>>
suspend fun getNoteById(id:Int):Note?
suspend fun insertNote(note: Note)
suspend fun deleteNote(note: Note)
}
class NoteRepositoryImpl(
private val noteDao:NoteDao
):NoteRepository {
override fun getNotes(): Flow<List<Note>> {
return noteDao.getNotes()
}
override suspend fun getNoteById(id: Int): Note? {
return noteDao.getNoteById(id)
}
override suspend fun insertNote(note: Note) {
noteDao.insertNote(note)
}
override suspend fun deleteNote(note: Note) {
noteDao.deleteNote(note)
}
}
创建AppModule作为Hilt的模型工厂
image.png目前只用创建前两个方法即可(其他的后面才提及),如果不了解Hilt并且想了解Hilt可前往Android开发者网站或我之前的文章Hilt了解。这里对Hilt的功能做一个简介:我们在创建某个对象时可能需要其他类的实例对象(称为依赖注入),每次创建这个类都需要按之前的繁琐步骤,Hilt的功能就是解决这类问题——自动化注入技术。
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideNoteDatabase(application: Application):NoteDatabase{
return Room.databaseBuilder(
application,
NoteDatabase::class.java,
NoteDatabase.DATABASE_NAME
).build()
}
@Provides
@Singleton
fun provideNoteRepository(database: NoteDatabase):NoteRepository{
return NoteRepositoryImpl(database.noteDao)
}
@Provides
@Singleton
fun provideNoteUseCases(repository: NoteRepository):NoteUseCases{
return NoteUseCases(
getNote = GetNote(repository),
deleteNote = DeleteNote(repository),
addNote = AddNote(repository),
getNotes = GetNotes(repository)
)
}
}
实现逻辑操作
image.png封装命令
排序命令一共有两行,一行是排序的主体,另外是排序的顺序是顺序还是倒序。
image.png
sealed class OrderType{
object Ascending:OrderType()
object Descending:OrderType()
}
copy方法在后面UI操作中实现两层排序选择时有用,这里简单说一下(因为可能部分读者在这里无法理解):我们先点击第一行选择,如Title,当我们点击第二行选择,如Ascending,需要记住第一行的选择。所以需要有一个方法copy拼接命令。
sealed class NoteOrder(val orderType: OrderType){
class Title(orderType: OrderType):NoteOrder(orderType)
class Date(orderType: OrderType):NoteOrder(orderType)
class Color(orderType: OrderType):NoteOrder(orderType)
fun copy(orderType: OrderType):NoteOrder{
return when(this){
is Title -> Title(orderType)
is Date -> Date(orderType)
is Color -> Color(orderType)
}
}
}
GetNotes
class GetNotes(
private val repository: NoteRepository
) {
operator fun invoke(
noteOrder: NoteOrder = NoteOrder.Date(OrderType.Descending)
):Flow<List<Note>>{
return repository.getNotes().map { notes ->
when(noteOrder.orderType){
is OrderType.Ascending ->{
when(noteOrder){
is NoteOrder.Title -> notes.sortedBy { it.title.lowercase() }
is NoteOrder.Date -> notes.sortedBy { it.timestamp }
is NoteOrder.Color -> notes.sortedBy { it.color }
}
}
is OrderType.Descending ->{
when(noteOrder){
is NoteOrder.Title -> notes.sortedByDescending { it.title.lowercase() }
is NoteOrder.Date -> notes.sortedByDescending { it.timestamp }
is NoteOrder.Color -> notes.sortedByDescending { it.color }
}
}
}
}
}
}
GetNote
class GetNote(
private val repository: NoteRepository
){
suspend operator fun invoke(id:Int):Note?{
return repository.getNoteById(id)
}
}
AddNote
class AddNote(
private val repository: NoteRepository
) {
@Throws(InvalidNoteException::class)
suspend operator fun invoke(note:Note){
if (note.title.isBlank()){
throw InvalidNoteException("The title of the note can't be empty.")
}
if (note.content.isBlank()){
throw InvalidNoteException("The content of the note can't be empty.")
}
repository.insertNote(note)
}
}
DeleteNote
class DeleteNote(
private val repository: NoteRepository
) {
suspend operator fun invoke(note:Note){
repository.deleteNote(note)
}
}
封装逻辑操作
data class NoteUseCases(
val getNotes:GetNotes,
val deleteNote: DeleteNote,
val addNote: AddNote,
val getNote: GetNote
)
因为NoteUseCase在两个界面的ViewModel等多个代码块需要作为依赖注入,所以前面AppModel中有NoteUseCase的提供方法provideNoteUseCases
image.png
网友评论