Android實(shí)戰(zhàn):手機(jī)筆記App(一)

引入

其實(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

image.png

導(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)建分類文件

image.png

資源文件配置
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的重要步驟

image.png

image.png

數(shù)據(jù)搭建

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

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時(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

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"
    }
}

創(chuàng)建Repository

image.png
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的模型工廠

image.png

目前只用創(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)邏輯操作

image.png

封裝命令

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


image.png
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


image.png

項(xiàng)目完整代碼:https://github.com/gun-ctrl/NoteApp

Android實(shí)戰(zhàn):手機(jī)筆記App(二)

Android實(shí)戰(zhàn):手機(jī)筆記App(三)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容