美文网首页无名之辈的Android之路收藏
Android实战:手机笔记App(一)

Android实战:手机笔记App(一)

作者: 搬码人 | 来源:发表于2022-02-14 18:05 被阅读0次

    引入

    其实这项目与我之前做的手机便签项目在功能上有点冲突,但是除了在UI方面不一样以外,其所使用的技术知识点也大有不同。此项目是小编学了一段时间Jetpack Compose之后在YouTube自学的一个项目,不再采用传统的View(命令式UI)而采用声明式UI技术。
    简介:首页是已创建的笔记的列表展示,点击右上角的菜单可对创建的笔记按不同需求进行排序,再次点击菜单按钮可对其进行隐藏,点击某个笔记的item可进入查看详情或进行修改。点击首页的添加悬浮键可添加新的笔记文本,上方可对文本设置背景色。

    image.png

    主要技术点

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

    创建分类文件

    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
        )
        */
    )
    

    创建Application类、并使用注解@HiltAndroidApp
    这一步是使用Hilt的重要步骤

    image.png image.png

    数据搭建

    创建数据库列表

    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.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)
        }
    }
    

    创建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

    项目完整代码:https://github.com/gun-ctrl/NoteApp

    Android实战:手机笔记App(二)

    Android实战:手机笔记App(三)

    相关文章

      网友评论

        本文标题:Android实战:手机笔记App(一)

        本文链接:https://www.haomeiwen.com/subject/raoclrtx.html