├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── values
│ │ │ │ ├── themes.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── strings.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ └── drawable
│ │ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ │ └── ro
│ │ │ │ └── alexmamo
│ │ │ │ └── roomjetpackcompose
│ │ │ │ ├── RoomJetpackComposeApp.kt
│ │ │ │ ├── domain
│ │ │ │ ├── model
│ │ │ │ │ ├── Response.kt
│ │ │ │ │ └── Book.kt
│ │ │ │ └── repository
│ │ │ │ │ └── BookRepository.kt
│ │ │ │ ├── data
│ │ │ │ ├── network
│ │ │ │ │ └── BookDb.kt
│ │ │ │ ├── repository
│ │ │ │ │ └── BookRepositoryImpl.kt
│ │ │ │ └── dao
│ │ │ │ │ └── BookDao.kt
│ │ │ │ ├── navigation
│ │ │ │ ├── Route.kt
│ │ │ │ └── NavGraph.kt
│ │ │ │ ├── presentation
│ │ │ │ ├── book_list
│ │ │ │ │ ├── components
│ │ │ │ │ │ ├── TitleText.kt
│ │ │ │ │ │ ├── AuthorText.kt
│ │ │ │ │ │ ├── BookListTopBar.kt
│ │ │ │ │ │ ├── InsertBookFloatingActionButton.kt
│ │ │ │ │ │ ├── EmptyBookListContent.kt
│ │ │ │ │ │ ├── AuthorTextField.kt
│ │ │ │ │ │ ├── TitleTextField.kt
│ │ │ │ │ │ ├── BookCard.kt
│ │ │ │ │ │ ├── BookListContent.kt
│ │ │ │ │ │ ├── InsertBookAlertDialog.kt
│ │ │ │ │ │ └── EditableBookCard.kt
│ │ │ │ │ ├── BookListViewModel.kt
│ │ │ │ │ └── BookListScreen.kt
│ │ │ │ ├── MainActivity.kt
│ │ │ │ └── book_details
│ │ │ │ │ ├── BookDetailsScreen.kt
│ │ │ │ │ └── components
│ │ │ │ │ ├── BookDetailsContent.kt
│ │ │ │ │ └── BookDetailsTopBar.kt
│ │ │ │ ├── components
│ │ │ │ ├── ActionButton.kt
│ │ │ │ ├── LoadingIndicator.kt
│ │ │ │ └── ActionIconButton.kt
│ │ │ │ ├── core
│ │ │ │ └── Utils.kt
│ │ │ │ └── di
│ │ │ │ └── AppModule.kt
│ │ └── AndroidManifest.xml
│ └── androidTest
│ │ └── java
│ │ └── ro
│ │ └── alexmamo
│ │ └── roomjetpackcompose
│ │ ├── HiltTestRunner.kt
│ │ ├── utils
│ │ └── Utils.kt
│ │ ├── data
│ │ ├── repository
│ │ │ └── FakeBookRepositoryImpl.kt
│ │ └── dao
│ │ │ └── BookDaoTest.kt
│ │ ├── presentation
│ │ └── book_list
│ │ │ ├── components
│ │ │ ├── BookListContentTest.kt
│ │ │ ├── BookListTopBarTest.kt
│ │ │ └── InsertBookAlertDialogTest.kt
│ │ │ └── BookListScreenTest.kt
│ │ ├── di
│ │ └── AppModuleTest.kt
│ │ ├── domain
│ │ └── BookRepositoryTest.kt
│ │ └── navigation
│ │ └── BookNavigationTest.kt
├── proguard-rules.pro
└── build.gradle.kts
├── .idea
├── .gitignore
├── kotlinc.xml
├── vcs.xml
├── deploymentTargetDropDown.xml
├── compiler.xml
├── migrations.xml
├── misc.xml
├── gradle.xml
├── runConfigurations.xml
├── deploymentTargetSelector.xml
├── inspectionProfiles
│ └── Project_Default.xml
└── androidTestResultsUserPreferences.xml
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── .gitignore
├── settings.gradle
├── gradle.properties
├── README.md
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexMamo/RoomJetpackCompose/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexMamo/RoomJetpackCompose/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexMamo/RoomJetpackCompose/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexMamo/RoomJetpackCompose/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexMamo/RoomJetpackCompose/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexMamo/RoomJetpackCompose/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexMamo/RoomJetpackCompose/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexMamo/RoomJetpackCompose/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexMamo/RoomJetpackCompose/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexMamo/RoomJetpackCompose/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexMamo/RoomJetpackCompose/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/RoomJetpackComposeApp.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class RoomJetpackComposeApp : Application()
--------------------------------------------------------------------------------
/.idea/deploymentTargetDropDown.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Feb 17 10:18:41 EET 2025
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "RoomJetpackCompose"
16 | include ':app'
17 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/domain/model/Response.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.domain.model
2 |
3 | sealed class Response {
4 | data object Idle : Response()
5 |
6 | data object Loading : Response()
7 |
8 | data class Success(
9 | val data: T
10 | ) : Response()
11 |
12 | data class Failure(
13 | val e: Exception
14 | ) : Response()
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/data/network/BookDb.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.data.network
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import ro.alexmamo.roomjetpackcompose.data.dao.BookDao
6 | import ro.alexmamo.roomjetpackcompose.domain.model.Book
7 |
8 | @Database(
9 | entities = [Book::class],
10 | version = 1,
11 | exportSchema = false
12 | )
13 | abstract class BookDb : RoomDatabase() {
14 | abstract val bookDao: BookDao
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/domain/repository/BookRepository.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.domain.repository
2 |
3 | import kotlinx.coroutines.flow.Flow
4 | import ro.alexmamo.roomjetpackcompose.domain.model.Book
5 |
6 | interface BookRepository {
7 | fun getBookList(): Flow>
8 |
9 | suspend fun getBookById(id: Int): Book?
10 |
11 | suspend fun insertBook(book: Book)
12 |
13 | suspend fun updateBook(book: Book)
14 |
15 | suspend fun deleteBook(book: Book)
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/navigation/Route.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.navigation
2 |
3 | import kotlinx.serialization.Serializable
4 | import ro.alexmamo.roomjetpackcompose.domain.model.Book
5 |
6 | @Serializable
7 | object BookListScreen
8 |
9 | @Serializable
10 | data class BookDetails(
11 | val id: Int,
12 | val title: String,
13 | val author: String
14 | )
15 |
16 | fun BookDetails.toBook() = Book(
17 | id = this.id,
18 | title = this.title,
19 | author = this.author
20 | )
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/TitleText.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.presentation.book_list.components
2 |
3 | import androidx.compose.material.Text
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.graphics.Color
6 | import androidx.compose.ui.unit.sp
7 |
8 | @Composable
9 | fun TitleText(
10 | title: String
11 | ) {
12 | Text(
13 | text = title,
14 | color = Color.DarkGray,
15 | fontSize = 25.sp
16 | )
17 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/HiltTestRunner.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import androidx.test.runner.AndroidJUnitRunner
6 | import dagger.hilt.android.testing.HiltTestApplication
7 |
8 | class HiltTestRunner : AndroidJUnitRunner() {
9 | override fun newApplication(
10 | cl: ClassLoader?,
11 | className: String?,
12 | context: Context?
13 | ): Application {
14 | return super.newApplication(cl, HiltTestApplication::class.java.name, context)
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/domain/model/Book.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.domain.model
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 | import ro.alexmamo.roomjetpackcompose.core.BOOK_TABLE
6 | import ro.alexmamo.roomjetpackcompose.navigation.BookDetails
7 |
8 | @Entity(tableName = BOOK_TABLE)
9 | data class Book(
10 | @PrimaryKey(autoGenerate = true)
11 | val id: Int,
12 | val title: String,
13 | val author: String
14 | )
15 |
16 | fun Book.toBookDetails() = BookDetails(
17 | id = this.id,
18 | title = this.title,
19 | author = this.author
20 | )
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/ActionButton.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.components
2 |
3 | import androidx.compose.material.Button
4 | import androidx.compose.material.Text
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.res.stringResource
7 |
8 | @Composable
9 | fun ActionButton(
10 | onActionButtonClick: () -> Unit,
11 | resourceId: Int
12 | ) {
13 | Button(
14 | onClick = onActionButtonClick
15 | ) {
16 | Text(
17 | text = stringResource(
18 | id = resourceId
19 | )
20 | )
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/LoadingIndicator.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.components
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.material.CircularProgressIndicator
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Alignment
8 | import androidx.compose.ui.Modifier
9 |
10 | @Composable
11 | fun LoadingIndicator() {
12 | Box(
13 | modifier = Modifier.fillMaxSize(),
14 | contentAlignment = Alignment.Center
15 | ){
16 | CircularProgressIndicator()
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/AuthorText.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.presentation.book_list.components
2 |
3 | import androidx.compose.material.Text
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.graphics.Color
6 | import androidx.compose.ui.text.style.TextDecoration
7 | import androidx.compose.ui.unit.sp
8 |
9 | @Composable
10 | fun AuthorText(
11 | author: String
12 | ) {
13 | Text(
14 | text = "by $author",
15 | color = Color.DarkGray,
16 | fontSize = 12.sp,
17 | textDecoration = TextDecoration.Underline
18 | )
19 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/utils/Utils.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.utils
2 |
3 | import android.content.Context
4 | import ro.alexmamo.roomjetpackcompose.R
5 | import ro.alexmamo.roomjetpackcompose.domain.model.Book
6 |
7 | fun getBookTest(context: Context): Book {
8 | return Book(
9 | id = 1,
10 | title = context.getString(R.string.title_test),
11 | author = context.getString(R.string.author_test)
12 | )
13 | }
14 |
15 | fun getUpdatedBookTest(context: Context): Book {
16 | return getBookTest(context).copy(
17 | title = context.getString(R.string.new_title_test)
18 | )
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/BookListTopBar.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.presentation.book_list.components
2 |
3 | import androidx.compose.material.Text
4 | import androidx.compose.material.TopAppBar
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.res.stringResource
7 | import ro.alexmamo.roomjetpackcompose.R
8 |
9 | @Composable
10 | fun BookListTopBar() {
11 | TopAppBar (
12 | title = {
13 | Text(
14 | text = stringResource(
15 | id = R.string.book_list_screen_title
16 | )
17 | )
18 | }
19 | )
20 | }
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.presentation
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.navigation.compose.rememberNavController
7 | import dagger.hilt.android.AndroidEntryPoint
8 | import ro.alexmamo.roomjetpackcompose.navigation.NavGraph
9 |
10 | @AndroidEntryPoint
11 | class MainActivity : ComponentActivity() {
12 | override fun onCreate(savedInstanceState: Bundle?) {
13 | super.onCreate(savedInstanceState)
14 | setContent {
15 | NavGraph(
16 | navController = rememberNavController()
17 | )
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/ActionIconButton.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.components
2 |
3 | import androidx.compose.material.Icon
4 | import androidx.compose.material.IconButton
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.graphics.vector.ImageVector
7 | import androidx.compose.ui.res.stringResource
8 |
9 | @Composable
10 | fun ActionIconButton(
11 | onActionIconButtonClick: () -> Unit,
12 | imageVector: ImageVector,
13 | resourceId: Int
14 | ) {
15 | IconButton(
16 | onClick = onActionIconButtonClick
17 | ) {
18 | Icon(
19 | imageVector = imageVector,
20 | contentDescription = stringResource(
21 | id = resourceId
22 | )
23 | )
24 | }
25 | }
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/data/repository/BookRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.data.repository
2 |
3 | import ro.alexmamo.roomjetpackcompose.data.dao.BookDao
4 | import ro.alexmamo.roomjetpackcompose.domain.model.Book
5 | import ro.alexmamo.roomjetpackcompose.domain.repository.BookRepository
6 |
7 | class BookRepositoryImpl(
8 | private val bookDao: BookDao
9 | ) : BookRepository {
10 | override fun getBookList() = bookDao.getBookList()
11 |
12 | override suspend fun getBookById(id: Int) = bookDao.getBookById(id)
13 |
14 | override suspend fun insertBook(book: Book) = bookDao.insertBook(book)
15 |
16 | override suspend fun updateBook(book: Book) = bookDao.updateBook(book)
17 |
18 | override suspend fun deleteBook(book: Book) = bookDao.deleteBook(book)
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/data/dao/BookDao.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.data.dao
2 |
3 | import androidx.room.*
4 | import androidx.room.OnConflictStrategy.Companion.IGNORE
5 | import kotlinx.coroutines.flow.Flow
6 | import ro.alexmamo.roomjetpackcompose.core.BOOK_TABLE
7 | import ro.alexmamo.roomjetpackcompose.domain.model.Book
8 |
9 | @Dao
10 | interface BookDao {
11 | @Query("SELECT * FROM $BOOK_TABLE ORDER BY id ASC")
12 | fun getBookList(): Flow>
13 |
14 | @Query("SELECT * FROM $BOOK_TABLE WHERE id = :id")
15 | suspend fun getBookById(id: Int): Book
16 |
17 | @Insert(onConflict = IGNORE)
18 | suspend fun insertBook(book: Book)
19 |
20 | @Update
21 | suspend fun updateBook(book: Book)
22 |
23 | @Delete
24 | suspend fun deleteBook(book: Book)
25 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/core/Utils.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.core
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import android.widget.Toast.LENGTH_LONG
6 | import android.widget.Toast.makeText
7 | import androidx.compose.material.SnackbarHostState
8 | import kotlinx.coroutines.CoroutineScope
9 | import kotlinx.coroutines.launch
10 |
11 | const val TAG = "AppTag"
12 | const val BOOK_TABLE = "book_table"
13 | const val AUTHOR_FIELD = "author"
14 | const val TITLE_FIELD = "title"
15 |
16 | fun logMessage(
17 | message: String
18 | ) = Log.e(TAG, message)
19 |
20 | fun showToastMessage(
21 | context: Context,
22 | message: String
23 | ) = makeText(context, message, LENGTH_LONG).show()
24 |
25 | fun showSnackbarMessage(
26 | coroutineScope: CoroutineScope,
27 | snackbarHostState: SnackbarHostState,
28 | message: String
29 | ) = coroutineScope.launch {
30 | snackbarHostState.showSnackbar(message)
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_details/BookDetailsScreen.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.presentation.book_details
2 |
3 | import androidx.compose.material.Scaffold
4 | import androidx.compose.runtime.Composable
5 | import ro.alexmamo.roomjetpackcompose.domain.model.Book
6 | import ro.alexmamo.roomjetpackcompose.presentation.book_details.components.BookDetailsContent
7 | import ro.alexmamo.roomjetpackcompose.presentation.book_details.components.BookDetailsTopBar
8 |
9 | @Composable
10 | fun BookDetailsScreen(
11 | book: Book,
12 | navigateBack: () -> Unit
13 | ) {
14 | Scaffold(
15 | topBar = {
16 | BookDetailsTopBar(
17 | onArrowBackIconClick = navigateBack
18 | )
19 | },
20 | content = { innerPadding ->
21 | BookDetailsContent(
22 | innerPadding = innerPadding,
23 | book = book
24 | )
25 | }
26 | )
27 | }
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/InsertBookFloatingActionButton.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.presentation.book_list.components
2 |
3 | import androidx.compose.material.FloatingActionButton
4 | import androidx.compose.material.Icon
5 | import androidx.compose.material.MaterialTheme
6 | import androidx.compose.material.icons.Icons
7 | import androidx.compose.material.icons.filled.Add
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.res.stringResource
10 | import ro.alexmamo.roomjetpackcompose.R
11 |
12 | @Composable
13 | fun InsertBookFloatingActionButton(
14 | onInsertBookFloatingActionButtonClick: () -> Unit
15 | ) {
16 | FloatingActionButton(
17 | backgroundColor = MaterialTheme.colors.primary,
18 | onClick = onInsertBookFloatingActionButtonClick
19 | ) {
20 | Icon(
21 | imageVector = Icons.Default.Add,
22 | contentDescription = stringResource(
23 | id = R.string.open_insert_book_dialog
24 | )
25 | )
26 | }
27 | }
--------------------------------------------------------------------------------
/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/EmptyBookListContent.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.presentation.book_list.components
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.PaddingValues
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.res.stringResource
12 | import androidx.compose.ui.unit.sp
13 | import ro.alexmamo.roomjetpackcompose.R
14 |
15 | @Composable
16 | fun EmptyBookListContent(
17 | innerPadding: PaddingValues
18 | ) {
19 | Box(
20 | modifier = Modifier.fillMaxSize().padding(innerPadding),
21 | contentAlignment = Alignment.Center,
22 | ){
23 | Text(
24 | text = stringResource(
25 | id = R.string.empty_book_list_text
26 | ),
27 | fontSize = 18.sp
28 | )
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_details/components/BookDetailsContent.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.presentation.book_details.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.PaddingValues
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.unit.dp
10 | import ro.alexmamo.roomjetpackcompose.domain.model.Book
11 | import ro.alexmamo.roomjetpackcompose.presentation.book_list.components.AuthorText
12 | import ro.alexmamo.roomjetpackcompose.presentation.book_list.components.TitleText
13 |
14 | @Composable
15 | fun BookDetailsContent(
16 | innerPadding: PaddingValues,
17 | book: Book
18 | ) {
19 | Column(
20 | modifier = Modifier.fillMaxSize().padding(innerPadding).padding(8.dp)
21 | ) {
22 | TitleText(
23 | title = book.title
24 | )
25 | AuthorText(
26 | author = book.author
27 | )
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/data/repository/FakeBookRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.data.repository
2 |
3 | import kotlinx.coroutines.flow.flow
4 | import ro.alexmamo.roomjetpackcompose.domain.model.Book
5 | import ro.alexmamo.roomjetpackcompose.domain.repository.BookRepository
6 |
7 | class FakeBookRepositoryImpl() : BookRepository {
8 | private val bookList = mutableListOf()
9 |
10 | override fun getBookList() = flow {
11 | emit(bookList)
12 | }
13 |
14 | override suspend fun getBookById(id: Int) = bookList.find { book ->
15 | book.id == id
16 | }
17 |
18 | override suspend fun insertBook(book: Book) {
19 | bookList.add(book)
20 | }
21 |
22 | override suspend fun updateBook(book: Book) {
23 | val indexOfFirstBook = bookList.indexOfFirst { firstBook ->
24 | firstBook.id == book.id
25 | }
26 | if (indexOfFirstBook != -1) {
27 | bookList[indexOfFirstBook] = book
28 | }
29 | }
30 |
31 | override suspend fun deleteBook(book: Book) {
32 | bookList.remove(book)
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/AuthorTextField.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.presentation.book_list.components
2 |
3 | import androidx.compose.material.Text
4 | import androidx.compose.material.TextField
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.mutableStateOf
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.runtime.setValue
10 | import androidx.compose.ui.res.stringResource
11 | import ro.alexmamo.roomjetpackcompose.R
12 |
13 | @Composable
14 | fun AuthorTextField(
15 | author: String,
16 | onUpdateAuthor: (String) -> Unit
17 | ) {
18 | var author by remember { mutableStateOf(author) }
19 |
20 | TextField(
21 | value = author,
22 | onValueChange = { newAuthor ->
23 | author = newAuthor
24 | onUpdateAuthor(newAuthor)
25 | },
26 | placeholder = {
27 | Text(
28 | text = stringResource(
29 | id = R.string.book_author
30 | )
31 | )
32 | }
33 | )
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_details/components/BookDetailsTopBar.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.presentation.book_details.components
2 |
3 | import androidx.compose.material.Text
4 | import androidx.compose.material.TopAppBar
5 | import androidx.compose.material.icons.Icons
6 | import androidx.compose.material.icons.automirrored.outlined.ArrowBack
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.res.stringResource
9 | import ro.alexmamo.roomjetpackcompose.R
10 | import ro.alexmamo.roomjetpackcompose.components.ActionIconButton
11 |
12 | @Composable
13 | fun BookDetailsTopBar(
14 | onArrowBackIconClick: () -> Unit
15 | ) {
16 | TopAppBar (
17 | title = {
18 | Text(
19 | text = stringResource(
20 | id = R.string.book_details_screen_title
21 | )
22 | )
23 | },
24 | navigationIcon = {
25 | ActionIconButton(
26 | onActionIconButtonClick = onArrowBackIconClick,
27 | imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
28 | resourceId = R.string.navigate_back
29 | )
30 | }
31 | )
32 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/BookListContentTest.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.presentation.book_list.components
2 |
3 | import androidx.annotation.StringRes
4 | import androidx.compose.ui.test.assertIsDisplayed
5 | import androidx.compose.ui.test.junit4.createAndroidComposeRule
6 | import androidx.compose.ui.test.onNodeWithText
7 | import dagger.hilt.android.testing.HiltAndroidRule
8 | import dagger.hilt.android.testing.HiltAndroidTest
9 | import org.junit.Rule
10 | import org.junit.Test
11 | import ro.alexmamo.roomjetpackcompose.R
12 | import ro.alexmamo.roomjetpackcompose.presentation.MainActivity
13 |
14 | @HiltAndroidTest
15 | class BookListContentTest {
16 | @get:Rule(order = 0)
17 | var hiltRule = HiltAndroidRule(this)
18 |
19 | @get:Rule(order = 1)
20 | val composeTestRule = createAndroidComposeRule()
21 |
22 | @Test
23 | fun testBookListContent() {
24 | composeTestRule.apply {
25 | onNodeWithText(getString(R.string.empty_book_list_text))
26 | .assertIsDisplayed()
27 | }
28 | }
29 |
30 | private fun getString(@StringRes resId: Int) = composeTestRule.activity.getString(resId)
31 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/BookListTopBarTest.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.presentation.book_list.components
2 |
3 | import androidx.annotation.StringRes
4 | import androidx.compose.ui.test.assertIsDisplayed
5 | import androidx.compose.ui.test.junit4.createAndroidComposeRule
6 | import androidx.compose.ui.test.onNodeWithText
7 | import dagger.hilt.android.testing.HiltAndroidRule
8 | import dagger.hilt.android.testing.HiltAndroidTest
9 | import org.junit.Rule
10 | import org.junit.Test
11 | import ro.alexmamo.roomjetpackcompose.R
12 | import ro.alexmamo.roomjetpackcompose.presentation.MainActivity
13 |
14 | @HiltAndroidTest
15 | class BookListTopBarTest {
16 | @get:Rule(order = 0)
17 | var hiltRule = HiltAndroidRule(this)
18 |
19 | @get:Rule(order = 1)
20 | val composeTestRule = createAndroidComposeRule()
21 |
22 | @Test
23 | fun testBookListTopBar() {
24 | composeTestRule.apply {
25 | onNodeWithText(getString(R.string.book_list_screen_title))
26 | .assertIsDisplayed()
27 | }
28 | }
29 |
30 | private fun getString(@StringRes resId: Int) = composeTestRule.activity.getString(resId)
31 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/InsertBookAlertDialogTest.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.presentation.book_list.components
2 |
3 | import androidx.annotation.StringRes
4 | import androidx.compose.ui.test.assertIsDisplayed
5 | import androidx.compose.ui.test.junit4.createAndroidComposeRule
6 | import androidx.compose.ui.test.onNodeWithContentDescription
7 | import dagger.hilt.android.testing.HiltAndroidRule
8 | import dagger.hilt.android.testing.HiltAndroidTest
9 | import org.junit.Rule
10 | import org.junit.Test
11 | import ro.alexmamo.roomjetpackcompose.R
12 | import ro.alexmamo.roomjetpackcompose.presentation.MainActivity
13 |
14 | @HiltAndroidTest
15 | class InsertBookAlertDialogTest {
16 | @get:Rule(order = 0)
17 | var hiltRule = HiltAndroidRule(this)
18 |
19 | @get:Rule(order = 1)
20 | val composeTestRule = createAndroidComposeRule()
21 |
22 | @Test
23 | fun testInsertBookFloatingActionButton() {
24 | composeTestRule.apply {
25 | onNodeWithContentDescription(getString(R.string.open_insert_book_dialog))
26 | .assertIsDisplayed()
27 | }
28 | }
29 |
30 | private fun getString(@StringRes resId: Int) = composeTestRule.activity.getString(resId)
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/di/AppModule.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.di
2 |
3 | import android.content.Context
4 | import androidx.room.Room
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.android.qualifiers.ApplicationContext
9 | import dagger.hilt.components.SingletonComponent
10 | import ro.alexmamo.roomjetpackcompose.R
11 | import ro.alexmamo.roomjetpackcompose.data.dao.BookDao
12 | import ro.alexmamo.roomjetpackcompose.data.network.BookDb
13 | import ro.alexmamo.roomjetpackcompose.data.repository.BookRepositoryImpl
14 | import ro.alexmamo.roomjetpackcompose.domain.repository.BookRepository
15 |
16 | @Module
17 | @InstallIn(SingletonComponent::class)
18 | class AppModule {
19 | @Provides
20 | fun provideBookDb(
21 | @ApplicationContext
22 | context: Context
23 | ) = Room.databaseBuilder(
24 | context,
25 | BookDb::class.java,
26 | context.resources.getString(R.string.db_name)
27 | ).build()
28 |
29 | @Provides
30 | fun provideBookDao(
31 | bookDb: BookDb
32 | ) = bookDb.bookDao
33 |
34 | @Provides
35 | fun provideBookRepository(
36 | bookDao: BookDao
37 | ): BookRepository = BookRepositoryImpl(
38 | bookDao = bookDao
39 | )
40 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/di/AppModuleTest.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.di
2 |
3 | import android.content.Context
4 | import androidx.room.Room
5 | import androidx.test.core.app.ApplicationProvider
6 | import dagger.Module
7 | import dagger.Provides
8 | import dagger.hilt.components.SingletonComponent
9 | import dagger.hilt.testing.TestInstallIn
10 | import ro.alexmamo.roomjetpackcompose.data.network.BookDb
11 | import ro.alexmamo.roomjetpackcompose.domain.repository.BookRepository
12 | import ro.alexmamo.roomjetpackcompose.presentation.book_list.BookListViewModel
13 | import ro.alexmamo.roomjetpackcompose.data.repository.FakeBookRepositoryImpl
14 |
15 | @Module
16 | @TestInstallIn(
17 | components = [SingletonComponent::class],
18 | replaces = [AppModule::class]
19 | )
20 | class AppModuleTest {
21 | @Provides
22 | fun provideBookDb() = Room.inMemoryDatabaseBuilder(
23 | ApplicationProvider.getApplicationContext(),
24 | BookDb::class.java
25 | ).build()
26 |
27 | @Provides
28 | fun provideBookDao(
29 | bookDb: BookDb
30 | ) = bookDb.bookDao
31 |
32 | @Provides
33 | fun provideBookRepository(): BookRepository = FakeBookRepositoryImpl()
34 |
35 | @Provides
36 | fun provideBookListViewModel(
37 | repo: BookRepository
38 | ) = BookListViewModel(repo)
39 | }
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/navigation/NavGraph.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.navigation
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.navigation.NavHostController
5 | import androidx.navigation.compose.NavHost
6 | import androidx.navigation.compose.composable
7 | import androidx.navigation.toRoute
8 | import ro.alexmamo.roomjetpackcompose.domain.model.toBookDetails
9 | import ro.alexmamo.roomjetpackcompose.presentation.book_list.BookListScreen
10 | import ro.alexmamo.roomjetpackcompose.presentation.book_details.BookDetailsScreen
11 |
12 | @Composable
13 | fun NavGraph(
14 | navController: NavHostController
15 | ) {
16 | NavHost(
17 | navController = navController,
18 | startDestination = BookListScreen
19 | ) {
20 | composable {
21 | BookListScreen(
22 | navigateToBookDetailsScreen = { book ->
23 | val bookDetails = book.toBookDetails()
24 | navController.navigate(bookDetails)
25 | }
26 | )
27 | }
28 | composable { entry ->
29 | val bookDetails = entry.toRoute()
30 | val book = bookDetails.toBook()
31 | BookDetailsScreen(
32 | book = book,
33 | navigateBack = navController::navigateUp
34 | )
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | RoomJetpackCompose
4 | books_db
5 |
6 | "Book List"
7 | "Book Details"
8 |
9 | "The book list is empty."
10 | "Insert book"
11 |
12 | "Insert"
13 | "Update"
14 | "Cancel"
15 |
16 | "Edit"
17 | Delete
18 |
19 | "Open insert book dialog."
20 | Navigate back
21 |
22 | "Type a book title…"
23 | "Type a book author…"
24 |
25 | "Book %1$s cannot be empty."
26 | "No book updates performed."
27 | "Book successfully %1$s."
28 |
29 | "Title Test"
30 | "Author Test"
31 | "New Title Test"
32 |
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/TitleTextField.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.presentation.book_list.components
2 |
3 | import androidx.compose.material.Text
4 | import androidx.compose.material.TextField
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.LaunchedEffect
7 | import androidx.compose.runtime.getValue
8 | import androidx.compose.runtime.mutableStateOf
9 | import androidx.compose.runtime.remember
10 | import androidx.compose.runtime.setValue
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.focus.FocusRequester
13 | import androidx.compose.ui.focus.focusRequester
14 | import androidx.compose.ui.res.stringResource
15 | import androidx.compose.ui.text.TextRange
16 | import androidx.compose.ui.text.input.TextFieldValue
17 | import ro.alexmamo.roomjetpackcompose.R
18 |
19 | @Composable
20 | fun TitleTextField(
21 | title: String,
22 | onUpdateTitle: (String) -> Unit
23 | ) {
24 | var title by remember { mutableStateOf(TextFieldValue(
25 | text = title,
26 | selection = TextRange(title.length)
27 | )) }
28 | val focusRequester = remember { FocusRequester() }
29 |
30 | LaunchedEffect(Unit) {
31 | focusRequester.requestFocus()
32 | }
33 |
34 | TextField(
35 | modifier = Modifier.focusRequester(focusRequester),
36 | value = title,
37 | onValueChange = { newTitle ->
38 | title = newTitle
39 | onUpdateTitle(newTitle.text)
40 | },
41 | placeholder = {
42 | Text(
43 | text = stringResource(
44 | id = R.string.book_title
45 | )
46 | )
47 | }
48 | )
49 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application)
3 | alias(libs.plugins.kotlin.android)
4 | alias(libs.plugins.compose.compiler)
5 | alias(libs.plugins.devtools.ksp)
6 | alias(libs.plugins.hilt.android)
7 | alias(libs.plugins.kotlin.serialization)
8 | }
9 |
10 | android {
11 | namespace = "ro.alexmamo.roomjetpackcompose"
12 | compileSdk = 35
13 |
14 | defaultConfig {
15 | applicationId = "ro.alexmamo.roomjetpackcompose"
16 | minSdk = 21
17 | targetSdk = 35
18 | versionCode = 1
19 | versionName = "1.0"
20 |
21 | testInstrumentationRunner = "ro.alexmamo.roomjetpackcompose.HiltTestRunner"
22 | }
23 |
24 | compileOptions {
25 | sourceCompatibility = JavaVersion.VERSION_21
26 | targetCompatibility = JavaVersion.VERSION_21
27 | }
28 | kotlinOptions {
29 | jvmTarget = JavaVersion.VERSION_21.toString()
30 | }
31 | buildFeatures {
32 | compose = true
33 | }
34 | composeOptions {
35 | kotlinCompilerExtensionVersion = libs.versions.compose.get()
36 | }
37 | }
38 | dependencies {
39 | //Compose
40 | implementation(platform(libs.compose.bom))
41 | implementation(libs.compose.material)
42 | implementation(libs.compose.material.icons)
43 | //Navigation
44 | implementation(libs.navigation.compose)
45 | //Hilt Navigation Compose
46 | implementation(libs.hilt.navigation.compose)
47 | //Hilt
48 | implementation(libs.hilt)
49 | ksp(libs.hilt.compiler)
50 | //Room
51 | implementation(libs.room.runtime)
52 | implementation(libs.room.ktx)
53 | ksp(libs.room.compiler)
54 | //Serialization
55 | implementation(libs.serialization)
56 | //Tests
57 | androidTestImplementation(libs.navigation.testing)
58 | androidTestImplementation(libs.hilt.android.testing)
59 | androidTestImplementation(libs.runner)
60 | androidTestImplementation(libs.ui.test.junit4)
61 | androidTestImplementation(libs.kotlinx.coroutines.test)
62 | androidTestImplementation(libs.truth)
63 | debugImplementation(libs.ui.test.manifest)
64 | }
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/BookCard.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.presentation.book_list.components
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.material.Card
10 | import androidx.compose.material.MaterialTheme
11 | import androidx.compose.material.icons.Icons
12 | import androidx.compose.material.icons.filled.Delete
13 | import androidx.compose.material.icons.filled.Edit
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.unit.dp
17 | import ro.alexmamo.roomjetpackcompose.R
18 | import ro.alexmamo.roomjetpackcompose.components.ActionIconButton
19 | import ro.alexmamo.roomjetpackcompose.domain.model.Book
20 |
21 | @Composable
22 | fun BookCard(
23 | book: Book,
24 | onBookCardClick: () -> Unit,
25 | onEditBook: () -> Unit,
26 | onDeleteBook: () -> Unit
27 | ) {
28 | Card(
29 | modifier = Modifier.fillMaxWidth().padding(
30 | start = 8.dp,
31 | top = 4.dp,
32 | end = 8.dp,
33 | bottom = 4.dp
34 | ).clickable {
35 | onBookCardClick()
36 | },
37 | shape = MaterialTheme.shapes.small,
38 | elevation = 3.dp
39 | ) {
40 | Row(
41 | modifier = Modifier.fillMaxWidth().padding(8.dp)
42 | ) {
43 | Column {
44 | TitleText(
45 | title = book.title
46 | )
47 | AuthorText(
48 | author = book.author
49 | )
50 | }
51 | Spacer(
52 | modifier = Modifier.weight(1f)
53 | )
54 | ActionIconButton(
55 | onActionIconButtonClick = onEditBook,
56 | imageVector = Icons.Default.Edit,
57 | resourceId = R.string.edit_icon
58 | )
59 | ActionIconButton(
60 | onActionIconButtonClick = onDeleteBook,
61 | imageVector = Icons.Default.Delete,
62 | resourceId = R.string.delete_icon
63 | )
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RoomJetpackCompose
2 | It's an app built with [Kotlin][1] that shows how to perform CRUD operations in the Room database using Kotlin Flow in clean architecture using [Android Architecture Components][3] and the MVVM Architecture Pattern. For the UI it uses Jetpack Compose, Android's modern toolkit for building native UI.
3 |
4 | 
5 |
6 | Below you can find the docs for each tehnology that is used in this app:
7 |
8 | ## Firebase Products:
9 | * [Firebase Authentication][2]
10 |
11 | ## Android Architecture Components:
12 | * [ViewModel][5]
13 | * [Navigation][12]
14 |
15 | ## Dependency Injection:
16 | * [Hilt for Android][6]
17 |
18 | ## Asynchronous Programming:
19 | * [Kotlin Coroutines][7]
20 | * [Asynchronous Flow][8]
21 |
22 | ## Other Android Components:
23 | * [Jetpack Compose][9]
24 | * [Room][13]
25 |
26 | ---
27 |
28 | This repo represents the code for following article writen on the Medium publication:
29 |
30 | * [How to read data from Room using Kotlin Flow in Jetpack Compose?][10]
31 |
32 | See it also on youtube:
33 |
34 | * https://youtu.be/BIMSsgyGBKE
35 |
36 | **License**
37 | ---
38 | The code in this project is licensed under the Apache License 2.0.
39 |
40 | Copyright 2018 Google LLC
41 |
42 | Licensed under the Apache License, Version 2.0 (the "License");
43 | you may not use this file except in compliance with the License.
44 | You may obtain a copy of the License at
45 |
46 | https://www.apache.org/licenses/LICENSE-2.0
47 |
48 | Unless required by applicable law or agreed to in writing, software
49 | distributed under the License is distributed on an "AS IS" BASIS,
50 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
51 | See the License for the specific language governing permissions and
52 | limitations under the License.
53 |
54 | **Disclaimer**
55 | ---
56 | * This is not an officially supported Google product.
57 |
58 | [1]: https://kotlinlang.org/
59 | [3]: https://developer.android.com/topic/libraries/architecture
60 | [5]: https://developer.android.com/topic/libraries/architecture/viewmodel
61 | [6]: https://developer.android.com/training/dependency-injection/hilt-android
62 | [7]: https://kotlinlang.org/docs/coroutines-overview.html
63 | [8]: https://kotlinlang.org/docs/flow.html
64 | [9]: https://developer.android.com/jetpack/compose
65 | [10]: https://medium.com/firebase-tips-tricks/how-to-read-data-from-room-using-kotlin-flow-in-jetpack-compose-7a720dec35f5
66 | [12]: https://developer.android.com/guide/navigation
67 | [13]: https://developer.android.com/training/data-storage/room
68 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/BookListScreenTest.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.presentation.book_list
2 |
3 | import android.content.Context
4 | import androidx.annotation.StringRes
5 | import androidx.compose.ui.test.assertIsDisplayed
6 | import androidx.compose.ui.test.junit4.createAndroidComposeRule
7 | import androidx.compose.ui.test.onNodeWithContentDescription
8 | import androidx.compose.ui.test.onNodeWithText
9 | import androidx.compose.ui.test.performClick
10 | import androidx.compose.ui.test.performTextInput
11 | import androidx.test.core.app.ApplicationProvider
12 | import dagger.hilt.android.testing.HiltAndroidRule
13 | import dagger.hilt.android.testing.HiltAndroidTest
14 | import org.junit.Rule
15 | import org.junit.Test
16 | import ro.alexmamo.roomjetpackcompose.R
17 | import ro.alexmamo.roomjetpackcompose.presentation.MainActivity
18 | import ro.alexmamo.roomjetpackcompose.utils.getBookTest
19 |
20 | @HiltAndroidTest
21 | class BookListScreenTest {
22 | @get:Rule(order = 0)
23 | var hiltRule = HiltAndroidRule(this)
24 |
25 | @get:Rule(order = 1)
26 | val composeTestRule = createAndroidComposeRule()
27 |
28 | val context = ApplicationProvider.getApplicationContext()
29 | private val bookTest = getBookTest(context)
30 |
31 | @Test
32 | fun testBookClickAndNavigationToBookDetailsScreenAndBackToBookListScreen() {
33 | composeTestRule.apply {
34 | onNodeWithContentDescription(getString(R.string.open_insert_book_dialog))
35 | .performClick()
36 | onNodeWithText(getString(R.string.book_title))
37 | .performTextInput(bookTest.title)
38 | onNodeWithText(getString(R.string.book_author))
39 | .performTextInput(bookTest.author)
40 | onNodeWithText(getString(R.string.insert_button))
41 | .performClick()
42 | onNodeWithText(bookTest.title)
43 | .performClick()
44 | onNodeWithText(getString(R.string.book_details_screen_title))
45 | .assertIsDisplayed()
46 | onNodeWithText(bookTest.title)
47 | .assertIsDisplayed()
48 | onNodeWithText("by ${bookTest.author}")
49 | .assertIsDisplayed()
50 | onNodeWithContentDescription(getString(R.string.navigate_back))
51 | .performClick()
52 | onNodeWithText(getString(R.string.book_list_screen_title))
53 | .assertIsDisplayed()
54 | }
55 | }
56 |
57 | private fun getString(@StringRes resId: Int) = composeTestRule.activity.getString(resId)
58 | }
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | gradle = "8.8.1"
3 | kotlin = "2.1.10"
4 | ksp = "2.1.10-1.0.30"
5 | hilt = "2.55"
6 | composeBom = "2025.02.00"
7 | compose = "1.5.15"
8 | navigationCompose = "2.8.7"
9 | hiltNavigationCompose = "1.2.0"
10 | room = "2.6.1"
11 | serialization = "1.7.3"
12 | #Tests
13 | runner = "1.6.2"
14 | uiTestJunit4 = "1.7.8"
15 | kotlinxCoroutinesTest = "1.9.0"
16 | truth = "1.1.3"
17 | uiTestManifest = "1.7.8"
18 |
19 | [plugins]
20 | android-application = { id = "com.android.application", version.ref = "gradle" }
21 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
22 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
23 | devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
24 | hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
25 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
26 |
27 | [libraries]
28 | compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
29 | compose-material = { module = "androidx.compose.material:material" }
30 | compose-material-icons = { module = "androidx.compose.material:material-icons-extended" }
31 | navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
32 | hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
33 | hilt = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
34 | hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
35 | room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
36 | room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
37 | room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
38 | serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization"}
39 | #Tests
40 | navigation-testing = { module = "androidx.navigation:navigation-testing", version.ref = "navigationCompose" }
41 | hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" }
42 | runner = { module = "androidx.test:runner", version.ref = "runner" }
43 | ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "uiTestJunit4" }
44 | kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" }
45 | truth = { module = "com.google.truth:truth", version.ref = "truth" }
46 | ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "uiTestManifest" }
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/BookListContent.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.presentation.book_list.components
2 |
3 | import androidx.compose.foundation.layout.PaddingValues
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.lazy.LazyColumn
7 | import androidx.compose.foundation.lazy.items
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.getValue
10 | import androidx.compose.runtime.mutableIntStateOf
11 | import androidx.compose.runtime.remember
12 | import androidx.compose.runtime.setValue
13 | import androidx.compose.ui.Modifier
14 | import ro.alexmamo.roomjetpackcompose.domain.model.Book
15 |
16 | const val NON_EXISTENT_BOOK_ID = -1
17 |
18 | @Composable
19 | fun BookListContent(
20 | innerPadding: PaddingValues,
21 | bookList: List,
22 | onBookCardClick: (Book) -> Unit,
23 | onUpdateBook: (Book) -> Unit,
24 | onEmptyBookField: (String) -> Unit,
25 | onDeleteBook: (Book) -> Unit,
26 | onNoBookUpdates: () -> Unit
27 | ) {
28 | var editBookId by remember { mutableIntStateOf(NON_EXISTENT_BOOK_ID) }
29 |
30 | LazyColumn(
31 | modifier = Modifier.fillMaxSize().padding(innerPadding)
32 | ) {
33 | items(
34 | items = bookList,
35 | key = { book ->
36 | book.id
37 | }
38 | ) { book ->
39 | if (editBookId != book.id) {
40 | BookCard(
41 | book = book,
42 | onBookCardClick = {
43 | onBookCardClick(book)
44 | },
45 | onEditBook = {
46 | editBookId = book.id
47 | },
48 | onDeleteBook = {
49 | onDeleteBook(book)
50 | editBookId = NON_EXISTENT_BOOK_ID
51 | }
52 | )
53 | } else {
54 | EditableBookCard(
55 | book = book,
56 | onUpdateBook = { updatedBook ->
57 | onUpdateBook(updatedBook)
58 | editBookId = NON_EXISTENT_BOOK_ID
59 | },
60 | onEmptyBookField = onEmptyBookField,
61 | onNoBookUpdates = {
62 | onNoBookUpdates()
63 | editBookId = NON_EXISTENT_BOOK_ID
64 | },
65 | onCancel = {
66 | editBookId = NON_EXISTENT_BOOK_ID
67 | }
68 | )
69 | }
70 | }
71 | }
72 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/domain/BookRepositoryTest.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.domain
2 |
3 | import android.content.Context
4 | import androidx.activity.ComponentActivity
5 | import androidx.compose.ui.test.junit4.createAndroidComposeRule
6 | import androidx.test.core.app.ApplicationProvider
7 | import com.google.common.truth.Truth
8 | import dagger.hilt.android.testing.HiltAndroidRule
9 | import dagger.hilt.android.testing.HiltAndroidTest
10 | import kotlinx.coroutines.flow.first
11 | import kotlinx.coroutines.runBlocking
12 | import kotlinx.coroutines.test.runTest
13 | import org.junit.Before
14 | import org.junit.Rule
15 | import org.junit.Test
16 | import ro.alexmamo.roomjetpackcompose.domain.repository.BookRepository
17 | import ro.alexmamo.roomjetpackcompose.utils.getBookTest
18 | import ro.alexmamo.roomjetpackcompose.utils.getUpdatedBookTest
19 | import javax.inject.Inject
20 |
21 | @HiltAndroidTest
22 | class BookRepositoryTest {
23 | @get:Rule(order = 0)
24 | var hiltRule = HiltAndroidRule(this)
25 |
26 | @get:Rule(order = 1)
27 | val composeTestRule = createAndroidComposeRule()
28 |
29 | @Inject
30 | lateinit var fakeRepo: BookRepository
31 |
32 | val context = ApplicationProvider.getApplicationContext()
33 | private val bookTest = getBookTest(context)
34 | private val updatedBookTest = getUpdatedBookTest(context)
35 |
36 | @Before
37 | fun init() {
38 | hiltRule.inject()
39 | }
40 |
41 | @Test
42 | fun testInsertAndGetBookById() = runBlocking {
43 | fakeRepo.insertBook(bookTest)
44 | val book = fakeRepo.getBookById(bookTest.id)
45 | Truth.assertThat(book).isEqualTo(bookTest)
46 | }
47 |
48 | @Test
49 | fun testInsertAndCheckIfBookExistsInBookList() = runBlocking {
50 | fakeRepo.insertBook(bookTest)
51 | val bookList = fakeRepo.getBookList().first()
52 | Truth.assertThat(bookTest).isIn(bookList)
53 | }
54 |
55 | @Test
56 | fun testInsertAndCheckTheSizeOfBookList() = runTest {
57 | fakeRepo.insertBook(bookTest)
58 | val bookList = fakeRepo.getBookList().first()
59 | Truth.assertThat(bookList.size).isEqualTo(1)
60 | }
61 |
62 | @Test
63 | fun testUpdateAndGetBookById() = runTest {
64 | fakeRepo.insertBook(bookTest)
65 | fakeRepo.updateBook(updatedBookTest)
66 | val book = fakeRepo.getBookById(bookTest.id)
67 | Truth.assertThat(book?.title).isEqualTo(updatedBookTest.title)
68 | }
69 |
70 | @Test
71 | @Throws(Exception::class)
72 | fun testInsertAndDeleteAndCheckTheSizeOfBookList() = runTest {
73 | fakeRepo.insertBook(bookTest)
74 | fakeRepo.deleteBook(bookTest)
75 | val bookList = fakeRepo.getBookList().first()
76 | Truth.assertThat(bookList).isEmpty()
77 | }
78 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/data/dao/BookDaoTest.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.data.dao
2 |
3 | import android.content.Context
4 | import androidx.test.core.app.ApplicationProvider
5 | import com.google.common.truth.Truth
6 | import dagger.hilt.android.testing.HiltAndroidRule
7 | import dagger.hilt.android.testing.HiltAndroidTest
8 | import kotlinx.coroutines.flow.first
9 | import kotlinx.coroutines.test.runTest
10 | import org.junit.After
11 | import org.junit.Before
12 | import org.junit.Rule
13 | import org.junit.Test
14 | import ro.alexmamo.roomjetpackcompose.data.network.BookDb
15 | import ro.alexmamo.roomjetpackcompose.utils.getBookTest
16 | import ro.alexmamo.roomjetpackcompose.utils.getUpdatedBookTest
17 | import java.io.IOException
18 | import javax.inject.Inject
19 |
20 | @HiltAndroidTest
21 | class BookDaoTest() {
22 | @get:Rule
23 | var hiltRule = HiltAndroidRule(this)
24 |
25 | @Inject
26 | lateinit var bookDao: BookDao
27 | @Inject
28 | lateinit var bookDb: BookDb
29 |
30 | val context = ApplicationProvider.getApplicationContext()
31 | private val bookTest = getBookTest(context)
32 | private val updatedBookTest = getUpdatedBookTest(context)
33 |
34 | @Before
35 | fun init() {
36 | hiltRule.inject()
37 | }
38 |
39 | @Test
40 | @Throws(Exception::class)
41 | fun testInsertAndGetBookById() = runTest {
42 | bookDao.insertBook(bookTest)
43 | val book = bookDao.getBookById(bookTest.id)
44 | Truth.assertThat(book).isEqualTo(bookTest)
45 | }
46 |
47 | @Test
48 | @Throws(Exception::class)
49 | fun testInsertAndCheckIfBookExistsInBookList() = runTest {
50 | bookDao.insertBook(bookTest)
51 | val bookList = bookDao.getBookList().first()
52 | Truth.assertThat(bookTest).isIn(bookList)
53 | }
54 |
55 | @Test
56 | @Throws(Exception::class)
57 | fun testInsertAndCheckTheSizeOfBookList() = runTest {
58 | bookDao.insertBook(bookTest)
59 | val bookList = bookDao.getBookList().first()
60 | Truth.assertThat(bookList.size).isEqualTo(1)
61 | }
62 |
63 | @Test
64 | @Throws(Exception::class)
65 | fun testUpdateAndGetBookById() = runTest {
66 | bookDao.insertBook(bookTest)
67 | bookDao.updateBook(updatedBookTest)
68 | val book = bookDao.getBookById(bookTest.id)
69 | Truth.assertThat(book.title).isEqualTo(updatedBookTest.title)
70 | }
71 |
72 | @Test
73 | @Throws(Exception::class)
74 | fun testInsertAndDeleteAndCheckTheSizeOfBookList() = runTest {
75 | bookDao.insertBook(bookTest)
76 | bookDao.deleteBook(bookTest)
77 | val bookList = bookDao.getBookList().first()
78 | Truth.assertThat(bookList).isEmpty()
79 | }
80 |
81 | @After
82 | @Throws(IOException::class)
83 | fun closeDb() {
84 | bookDb.close()
85 | }
86 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/BookListViewModel.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.presentation.book_list
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import dagger.hilt.android.lifecycle.HiltViewModel
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import kotlinx.coroutines.flow.SharingStarted
8 | import kotlinx.coroutines.flow.StateFlow
9 | import kotlinx.coroutines.flow.asStateFlow
10 | import kotlinx.coroutines.flow.map
11 | import kotlinx.coroutines.flow.stateIn
12 | import kotlinx.coroutines.launch
13 | import ro.alexmamo.roomjetpackcompose.domain.model.Book
14 | import ro.alexmamo.roomjetpackcompose.domain.model.Response
15 | import ro.alexmamo.roomjetpackcompose.domain.repository.BookRepository
16 | import javax.inject.Inject
17 |
18 | typealias InsertBookResponse = Response
19 | typealias UpdateBookResponse = Response
20 | typealias DeleteBookResponse = Response
21 |
22 | @HiltViewModel
23 | class BookListViewModel @Inject constructor(
24 | private val repo: BookRepository
25 | ) : ViewModel() {
26 | val bookListState = repo.getBookList().map { bookList ->
27 | try {
28 | Response.Success(bookList)
29 | } catch (e: Exception) {
30 | Response.Failure(e)
31 | }
32 | }.stateIn(
33 | scope = viewModelScope,
34 | started = SharingStarted.WhileSubscribed(5_000),
35 | initialValue = Response.Loading
36 | )
37 |
38 | private val _insertBookState = MutableStateFlow(Response.Idle)
39 | val insertBookState: StateFlow = _insertBookState.asStateFlow()
40 |
41 | private val _updateBookState = MutableStateFlow(Response.Idle)
42 | val updateBookState: StateFlow = _updateBookState.asStateFlow()
43 |
44 | private val _deleteBookState = MutableStateFlow(Response.Idle)
45 | val deleteBookState: StateFlow = _deleteBookState.asStateFlow()
46 |
47 | fun insertBook(book: Book) = viewModelScope.launch {
48 | try {
49 | _insertBookState.value = Response.Loading
50 | _insertBookState.value = Response.Success(repo.insertBook(book))
51 | } catch (e: Exception) {
52 | Response.Failure(e)
53 | }
54 | }
55 |
56 | fun resetInsertBookState() {
57 | _insertBookState.value = Response.Idle
58 | }
59 |
60 | fun updateBook(book: Book) = viewModelScope.launch {
61 | try {
62 | _updateBookState.value = Response.Loading
63 | _updateBookState.value = Response.Success(repo.updateBook(book))
64 | } catch (e: Exception) {
65 | Response.Failure(e)
66 | }
67 | }
68 |
69 | fun resetUpdateBookState() {
70 | _updateBookState.value = Response.Idle
71 | }
72 |
73 | fun deleteBook(book: Book) = viewModelScope.launch {
74 | try {
75 | _deleteBookState.value = Response.Loading
76 | _deleteBookState.value = Response.Success(repo.deleteBook(book))
77 | } catch (e: Exception) {
78 | Response.Failure(e)
79 | }
80 | }
81 |
82 | fun resetDeleteBookState() {
83 | _deleteBookState.value = Response.Idle
84 | }
85 | }
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/InsertBookAlertDialog.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.presentation.book_list.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Spacer
5 | import androidx.compose.foundation.layout.height
6 | import androidx.compose.material.AlertDialog
7 | import androidx.compose.material.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.getValue
10 | import androidx.compose.runtime.mutableStateOf
11 | import androidx.compose.runtime.remember
12 | import androidx.compose.runtime.setValue
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.res.stringResource
15 | import androidx.compose.ui.unit.dp
16 | import ro.alexmamo.roomjetpackcompose.R
17 | import ro.alexmamo.roomjetpackcompose.components.ActionButton
18 | import ro.alexmamo.roomjetpackcompose.core.AUTHOR_FIELD
19 | import ro.alexmamo.roomjetpackcompose.core.TITLE_FIELD
20 | import ro.alexmamo.roomjetpackcompose.domain.model.Book
21 |
22 | const val EMPTY_STRING = ""
23 |
24 | @Composable
25 | fun InsertBookAlertDialog(
26 | onInsertBook: (book: Book) -> Unit,
27 | onEmptyBookField: (String) -> Unit,
28 | onInsertBookDialogCancel: () -> Unit,
29 | ) {
30 | var title by remember { mutableStateOf(EMPTY_STRING) }
31 | var author by remember { mutableStateOf(EMPTY_STRING) }
32 |
33 | AlertDialog(
34 | onDismissRequest = onInsertBookDialogCancel,
35 | title = {
36 | Text(
37 | text = stringResource(
38 | id = R.string.insert_book
39 | )
40 | )
41 | },
42 | text = {
43 | Column {
44 | TitleTextField(
45 | title = title,
46 | onUpdateTitle = { newTitle ->
47 | title = newTitle
48 | }
49 | )
50 | Spacer(
51 | modifier = Modifier.height(16.dp)
52 | )
53 | AuthorTextField(
54 | author = author,
55 | onUpdateAuthor = { newAuthor ->
56 | author = newAuthor
57 | }
58 | )
59 | }
60 | },
61 | confirmButton = {
62 | ActionButton(
63 | onActionButtonClick = {
64 | if (title.isEmpty()) {
65 | onEmptyBookField(TITLE_FIELD)
66 | return@ActionButton
67 | }
68 | if (author.isEmpty()) {
69 | onEmptyBookField(AUTHOR_FIELD)
70 | return@ActionButton
71 | }
72 | onInsertBook(Book(
73 | id = 0,
74 | title = title,
75 | author = author
76 | ))
77 | onInsertBookDialogCancel()
78 | },
79 | resourceId = R.string.insert_button
80 | )
81 | },
82 | dismissButton = {
83 | ActionButton(
84 | onActionButtonClick = onInsertBookDialogCancel,
85 | resourceId = R.string.cancel_button
86 | )
87 | }
88 | )
89 | }
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/EditableBookCard.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.presentation.book_list.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.layout.height
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.layout.width
10 | import androidx.compose.material.Card
11 | import androidx.compose.material.MaterialTheme
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.getValue
14 | import androidx.compose.runtime.mutableStateOf
15 | import androidx.compose.runtime.remember
16 | import androidx.compose.runtime.setValue
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.unit.dp
19 | import ro.alexmamo.roomjetpackcompose.R
20 | import ro.alexmamo.roomjetpackcompose.components.ActionButton
21 | import ro.alexmamo.roomjetpackcompose.core.AUTHOR_FIELD
22 | import ro.alexmamo.roomjetpackcompose.core.TITLE_FIELD
23 | import ro.alexmamo.roomjetpackcompose.domain.model.Book
24 |
25 | @Composable
26 | fun EditableBookCard(
27 | book: Book,
28 | onUpdateBook: (Book) -> Unit,
29 | onEmptyBookField: (String) -> Unit,
30 | onNoBookUpdates: () -> Unit,
31 | onCancel: () -> Unit
32 | ) {
33 | var updatedBook by remember { mutableStateOf(book) }
34 |
35 | Card(
36 | modifier = Modifier.fillMaxWidth().padding(
37 | start = 8.dp,
38 | top = 4.dp,
39 | end = 8.dp,
40 | bottom = 4.dp
41 | ),
42 | shape = MaterialTheme.shapes.small,
43 | elevation = 3.dp
44 | ) {
45 | Column(
46 | modifier = Modifier.padding(8.dp)
47 | ) {
48 | TitleTextField(
49 | title = updatedBook.title,
50 | onUpdateTitle = { newTitle ->
51 | updatedBook = updatedBook.copy(
52 | title = newTitle
53 | )
54 | }
55 | )
56 | Spacer(
57 | modifier = Modifier.height(8.dp)
58 | )
59 | AuthorTextField(
60 | author = updatedBook.author,
61 | onUpdateAuthor = { newAuthor ->
62 | updatedBook = updatedBook.copy(
63 | author = newAuthor
64 | )
65 | }
66 | )
67 | Row {
68 | ActionButton(
69 | onActionButtonClick = onCancel,
70 | resourceId = R.string.cancel_button
71 | )
72 | Spacer(
73 | modifier = Modifier.width(8.dp)
74 | )
75 | ActionButton(
76 | onActionButtonClick = {
77 | updatedBook.apply {
78 | if (title.isEmpty()) {
79 | onEmptyBookField(TITLE_FIELD)
80 | } else if (author.isEmpty()) {
81 | onEmptyBookField(AUTHOR_FIELD)
82 | } else {
83 | if (updatedBook != book) {
84 | onUpdateBook(updatedBook)
85 | } else {
86 | onNoBookUpdates()
87 | }
88 | }
89 | }
90 | },
91 | resourceId = R.string.update_button
92 | )
93 | }
94 | }
95 | }
96 | }
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/navigation/BookNavigationTest.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.navigation
2 |
3 | import android.content.Context
4 | import androidx.activity.compose.setContent
5 | import androidx.annotation.StringRes
6 | import androidx.compose.ui.platform.LocalContext
7 | import androidx.compose.ui.test.assertIsDisplayed
8 | import androidx.compose.ui.test.junit4.createAndroidComposeRule
9 | import androidx.compose.ui.test.onNodeWithContentDescription
10 | import androidx.compose.ui.test.onNodeWithText
11 | import androidx.compose.ui.test.performClick
12 | import androidx.navigation.compose.ComposeNavigator
13 | import androidx.navigation.compose.NavHost
14 | import androidx.navigation.compose.composable
15 | import androidx.navigation.testing.TestNavHostController
16 | import androidx.navigation.toRoute
17 | import androidx.test.core.app.ApplicationProvider
18 | import com.google.common.truth.Truth
19 | import dagger.hilt.android.testing.HiltAndroidRule
20 | import dagger.hilt.android.testing.HiltAndroidTest
21 | import org.junit.Before
22 | import org.junit.Rule
23 | import org.junit.Test
24 | import ro.alexmamo.roomjetpackcompose.R
25 | import ro.alexmamo.roomjetpackcompose.domain.model.toBookDetails
26 | import ro.alexmamo.roomjetpackcompose.presentation.MainActivity
27 | import ro.alexmamo.roomjetpackcompose.presentation.book_list.BookListScreen
28 | import ro.alexmamo.roomjetpackcompose.presentation.book_list.BookListViewModel
29 | import ro.alexmamo.roomjetpackcompose.presentation.book_details.BookDetailsScreen
30 | import ro.alexmamo.roomjetpackcompose.utils.getBookTest
31 | import javax.inject.Inject
32 |
33 | @HiltAndroidTest
34 | class BookNavigationTest {
35 | @get:Rule(order = 0)
36 | var hiltRule = HiltAndroidRule(this)
37 |
38 | @get:Rule(order = 1)
39 | val composeTestRule = createAndroidComposeRule()
40 |
41 | @Inject
42 | lateinit var fakeViewModel: BookListViewModel
43 |
44 | val context = ApplicationProvider.getApplicationContext()
45 | private val bookTest = getBookTest(context)
46 |
47 | lateinit var navController: TestNavHostController
48 |
49 | @Before
50 | fun init() {
51 | hiltRule.inject()
52 | }
53 |
54 | @Before
55 | fun setupNavHost() {
56 | composeTestRule.activity.setContent {
57 | navController = TestNavHostController(LocalContext.current)
58 | navController.navigatorProvider.addNavigator(ComposeNavigator())
59 | fakeViewModel.insertBook(bookTest)
60 |
61 | NavHost(
62 | navController = navController,
63 | startDestination = BookListScreen
64 | ) {
65 | composable {
66 | BookListScreen(
67 | viewModel = fakeViewModel,
68 | navigateToBookDetailsScreen = { book ->
69 | val bookDetails = book.toBookDetails()
70 | navController.navigate(bookDetails)
71 | }
72 | )
73 | }
74 | composable { entry ->
75 | val bookDetails = entry.toRoute()
76 | val book = bookDetails.toBook()
77 | BookDetailsScreen(
78 | book = book,
79 | navigateBack = navController::navigateUp
80 | )
81 | }
82 | }
83 | }
84 | composeTestRule.waitForIdle()
85 | }
86 |
87 | @Test
88 | fun testStartDestinationByRoute() {
89 | val startDestination = navController.graph.startDestinationRoute
90 | val currentDestination = navController.currentBackStackEntry?.destination?.route
91 | Truth.assertThat(currentDestination).isEqualTo(startDestination)
92 | }
93 |
94 | @Test
95 | fun testStartDestinationByText() {
96 | composeTestRule
97 | .onNodeWithText(getString(R.string.book_list_screen_title))
98 | .assertIsDisplayed()
99 | }
100 |
101 | @Test
102 | fun testNavigationFromBookListScreenToBookDetailsScreen() {
103 | composeTestRule.apply {
104 | onNodeWithText(getString(R.string.book_list_screen_title))
105 | .assertIsDisplayed()
106 | onNodeWithText(bookTest.title)
107 | .performClick()
108 | onNodeWithText(getString(R.string.book_details_screen_title))
109 | .assertIsDisplayed()
110 | }
111 | }
112 |
113 | @Test
114 | fun testNavigationFromBookListScreenToBookDetailsScreenAndBack() {
115 | composeTestRule.apply {
116 | onNodeWithText(getString(R.string.book_list_screen_title))
117 | .assertIsDisplayed()
118 | onNodeWithText(bookTest.title)
119 | .performClick()
120 | onNodeWithText(getString(R.string.book_details_screen_title))
121 | .assertIsDisplayed()
122 | onNodeWithContentDescription(getString(R.string.navigate_back))
123 | .performClick()
124 | onNodeWithText(getString(R.string.book_list_screen_title))
125 | .assertIsDisplayed()
126 | }
127 | }
128 |
129 | private fun getString(@StringRes resId: Int) = composeTestRule.activity.getString(resId)
130 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/BookListScreen.kt:
--------------------------------------------------------------------------------
1 | package ro.alexmamo.roomjetpackcompose.presentation.book_list
2 |
3 | import androidx.compose.material.Scaffold
4 | import androidx.compose.material.SnackbarHost
5 | import androidx.compose.material.SnackbarHostState
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.LaunchedEffect
8 | import androidx.compose.runtime.getValue
9 | import androidx.compose.runtime.mutableStateOf
10 | import androidx.compose.runtime.remember
11 | import androidx.compose.runtime.rememberCoroutineScope
12 | import androidx.compose.runtime.setValue
13 | import androidx.compose.ui.platform.LocalContext
14 | import androidx.hilt.navigation.compose.hiltViewModel
15 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
16 | import ro.alexmamo.roomjetpackcompose.R
17 | import ro.alexmamo.roomjetpackcompose.components.LoadingIndicator
18 | import ro.alexmamo.roomjetpackcompose.core.logMessage
19 | import ro.alexmamo.roomjetpackcompose.core.showSnackbarMessage
20 | import ro.alexmamo.roomjetpackcompose.core.showToastMessage
21 | import ro.alexmamo.roomjetpackcompose.domain.model.Book
22 | import ro.alexmamo.roomjetpackcompose.domain.model.Response
23 | import ro.alexmamo.roomjetpackcompose.presentation.book_list.components.BookListContent
24 | import ro.alexmamo.roomjetpackcompose.presentation.book_list.components.BookListTopBar
25 | import ro.alexmamo.roomjetpackcompose.presentation.book_list.components.EmptyBookListContent
26 | import ro.alexmamo.roomjetpackcompose.presentation.book_list.components.InsertBookAlertDialog
27 | import ro.alexmamo.roomjetpackcompose.presentation.book_list.components.InsertBookFloatingActionButton
28 |
29 | @Composable
30 | fun BookListScreen(
31 | viewModel: BookListViewModel = hiltViewModel(),
32 | navigateToBookDetailsScreen: (Book) -> Unit
33 | ) {
34 | val context = LocalContext.current
35 | val resources = context.resources
36 | val coroutineScope = rememberCoroutineScope()
37 | val snackbarHostState = remember { SnackbarHostState() }
38 | var openInsertBookDialog by remember { mutableStateOf(false) }
39 | val bookListResponse by viewModel.bookListState.collectAsStateWithLifecycle()
40 | val insertBookResponse by viewModel.insertBookState.collectAsStateWithLifecycle()
41 | val updateBookResponse by viewModel.updateBookState.collectAsStateWithLifecycle()
42 | val deleteBookResponse by viewModel.deleteBookState.collectAsStateWithLifecycle()
43 |
44 | Scaffold(
45 | topBar = {
46 | BookListTopBar()
47 | },
48 | floatingActionButton = {
49 | InsertBookFloatingActionButton(
50 | onInsertBookFloatingActionButtonClick = {
51 | openInsertBookDialog = true
52 | }
53 | )
54 | },
55 | snackbarHost = {
56 | SnackbarHost(
57 | hostState = snackbarHostState
58 | )
59 | }
60 | ) { innerPadding ->
61 | when(val bookListResponse = bookListResponse) {
62 | is Response.Idle -> {}
63 | is Response.Loading -> LoadingIndicator()
64 | is Response.Success -> bookListResponse.data.let { bookList ->
65 | if (bookList.isEmpty()) {
66 | EmptyBookListContent(
67 | innerPadding = innerPadding
68 | )
69 | } else {
70 | BookListContent(
71 | innerPadding = innerPadding,
72 | bookList = bookList,
73 | onBookCardClick = navigateToBookDetailsScreen,
74 | onUpdateBook = { book ->
75 | viewModel.updateBook(book)
76 | },
77 | onEmptyBookField = { bookField ->
78 | showSnackbarMessage(
79 | coroutineScope = coroutineScope,
80 | snackbarHostState = snackbarHostState,
81 | message = resources.getString(R.string.empty_book_field_message, bookField)
82 | )
83 | },
84 | onDeleteBook = { bookId ->
85 | viewModel.deleteBook(bookId)
86 | },
87 | onNoBookUpdates = {
88 | showSnackbarMessage(
89 | coroutineScope = coroutineScope,
90 | snackbarHostState = snackbarHostState,
91 | message = resources.getString(R.string.no_book_updates_message)
92 | )
93 | }
94 | )
95 | }
96 | }
97 | is Response.Failure -> bookListResponse.e.message?.let { errorMessage ->
98 | LaunchedEffect(errorMessage) {
99 | logMessage(errorMessage)
100 | showToastMessage(context, errorMessage)
101 | }
102 | }
103 | }
104 | }
105 |
106 | if (openInsertBookDialog) {
107 | InsertBookAlertDialog(
108 | onInsertBook = { book ->
109 | viewModel.insertBook(book)
110 | },
111 | onEmptyBookField = { emptyField ->
112 | showSnackbarMessage(
113 | coroutineScope = coroutineScope,
114 | snackbarHostState = snackbarHostState,
115 | message = resources.getString(R.string.empty_book_field_message, emptyField)
116 | )
117 | },
118 | onInsertBookDialogCancel = {
119 | openInsertBookDialog = false
120 | }
121 | )
122 | }
123 |
124 | when(val insertBookResponse = insertBookResponse) {
125 | is Response.Idle -> {}
126 | is Response.Loading -> LoadingIndicator()
127 | is Response.Success -> LaunchedEffect(Unit) {
128 | showSnackbarMessage(
129 | coroutineScope = coroutineScope,
130 | snackbarHostState = snackbarHostState,
131 | message = resources.getString(R.string.book_action_message, BookAction.ADDED)
132 | )
133 | viewModel.resetInsertBookState()
134 | }
135 | is Response.Failure -> insertBookResponse.e.message?.let { errorMessage ->
136 | LaunchedEffect(errorMessage) {
137 | logMessage(errorMessage)
138 | showToastMessage(context, errorMessage)
139 | }
140 | }
141 | }
142 |
143 | when(val updateBookResponse = updateBookResponse) {
144 | is Response.Idle -> {}
145 | is Response.Loading -> LoadingIndicator()
146 | is Response.Success -> LaunchedEffect(Unit) {
147 | showSnackbarMessage(
148 | coroutineScope = coroutineScope,
149 | snackbarHostState = snackbarHostState,
150 | message = resources.getString(R.string.book_action_message, BookAction.UPDATED)
151 | )
152 | viewModel.resetUpdateBookState()
153 | }
154 | is Response.Failure -> updateBookResponse.e.message?.let { errorMessage ->
155 | LaunchedEffect(errorMessage) {
156 | logMessage(errorMessage)
157 | showToastMessage(context, errorMessage)
158 | }
159 | }
160 | }
161 |
162 | when(val deleteBookResponse = deleteBookResponse) {
163 | is Response.Idle -> {}
164 | is Response.Loading -> LoadingIndicator()
165 | is Response.Success -> LaunchedEffect(Unit) {
166 | showSnackbarMessage(
167 | coroutineScope = coroutineScope,
168 | snackbarHostState = snackbarHostState,
169 | message = resources.getString(R.string.book_action_message, BookAction.DELETED)
170 | )
171 | viewModel.resetDeleteBookState()
172 | }
173 | is Response.Failure -> deleteBookResponse.e.message?.let { errorMessage ->
174 | LaunchedEffect(errorMessage) {
175 | logMessage(errorMessage)
176 | showToastMessage(context, errorMessage)
177 | }
178 | }
179 | }
180 | }
181 |
182 | enum class BookAction() {
183 | ADDED,
184 | UPDATED,
185 | DELETED
186 | }
--------------------------------------------------------------------------------
/.idea/androidTestResultsUserPreferences.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
865 |
866 |
867 |
--------------------------------------------------------------------------------