引入
其實(shí)這項(xiàng)目與我之前做的手機(jī)便簽項(xiàng)目在功能上有點(diǎn)沖突,但是除了在UI方面不一樣以外,其所使用的技術(shù)知識(shí)點(diǎn)也大有不同。此項(xiàng)目是小編學(xué)了一段時(shí)間Jetpack Compose之后在YouTube自學(xué)的一個(gè)項(xiàng)目,不再采用傳統(tǒng)的View(命令式UI)而采用聲明式UI技術(shù)。
簡(jiǎn)介:首頁是已創(chuàng)建的筆記的列表展示,點(diǎn)擊右上角的菜單可對(duì)創(chuàng)建的筆記按不同需求進(jìn)行排序,再次點(diǎn)擊菜單按鈕可對(duì)其進(jìn)行隱藏,點(diǎn)擊某個(gè)筆記的item可進(jìn)入查看詳情或進(jìn)行修改。點(diǎn)擊首頁的添加懸浮鍵可添加新的筆記文本,上方可對(duì)文本設(shè)置背景色。
image.png
主要技術(shù)點(diǎn)
1、Jetpack Compose(項(xiàng)目支撐,要有基礎(chǔ)才能看懂)
2、MVVM設(shè)計(jì)模式
3、Hilt自動(dòng)化注入技術(shù)
項(xiàng)目準(zhǔn)備
創(chuàng)建Empty Compose Activity

導(dǎo)入項(xiàng)目所需依賴項(xiàng)
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
}
}
創(chuàng)建分類文件

資源文件配置
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
)
*/
)
創(chuàng)建Application類、并使用注解@HiltAndroidApp
這一步是使用Hilt的重要步驟


數(shù)據(jù)搭建
創(chuàng)建數(shù)據(jù)庫(kù)列表

@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時(shí)可選的背景顏色
companion object{
val noteColors = listOf(RedOrange, LightGreen, Violet, RedPink, BabyBlue)
}
}
//自定義Exception 用于保存內(nèi)容為空時(shí)拋出異常 并提示用戶
class InvalidNoteException(message:String):Exception(message)
創(chuàng)建Dao與RoomDatabase

@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"
}
}
創(chuàng)建Repository

interface 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)
}
}
創(chuàng)建AppModule作為Hilt的模型工廠

目前只用創(chuàng)建前兩個(gè)方法即可(其他的后面才提及),如果不了解Hilt并且想了解Hilt可前往Android開發(fā)者網(wǎng)站或我之前的文章Hilt了解。這里對(duì)Hilt的功能做一個(gè)簡(jiǎn)介:我們?cè)趧?chuàng)建某個(gè)對(duì)象時(shí)可能需要其他類的實(shí)例對(duì)象(稱為依賴注入),每次創(chuàng)建這個(gè)類都需要按之前的繁瑣步驟,Hilt的功能就是解決這類問題——自動(dòng)化注入技術(shù)。
@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)
)
}
}
實(shí)現(xiàn)邏輯操作

封裝命令
排序命令一共有兩行,一行是排序的主體,另外是排序的順序是順序還是倒序。

sealed class OrderType{
object Ascending:OrderType()
object Descending:OrderType()
}
copy方法在后面UI操作中實(shí)現(xiàn)兩層排序選擇時(shí)有用,這里簡(jiǎn)單說一下(因?yàn)榭赡懿糠肿x者在這里無法理解):我們先點(diǎn)擊第一行選擇,如Title,當(dāng)我們點(diǎn)擊第二行選擇,如Ascending,需要記住第一行的選擇。所以需要有一個(gè)方法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
)
因?yàn)镹oteUseCase在兩個(gè)界面的ViewModel等多個(gè)代碼塊需要作為依賴注入,所以前面AppModel中有NoteUseCase的提供方法provideNoteUseCases

