在本博客中,您将学习怎么运用具有声明性功用的简略UI组件来创立记事本运用程序。您将不会修改任何XML布局或运用布局修改器。相反,您将调用组合函数来界说要运用的元素,然后Compose修改器将完结其他的作业。该运用程序将运用MVVM架构、Room数据库来在本地保存、列表、更改和删去数据以及用于依靠注入的Hilt&Dagger来开发。

一、你会学到什么

  • Jetpack Compose UI
  • SQLit数据库和SQLite查询言语,完结完好的CRUD
  • Android Corrotinas(协程)
  • 怎么构建运用程序完结MVVM架构

Jetpack Compose:是Android推荐的用于创立本机UI的工具包。它简化并加快了Android上的UI开发。

Composable Preview:经过@Composable函数中的@Preview注解,在Android Studio的Split或design选项卡中,能够预览创立的组件

迭代模式:在此工具中,能够与Android Studio本身的可视化进行交互并测试不同的功用,例如更改单选按钮的状况,而无需模拟器;

布置预览:在此功用中,能够直接在模拟器中经过单击“运转”选项中的模拟器图标来运转@Preview,除了更可信的模拟之外,还能够运用权限和惯例的运用程序的上下文。

组合函数:这些函数答应您以编程方式界说运用程序的UI,描绘其形状和数据依靠性,而不是专注于构建UI的进程(初始化元素、将其附加到父元素等)。要创立可组合函数,只需将@Composable注释添加到函数称号即可。

MVVM架构组件:检查一个简略的图标,其间展示了架构的组件以及它们怎么协同作业。

Kotlin、Room和Jetpack Compose的运用开发之练练手

实体:运用Room是描绘数据库表的带注释的类。

Room数据库:简化数据库的运用并充任SQLite数据库的拜访点(躲藏SQLiteOpenHelper)。Room数据库运用DAO对SQLite数据库履行查询。

SQLite数据库:设备上存储。Room持久性库为您创立并保护该数据库。

存储库:您创立的一个类,主要用于办理多个数据源。

DAO:数据拜访目标。SQL查询到函数的映射。运用DAO时,您能够调用办法,Room会完结剩下的作业。

LiveData:数据存储类。它一直保护/换粗年最新版别的数据,并在数据产生变化时告诉调查者。

ViewModel:充任存储库(数据)和UI之间的通讯中心。UI不再需求担心书库来自哪里。ViewModel实例能够在activity/fragment重新创立后继续存在。

创立包来组织运用程序

Kotlin、Room和Jetpack Compose的运用开发之练练手

二、环境建立

在开端之前,装备开发环境很重要。确保Android Studio已装置并正确装备以进行Android开发。您的项目中还需求以下依靠项:

  • Kotlin:Android开发的官方编程言语
  • Jetpack Compose:用于创立UI的现代库
  • Room:用于运用本地数据库的持久性库

翻开builg.gradle(Module.app)

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("kotlin-kapt")
    id("com.google.dagger.hilt.android")
}

android {
    namespace = "com.example.jetnotes"
    compileSdk = 34
    defaultConfig {
        applicationId = "com.example.jetnotes"
        minSdk = 24
        targetSdk = 34
        versionCode = 1
        versionName = "1.0"
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables {
            useSupportLibrary = true
        }
    }
    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }
    buildFeatures {
        compose = true
    }
    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.1"
    }
    packaging {
        resources {
            excludes += "/META-INF/{AL2.0,LGPL2.1}"
        }
    }
}
dependencies {
    implementation("androidx.core:core-ktx:1.10.1")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
    implementation("androidx.activity:activity-compose:1.7.0")
    implementation(platform("androidx.compose:compose-bom:2023.08.00"))
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.ui:ui-graphics")
    implementation("androidx.compose.ui:ui-tooling-preview")
    implementation("androidx.compose.material3:material3")
    testImplementation("junit:junit:4.14-SNAPSHOT")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
    androidTestImplementation(platform("androidx.compose:compose-bom:2023.08.00"))
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-tooling")
    debugImplementation("androidx.compose.ui:ui-test-manifest")
    // Jetpack Compose Navigation
    implementation("androidx.navigation:navigation-compose:2.7.7")
    // Room components
    implementation("androidx.room:room-runtime:2.6.1")
    kapt("androidx.room:room-runtime:2.6.1")
    implementation("androidx.room:room-ktx:2.6.1")
    kapt("androidx.room:room-compiler:2.6.1")
    // Dagger - Hilt
    implementation("com.google.dagger:hilt-android:2.51")
    kapt("com.google.dagger:hilt-compiler:2.51")
    // icons
    implementation("androidx.compose.material:material-icons-extended:1.6.2")
    // swipe
    implementation("me.saket.swipe:swipe:1.3.0")
}
}

在build.gradle文件(项目:JetNotes)中如下所示:

plugins {
    id("com.android.application") version "8.2.2" apply false
    id("org.jetbrains.kotlin.android") version "1.9.0" apply false
    id("com.google.dagger.hilt.android") version "2.51" apply false
}

三、创立实体(模型)

该模型负责运用程序的业务逻辑和数据。在此示例中,咱们将运用Room创立本地数据库并界说NoteModel实体:

创立一个名为NodeModel的新Kotlin类文件,其间包括数据类。此类描绘实体(代表SQLite表)。类中的每个特点代表表中的一列。Room将运用这些特点来创立表并实例化数据库中的行目标。

下面为代码:

package com.example.jetnotes.data.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(// represents the table in the database
    // name of the table in the database
    tableName = "tb_note",
    indices = [
        Index("title", unique = true)// Bank rule not to register equal securities
    ]
)
data class NoteModel(
    @PrimaryKey(autoGenerate = true)// primary key
    @ColumnInfo(name = "id")// Annotation specifies the name for the table in the SQLite database, If you want to change it
    val id: Int = 0,
    @ColumnInfo(name = "title")
    val title: String,
    @ColumnInfo(name = "description")
    val description: String
)

让咱们看看这些注释的效果:

  • @Entity(table = "tb_note") 每个@Entity类代表一个SQLite表。
  • @PrimaryKey每个实体都需求一个主键。简而言之,每个单词都有自己的主意。
  • @ColumnInfo(name = "id")如果您希望表中列的称号与成员变量称号不同,则指定该列的称号。这样,该列的称号将是“id”、“title”、“description”

四、从运用程序创立DAO(数据拜访目标)

它是任何规划数据持久性的运用程序的基本部分。它充任运用程序业务逻辑和数据库之间的笼统层,促进数据的创立、读取、更新和删去(CRUD)。

对于常见的数据库操作,Room哭供给了便捷的注释,例如@Insert@Delete@Update@Query注释用于其他一切内容。您能够对SQLite支撑的任何查询进行编程。

注意:DAO有必要是接口或笼统类。

package com.example.jetnotes.data.room
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.example.jetnotes.data.model.NoteModel
import kotlinx.coroutines.flow.Flow
@Dao
interface NoteDao {
    // List Notes
    @Query("SELECT * FROM tb_note ORDER BY id ASC")
    fun getAllNotes(): Flow<List<NoteModel>>
    // Select Annotation
    @Query("SELECT * FROM tb_note WHERE id=:noteID")
    fun selectNoteID(noteID: Int): Flow<NoteModel>?
    // Insert Notes
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertNote(noteModel: NoteModel)
    // Update Annotation
    @Update
    suspend fun updateNote(noteModel: NoteModel)
    // Delete Annotation
    @Delete
    suspend fun deleteNote(noteModel: NoteModel)
}

注意:运用Flow或LiveData作为回来类型将确保每逢数据库中的数据产生更改时都会发送告诉。

五、完结数据库

Room数据库类有必要是笼统的并扩展RoomDatabase。一般,您的整个运用程序只需求一个Room数据库实例。

在Room创立数据库之前,@Database注释需求几个参数。

  • 将NoteModel指定为唯一具有实体列表的类。
  • 将版别设置为1.每逢数据库表架构产生更改时,您需求增加版别号。
  • exportSchema设置为false以不保存架构版别历史记录备份。 代码如下:
package com.example.jetnotes.data.room
import androidx.room.Database
import androidx.room.RoomDatabase
import com.example.jetnotes.data.model.NoteModel
// The @Database annotation requires arguments so that Room can create the database
// After list the database entities and the version number
@Database(
    // list App entities
    entities = [
        NoteModel::class
    ],
    // database version
    version = 1,
    // export DB declare with false to not keep DB
    exportSchema = false
)
abstract class NoteDatabase: RoomDatabase() {
    // function to return NoteDao
    abstract fun noteDao(): NoteDao
}

六、创立数据库实例

让咱们创立一个NoteModule目标,界说一个带有数据库构建器所需的Context参数的getNoteDb()办法。 让咱们创立一个回来getNoteDao类型的办法。

package com.example.jetnotes.data.room
import android.content.Context
import androidx.room.Room
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module// provide instance of certain type
@InstallIn(SingletonComponent::class)// inform in which Android class each module will be used or replaced
object NoteModule {
    @Provides
    @Singleton
    fun getNoteDb(
        @ApplicationContext
        context: Context
    ) = Room.databaseBuilder(
        context = context,
        NoteDatabase::class.java,
        "note.db"
    ).build()
    @Provides
    @Singleton
    fun getNoteDao(db: NoteDatabase) = db.noteDao()
}

七、Hilt运用类

一切运用Hilt的运用程序都有必要包括运用@HiltAndroidApp注解的Application类。

@HiltAndroidApp触发Hilt代码生成,包括用作运用程序级依靠项容器的运用程序基类。

package com.example.jetnotes
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class NoteApplication: Application() 

生成的Hilt组件附加到Appliacation目标的生命周期并为其供给依靠项。此外,它是运用程序的父组件,这意味着其他组件能够拜访供给的依靠项。

八、将依靠项注入到运用程序的主类中

一旦在Application类中装备了Hilt而且运用程序级组件可用,它就能够为具有@AndroidEntryPoint注解的其他Android类供给依靠项:

MainActivity

package com.example.jetnotes
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.example.jetnotes.ui.screens.HomeScreen
import com.example.jetnotes.ui.theme.JetNotesTheme
import com.example.jetnotes.viewmodels.NoteViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    private lateinit var navHostController: NavHostController
    private val noteViewModel: NoteViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            JetNotesTheme {
                navHostController = rememberNavController()
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    ScreenMain(navHostController, noteViewModel)
                }
            }
        }
    }
}
@Composable
private fun ScreenMain(navHostController: NavHostController, noteViewModel: NoteViewModel) {
    NavHost(navController = navHostController, startDestination = "home_screen") {
        composable("home_screen") {
            HomeScreen(navHostController = navHostController, noteViewModel = noteViewModel)
        }
        composable("note_screen") {
            NoteScreen(navController = navHostController, noteViewModel = noteViewModel, selected = null)
        }
        composable("note_screen/{note_id}", arguments = listOf(
            navArgument("note_id") {
                type = NavType.IntType
            }
        )) {
            val selected = it.arguments?.getInt("note_id")
            NoteScreen(navController = navHostController, noteViewModel = noteViewModel, selected = selected)
        }
    }
}

九、创立Repository

Repository类笼统了多个数据源的拜访。该Repository不是架构组件库的一部分,但它是代码分离和架构的最佳实践。Repository类供给了一个洁净的API,用于拜访运用程序其他部分中的数据。

为什么要运用Repository Repository办理查询并答应您运用多个后端。在最常见的示例中,Repository完结逻辑来决定是从网络获取数据还是运用本地数据库中缓存的成果。

怎么完结Repository 创立一个名为NoteRepo的Kotlin类文件并将以下代码粘贴到其间:

package com.example.jetnotes.repository
import com.example.jetnotes.data.model.NoteModel
import com.example.jetnotes.data.room.NoteDao
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class NoteRepo @Inject constructor(
    private val noteDao: NoteDao
) {
    fun getAllNotes(): Flow<List<NoteModel>> {
        return noteDao.getAllNotes()
    }
    fun selectNoteID(noteID: Int): Flow<NoteModel?> {
        return noteDao.selectNoteID(noteID)
    }
    suspend fun insertNote(note: NoteModel) {
        return noteDao.insertNote(note)
    }
    suspend fun updateNote(note: NoteModel) {
        return noteDao.updateNote(note)
    }
    suspend fun deleteNote(note: NoteModel) {
        return noteDao.deleteNote(note)
    }
}

十、Application Sealed Class

让咱们创立Kotlin密封类并运用它们来办理运用程序的状况。

密封类的子类能够有多个实例。这答应密封类的目标包括状况。在这些情况下或许的状况:stopped、loading、success和error。

package com.example.jetnotes.uitl
sealed class ResultState<out T> {
    object Idle: ResultState<Nothing>()
    object Loading: ResultState<Nothing>()
    data class Success<out T>(val data: T): ResultState<T>()
    data class Error(val exception: Throwable): ResultState<Nothing>()
}

十一、创立ViewModel

ViewModel充任模型和驶入之间的中介。他负责向View供给必要的数据并处理用户的操作。

ViewModel封账了数据表明和转换逻辑,答应视图与底层业务逻辑无关。

它能够包括可调查的特点,这些特点在数据更改时告诉视图,从而完结高效的数据绑定。

viewModelScope是一个预界说的CoroutineScope,包括在KTX ViewModel扩展中。一切协程有必要在一个范围内运转。 CoroutineScope办理一个或多个相关的协程。

launch是一个创立协程并将功用体的履行发送给相应署理的函数。DIspatchers.IO指示该协程应在为I/O操作保存的线程中运转。

HiltViewModel是Hilt特定的注释,可用于符号ViewModel类。它答应以通明的方式将依靠注入到ViewModel中,这就是咱们将在这个项目中运用的。

package com.example.jetnotes.viewmodels
import android.util.Log
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.jetnotes.data.model.NoteModel
import com.example.jetnotes.repository.NoteRepo
import com.example.jetnotes.uitl.ResultState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class NoteViewModel @Inject constructor(
    private val noteRepo: NoteRepo
): ViewModel(){
    val id: MutableState<Int> = mutableIntStateOf(0)
    val title: MutableState<String> = mutableStateOf("")
    val description: MutableState<String> = mutableStateOf("")
    private val _getNotes = MutableStateFlow<ResultState<List<NoteModel>>>(ResultState.Idle)
    val getNotes: StateFlow<ResultState<List<NoteModel>>> = _getNotes
    private val _selectedNote: MutableStateFlow<NoteModel?> = MutableStateFlow(null)
    val selectedNote: StateFlow<NoteModel?> = _selectedNote
    fun getNotes() {
        viewModelScope.launch {
            val result = try {
                noteRepo.getAllNotes().collect {
                    _getNotes.value = ResultState.Success(it)
                }
            } catch (error: Exception) {
                ResultState.Error(error)
            }
            Log.d("RESULT_STATE", "$result")
        }
    }
    private fun insertNote() {
        viewModelScope.launch(Dispatchers.IO) {
            val note = NoteModel(
                title = title.value,
                description = description.value
            )
            noteRepo.insertNote(note)
        }
    }
    private fun updateNote() {
        viewModelScope.launch(Dispatchers.IO) {
            val note = NoteModel(
                id = id.value,
                title = title.value,
                description = description.value
            )
            noteRepo.updateNote(note)
        }
    }
    private fun deleteNote() {
        viewModelScope.launch(Dispatchers.IO) {
            val note = NoteModel(
                id = id.value,
                title = title.value,
                description = description.value
            )
            noteRepo.deleteNote(note)
        }
    }
    fun getSelectNoteID(noteID: Int) {
        viewModelScope.launch {
            noteRepo.selectNoteID(noteID).collect {
                _selectedNote.value = it
            }
        }
    }
    fun updateNotesFields(selectedNote: NoteModel?) {
        if (selectedNote != null) {
            id.value = selectedNote.id
            title.value = selectedNote.title
            description.value = selectedNote.description
        } else {
            // clear all fields
            id.value = 0
            title.value = ""
            description.value = ""
        }
    }
    fun validateFields(): Boolean {
        return title.value.isNotEmpty() && description.value.isNotEmpty()
    }
    fun dbHandle(action: String): String {
        var result = action
        when(result) {
            "INSERT" -> insertNote()
            "UPDATE" -> updateNote()
            "DELETE" -> deleteNote()
            else -> { result = "NO_EVENT" }
        }
        return result
    }
}

十二、创立App HomeView或App MainScreen

视图是运用程序的表明层,负责向用户显现数据并捕获用户交互,例如taps、clicks和gestures。

它不该包括业务逻辑,而应侧重于直观地表明运用程序数据。 视图一般由用户界面组件组成,例如按钮、文本输入框、列表和图标。

package com.example.jetnotes.ui.screens
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.navigation.NavHostController
import com.example.jetnotes.R
import com.example.jetnotes.ui.components.HomeContent
import com.example.jetnotes.viewmodels.NoteViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(navHostController: NavHostController, noteViewModel: NoteViewModel) {
    LaunchedEffect(key1 = Unit) {
        noteViewModel.getNotes()
    }
    val getAllNotes by noteViewModel.getNotes.collectAsState()
    Scaffold(
        topBar = {
            TopAppBar(
                modifier = Modifier
                    .fillMaxWidth(),
                title = {
                    Text(stringResource(id = R.string.home_app_bar), fontWeight = FontWeight.Bold)
                })
        },
        content = {it
            HomeContent(
                modifier = Modifier.padding(it),
                getAllNotes = getAllNotes,
                navController = navHostController,
                onDelete = { event, note ->
                    noteViewModel.dbHandle(event)
                    noteViewModel.updateNotesFields(note)
                })
        },
        floatingActionButtonPosition = FabPosition.Center,
        floatingActionButton = {
            FloatingActionButton(
                onClick = {
                    navHostController.navigate("note_screen") {
                        popUpTo("note_screen") {
                            inclusive = true
                        }
                    }
                }
            ) {
                Icon(Icons.Default.Add, "Floating action button.")
            }
        }
    )
}

十三、创立检查主页内容

HomeContent是一个用户界面组件,代表主屏幕上的项目。它旨在从翻开运用程序的那一刻起就供给信息丰厚的用户体会。它能够包括多种元素,例如:

内容列表:在AppBar下方,HomeContent一般会显现与主屏幕相关的内容列表。这或许包括新闻、状况更新、特征产品或对用户重要的任何其他类型的信息。

操作按钮:HomeContent或许包括操作按钮或图标,答运用户履行特定使命、删去、更新、创立新项目或拜访运用程序的特定区域。

package com.example.jetnotes.ui..screens.home
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DeleteForever
import androidx.compose.material.icons.filled.EditNote
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.navigation.NavController
import com.example.jetnotes.data.model.NoteModel
import com.example.jetnotes.ui.theme.ALL_PADDING
import com.example.jetnotes.ui.theme.CARD_SHAPE
import com.example.jetnotes.ui.theme.ICON_SWIPE
import com.example.jetnotes.ui.theme.ICON_THRESHOLD
import com.example.jetnotes.ui.theme.Purple80
import com.example.jetnotes.ui.theme.PurpleGrey40
import com.example.jetnotes.ui.theme.TEXT_DEFAULT
import com.example.jetnotes.ui.theme.TITLE
import com.example.jetnotes.uitl.ResultState
import kotlinx.coroutines.launch
import me.saket.swipe.SwipeAction
import me.saket.swipe.SwipeableActionsBox
@Composable
fun HomeContent(
    modifier: Modifier,
    getAllNotes: ResultState<List<NoteModel>>,
    navController: NavController,
    onDelete: (String, NoteModel) -> Unit
) {
    val listNotes: List<NoteModel>
    if (getAllNotes is ResultState.Success) {
        listNotes = getAllNotes.data
        if (listNotes.isEmpty()) {
            EmptyContent()
        } else {
            LazyColumn(  
                modifier = modifier  
            ){
                items(listNotes) {note ->
                    Card(
                        modifier = Modifier
                            .fillMaxSize()
                            .padding(all = ALL_PADDING),
                        shape = RoundedCornerShape(size = CARD_SHAPE)
                    ) {
                        SwipeNote(note = note, navController = navController) {
                            onDelete(
                                "DELETE",
                                note
                            )
                        }
                    }
                }
            }
        }
    }
}
@Composable
fun SwipeNote(
    note: NoteModel,
    navController: NavController,
    onSwipe: () -> Unit = {}
) {
    val scope = rememberCoroutineScope()
    val deleteSwipe = SwipeAction(
        onSwipe = {
            scope.launch {
                onSwipe()
            }
        },
        icon = {
            Icon(
                modifier = Modifier
                    .padding(ALL_PADDING)
                    .size(ICON_SWIPE),
                imageVector = Icons.Default.DeleteForever,
                contentDescription = "",
                tint = Color.White
            )
        },
        background = Color.Red
    )
    val updateSwipe = SwipeAction(
        onSwipe = {
            navController.navigate("note_screen/${note.id}")
        },
        icon = {
            Icon(
                modifier = Modifier
                    .padding(ALL_PADDING)
                    .size(ICON_SWIPE),
                imageVector = Icons.Default.EditNote,
                contentDescription = "",
                tint = Color.White
            )
        },
        background = PurpleGrey40
    )
    SwipeableActionsBox(
        swipeThreshold = ICON_THRESHOLD,
        startActions = listOf(updateSwipe),
        endActions = listOf(deleteSwipe)
    ) {
        NoteItem(note)
    }
}
@Composable
fun NoteItem(note: NoteModel) {
    var expanded by remember { mutableStateOf(false) }
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .background(Purple80)
            .padding(all = ALL_PADDING)
    ) {
        Column(modifier = Modifier.weight(1f)) {
            Text(text = note.title, fontSize = TITLE, fontWeight = FontWeight.Bold)
            if (expanded) {
                Text(text = note.description, fontSize = TEXT_DEFAULT)
            }
        }
        IconButton(onClick = { expanded = !expanded }) {
            Icon(
                imageVector = if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
                contentDescription = if (expanded) {
                    "Menos"
                }else { "Mais" }
            )
        }
    }
}

创立注册视图

package com.example.jetnotes.ui.screens.note
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.navigation.NavController
import com.example.jetnotes.viewmodels.NoteViewModel
@Composable
fun NoteScreen(
    navController: NavController,
    noteViewModel: NoteViewModel,
    selected: Int?
) {
    selected?.let {  
        noteViewModel.getSelectNoteID(selected)  
        noteViewModel.updateNotesFields(noteViewModel.selectedNote.value)  
    }
    var title by noteViewModel.title
    var description by noteViewModel.description
    Scaffold(
        topBar = {
            if (selected == null) {
                CreateNoteTopBar(navController = navController, noteViewModel = noteViewModel)
            } else {
                UpdateNoteTopBar(navController = navController, noteViewModel = noteViewModel)
            }
        }
    ) { it ->
        NoteContent(
            modifier = Modifier.padding(it),
            title = title,
            onTitle = { title = it},
            description = description,
            onDescription = { description = it }
        )
    }
}

创立笔记内容

package com.example.jetnotes.ui.screens.note
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.example.jetnotes.R
import com.example.jetnotes.ui.theme.ALL_PADDING
import com.example.jetnotes.ui.theme.SPACER_HEIGHT
import com.example.jetnotes.ui.theme.TEXT_DEFAULT
import com.example.jetnotes.ui.theme.surface
@Composable
fun NoteContent(
    modifier: Modifier,
    title: String,
    onTitle: (String) -> Unit,
    description: String,
    onDescription: (String) -> Unit
) {
    val keyboardController = LocalSoftwareKeyboardController.current
    Column(
        modifier = modifier
            .fillMaxSize()
            .background(surface)
            .padding(ALL_PADDING)
    ) {
        OutlinedTextField(
            modifier = Modifier.fillMaxWidth(),
            value = title,
            onValueChange = {onTitle(it)},
            label = { Text(text = stringResource(id = R.string.note_title), fontSize = TEXT_DEFAULT) },
            placeholder = { Text(text = stringResource(id = R.string.place_holder_note_title), fontSize = TEXT_DEFAULT)},
            keyboardOptions = KeyboardOptions(
                keyboardType = KeyboardType.Text,
                imeAction = ImeAction.Next
            )
        )
        Spacer(modifier = Modifier.height(SPACER_HEIGHT))
        OutlinedTextField(
            modifier = Modifier.fillMaxWidth(),
            value = description, 
            onValueChange = {onDescription(it)},
            label = { Text(text = stringResource(id = R.string.note_description), fontSize = TEXT_DEFAULT) },
            placeholder = { Text(text = stringResource(id = R.string.place_holder_note_description), fontSize = TEXT_DEFAULT)},
            keyboardOptions = KeyboardOptions(
                keyboardType = KeyboardType.Text,
                imeAction = ImeAction.Done
            ),
            keyboardActions = KeyboardActions ( 
                onDone = {
                    keyboardController?.hide()
                }
            )
        )
    }
}

十四、从运用程序创立NoteTopApp

屏幕顶部一般包括一个运用程序栏,其间或许包括运用程序Logo和标题,以及搜索或菜单等操作按钮。

package com.example.jetnotes.ui.screens.note
import android.util.Log
import android.widget.Toast
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.EditNote
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
import com.example.jetnotes.R
import com.example.jetnotes.viewmodels.NoteViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CreateNoteTopBar(navController: NavController, noteViewModel: NoteViewModel) {
    val context = LocalContext.current
    TopAppBar(
        modifier = Modifier.fillMaxWidth(),
        title = { Text(text = stringResource(id = R.string.create_app_bar)) },
        navigationIcon = {
            IconButton(onClick = {
                navController.navigate("home_screen") {
                    popUpTo("home_screen") {
                        inclusive = true
                    }
                }
            }) {
                Icon(
                    imageVector = Icons.AutoMirrored.Filled.ArrowBack,
                    contentDescription = stringResource(id = R.string.icon_arrow_back)
                )
            }
        },
        actions = {
            IconButton(onClick = {
                if (noteViewModel.validateFields()) {
                    val insertResult = noteViewModel.dbHandle("INSERT")
                    if (insertResult == "INSERT") {
                        Toast.makeText(context, "Recorded successfully!!", Toast.LENGTH_LONG).show()
                        navController.navigate("home_screen")
                    } else {
                        Toast.makeText(context, "Recording error!!", Toast.LENGTH_LONG).show()
                    }
                    Log.d("RESULTINSERT", insertResult)
                } else {
                    Toast.makeText(context, "Fill in the fields!!", Toast.LENGTH_LONG).show()
                }
            }) {
                Icon(
                    imageVector = Icons.Default.Check,
                    contentDescription = stringResource(id = R.string.icon_check)
                )
            }
        }
    )
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun UpdateNoteTopBar(navController: NavController, noteViewModel: NoteViewModel) {
    val context = LocalContext.current
    TopAppBar(
        modifier = Modifier.fillMaxWidth(),
        title = { Text(text = stringResource(id = R.string.update_app_bar)) },
        navigationIcon = {
            IconButton(onClick = {
                navController.navigate("home_screen") {
                    popUpTo("home_screen") {
                        inclusive = true
                    }
                }
            }) {
                Icon(
                    imageVector = Icons.Default.Close,
                    contentDescription = stringResource(id = R.string.icon_arrow_back)
                )
            }
        },
        actions = {
            IconButton(onClick = {
                if (noteViewModel.validateFields()) {
                    val updateResult = noteViewModel.dbHandle("UPDATE")
                    if (updateResult == "UPDATE") {
                        Toast.makeText(context, "Update successfully!!", Toast.LENGTH_LONG).show()
                        navController.navigate("home_screen")
                    } else {
                        Toast.makeText(context, "Recording error!!", Toast.LENGTH_LONG).show()
                    }
                    Log.d("RESULT_UPDATE", updateResult)
                } else {
                    Toast.makeText(context, "Filled in the fields!!", Toast.LENGTH_LONG).show()
                }
            }) {
                Icon(
                    imageVector = Icons.Default.EditNote,
                    contentDescription = stringResource(id = R.string.icon_check)
                )
            }
        }
    )
}

十五、创立EmptyContent

咱们将开发UI组件,作为运用程序屏幕特定部分中缺失内容的符号。当没有要显现的数据或许列表或部分为空时,将运用它。

package com.example.jetnotes.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.SentimentVeryDissatisfied
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.example.jetnotes.R
import com.example.jetnotes.ui.theme.ICON_EMPTY
import com.example.jetnotes.ui.theme.Purple40
import com.example.jetnotes.ui.theme.surface
@Composable
fun EmptyContent() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(surface),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Icon(
            modifier = Modifier
                .size(ICON_EMPTY),
            imageVector = Icons.Default.SentimentVeryDissatisfied,
            contentDescription = stringResource(id = R.string.icon_empty),
            tint = Purple40
        )
        Text(
            text = "nn" + stringResource(id = R.string.text_empty)
        )
    }
}

下面是演示效果:

Kotlin、Room和Jetpack Compose的运用开发之练练手

Github链接:JetNotes

十六、结论

运用洁净的代码、MVVM架构、Kotlin、Jetpack Compose和Android Studio进行Android运用开发时创立满意用户希望和高质量运用程序的有用办法。选用良好的开发实践,包括关注点分离、测试和充足的文档,有助于保护洁净、可保护的代码。经过遵循这些实践,您能够创立更高效、可扩展而且随着运用程序的开展和开展而更易于保护的运用程序。

翻译自文章:Desenvolvimento de Aplicativos com Kotlin, Room e Jetpack Compose: Uma Jornada para Interfaces de Usurio Modernas e Eficientes: 作者:Gilber