无名之辈的Android之路收藏

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

2022-02-14  本文已影响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(三)

上一篇下一篇

猜你喜欢

热点阅读