├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── drawable
│ │ │ │ ├── power.png
│ │ │ │ ├── explore.png
│ │ │ │ ├── greetings.png
│ │ │ │ ├── ic_calendar.xml
│ │ │ │ ├── ic_bolt.xml
│ │ │ │ ├── ic_placeholder.xml
│ │ │ │ ├── ic_cake.xml
│ │ │ │ ├── ic_network_error.xml
│ │ │ │ ├── ic_logo.xml
│ │ │ │ ├── ic_launcher_foreground.xml
│ │ │ │ └── ic_search_document.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── values
│ │ │ │ ├── ic_launcher_background.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── strings.xml
│ │ │ │ └── themes.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── drawable-night
│ │ │ │ └── ic_placeholder.xml
│ │ │ └── values-night
│ │ │ │ └── themes.xml
│ │ ├── ic_launcher-playstore.png
│ │ ├── java
│ │ │ └── dev
│ │ │ │ └── amal
│ │ │ │ └── borutoapp
│ │ │ │ ├── Application.kt
│ │ │ │ ├── domain
│ │ │ │ ├── repository
│ │ │ │ │ ├── LocalDataSource.kt
│ │ │ │ │ ├── DataStoreOperations.kt
│ │ │ │ │ └── RemoteDataSource.kt
│ │ │ │ ├── model
│ │ │ │ │ ├── ApiResponse.kt
│ │ │ │ │ ├── HeroRemoteKeys.kt
│ │ │ │ │ ├── Hero.kt
│ │ │ │ │ └── OnBoardingPage.kt
│ │ │ │ └── use_cases
│ │ │ │ │ ├── read_onboarding
│ │ │ │ │ └── ReadOnBoardingUseCase.kt
│ │ │ │ │ ├── save_onboarding
│ │ │ │ │ └── SaveOnBoardingUseCase.kt
│ │ │ │ │ ├── get_selected_hero
│ │ │ │ │ └── GetSelectedHeroUseCase.kt
│ │ │ │ │ ├── get_all_heroes
│ │ │ │ │ └── GetAllHeroesUseCase.kt
│ │ │ │ │ ├── search_heroes
│ │ │ │ │ └── SearchHeroesUseCase.kt
│ │ │ │ │ └── UseCases.kt
│ │ │ │ ├── ui
│ │ │ │ └── theme
│ │ │ │ │ ├── Shape.kt
│ │ │ │ │ ├── Dimensions.kt
│ │ │ │ │ ├── Type.kt
│ │ │ │ │ ├── Theme.kt
│ │ │ │ │ └── Color.kt
│ │ │ │ ├── navigation
│ │ │ │ ├── Screen.kt
│ │ │ │ └── NavGraph.kt
│ │ │ │ ├── presentation
│ │ │ │ ├── screens
│ │ │ │ │ ├── home
│ │ │ │ │ │ ├── HomeViewModel.kt
│ │ │ │ │ │ ├── HomeTopBar.kt
│ │ │ │ │ │ └── HomeScreen.kt
│ │ │ │ │ ├── welcome
│ │ │ │ │ │ ├── WelcomeViewModel.kt
│ │ │ │ │ │ └── WelcomeScreen.kt
│ │ │ │ │ ├── splash
│ │ │ │ │ │ ├── SplashViewModel.kt
│ │ │ │ │ │ └── SplashScreen.kt
│ │ │ │ │ ├── search
│ │ │ │ │ │ ├── SearchViewModel.kt
│ │ │ │ │ │ ├── SearchScreen.kt
│ │ │ │ │ │ └── SearchTopBar.kt
│ │ │ │ │ └── details
│ │ │ │ │ │ ├── DetailsViewModel.kt
│ │ │ │ │ │ ├── DetailsScreen.kt
│ │ │ │ │ │ └── DetailsContent.kt
│ │ │ │ ├── components
│ │ │ │ │ ├── OrderedList.kt
│ │ │ │ │ ├── InfoBox.kt
│ │ │ │ │ ├── ShimmerEffect.kt
│ │ │ │ │ └── RatingWidget.kt
│ │ │ │ └── common
│ │ │ │ │ ├── EmptyScreen.kt
│ │ │ │ │ └── ListContent.kt
│ │ │ │ ├── data
│ │ │ │ ├── remote
│ │ │ │ │ └── BorutoApi.kt
│ │ │ │ ├── repository
│ │ │ │ │ ├── LocalDataSourceImpl.kt
│ │ │ │ │ ├── Repository.kt
│ │ │ │ │ ├── RemoteDataSourceImpl.kt
│ │ │ │ │ └── DataStoreOperationsImpl.kt
│ │ │ │ ├── local
│ │ │ │ │ ├── BorutoDatabase.kt
│ │ │ │ │ ├── DatabaseConverter.kt
│ │ │ │ │ └── dao
│ │ │ │ │ │ ├── HeroRemoteKeysDao.kt
│ │ │ │ │ │ └── HeroDao.kt
│ │ │ │ └── paging_source
│ │ │ │ │ ├── SearchHeroesSource.kt
│ │ │ │ │ └── HeroRemoteMediator.kt
│ │ │ │ ├── util
│ │ │ │ ├── Constants.kt
│ │ │ │ └── PaletteGenerator.kt
│ │ │ │ ├── di
│ │ │ │ ├── DatabaseModule.kt
│ │ │ │ ├── RepositoryModule.kt
│ │ │ │ └── NetworkModule.kt
│ │ │ │ └── MainActivity.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── dev
│ │ │ └── amal
│ │ │ └── borutoapp
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── dev
│ │ └── amal
│ │ └── borutoapp
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── README.md
├── .gitignore
├── settings.gradle
├── local.properties
├── gradle.properties
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustAmalll/BorutoApp/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/drawable/power.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustAmalll/BorutoApp/HEAD/app/src/main/res/drawable/power.png
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustAmalll/BorutoApp/HEAD/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/explore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustAmalll/BorutoApp/HEAD/app/src/main/res/drawable/explore.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/greetings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustAmalll/BorutoApp/HEAD/app/src/main/res/drawable/greetings.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustAmalll/BorutoApp/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustAmalll/BorutoApp/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustAmalll/BorutoApp/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustAmalll/BorutoApp/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustAmalll/BorutoApp/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustAmalll/BorutoApp/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustAmalll/BorutoApp/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustAmalll/BorutoApp/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustAmalll/BorutoApp/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustAmalll/BorutoApp/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Build Modern Android App with REST API and Ktor Server - APP
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #6200EE
4 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/Application.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class Application : Application()
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/domain/repository/LocalDataSource.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.domain.repository
2 |
3 | import dev.amal.borutoapp.domain.model.Hero
4 |
5 | interface LocalDataSource {
6 | suspend fun getSelectedHero(heroId: Int): Hero
7 | }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Dec 15 15:55:30 ALMT 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | .idea/
5 | /.idea/caches
6 | /.idea/libraries
7 | /.idea/modules.xml
8 | /.idea/workspace.xml
9 | /.idea/navEditor.xml
10 | /.idea/assetWizardSettings.xml
11 | .DS_Store
12 | /build
13 | /captures
14 | .externalNativeBuild
15 | .cxx
16 | local.properties
17 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/domain/repository/DataStoreOperations.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.domain.repository
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | interface DataStoreOperations {
6 | suspend fun saveOnBoardingState(completed: Boolean)
7 | fun readOnBoardingState(): Flow
8 | }
--------------------------------------------------------------------------------
/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 | dependencyResolutionManagement {
2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
3 | repositories {
4 | google()
5 | mavenCentral()
6 | jcenter() // Warning: this repository is going to shut down soon
7 | }
8 | }
9 | rootProject.name = "BorutoApp"
10 | include ':app'
11 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/domain/repository/RemoteDataSource.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.domain.repository
2 |
3 | import androidx.paging.PagingData
4 | import dev.amal.borutoapp.domain.model.Hero
5 | import kotlinx.coroutines.flow.Flow
6 |
7 | interface RemoteDataSource {
8 | fun getAllHeroes(): Flow>
9 | fun searchHeroes(query: String): Flow>
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val Shapes = Shapes(
8 | small = RoundedCornerShape(4.dp),
9 | medium = RoundedCornerShape(4.dp),
10 | large = RoundedCornerShape(0.dp)
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/domain/model/ApiResponse.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.domain.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class ApiResponse(
7 | val success: Boolean,
8 | val message: String? = null,
9 | val prevPage: Int? = null,
10 | val nextPage: Int? = null,
11 | val heroes: List = emptyList(),
12 | val lastUpdated: Long? = null
13 | )
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/domain/use_cases/read_onboarding/ReadOnBoardingUseCase.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.domain.use_cases.read_onboarding
2 |
3 | import dev.amal.borutoapp.data.repository.Repository
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | class ReadOnBoardingUseCase(
7 | private val repository: Repository
8 | ) {
9 | operator fun invoke(): Flow =
10 | repository.readOnBoardingState()
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/domain/use_cases/save_onboarding/SaveOnBoardingUseCase.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.domain.use_cases.save_onboarding
2 |
3 | import dev.amal.borutoapp.data.repository.Repository
4 |
5 | class SaveOnBoardingUseCase(
6 | private val repository: Repository
7 | ) {
8 | suspend operator fun invoke(completed: Boolean) {
9 | repository.saveOnBoardingState(completed = completed)
10 | }
11 | }
--------------------------------------------------------------------------------
/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/test/java/dev/amal/borutoapp/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/domain/use_cases/get_selected_hero/GetSelectedHeroUseCase.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.domain.use_cases.get_selected_hero
2 |
3 | import dev.amal.borutoapp.data.repository.Repository
4 | import dev.amal.borutoapp.domain.model.Hero
5 |
6 | class GetSelectedHeroUseCase(
7 | private val repository: Repository
8 | ) {
9 | suspend operator fun invoke(heroId: Int): Hero =
10 | repository.getSelectedHero(heroId = heroId)
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/navigation/Screen.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.navigation
2 |
3 | sealed class Screen(val route: String) {
4 | object Splash : Screen("splash_screen")
5 | object Welcome : Screen("welcome_screen")
6 | object Home : Screen("home_screen")
7 | object Details : Screen("details_screen/{heroId}") {
8 | fun passHeroId(heroId: Int): String = "details_screen/$heroId"
9 | }
10 | object Search : Screen("search_screen")
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/presentation/screens/home/HomeViewModel.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.presentation.screens.home
2 |
3 | import androidx.lifecycle.ViewModel
4 | import dagger.hilt.android.lifecycle.HiltViewModel
5 | import dev.amal.borutoapp.domain.use_cases.UseCases
6 | import javax.inject.Inject
7 |
8 | @HiltViewModel
9 | class HomeViewModel @Inject constructor(
10 | useCases: UseCases
11 | ) : ViewModel() {
12 | val getAllHeroes = useCases.getAllHeroesUseCase()
13 | }
--------------------------------------------------------------------------------
/local.properties:
--------------------------------------------------------------------------------
1 | ## This file is automatically generated by Android Studio.
2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
3 | #
4 | # This file should *NOT* be checked into Version Control Systems,
5 | # as it contains information specific to your local configuration.
6 | #
7 | # Location of the SDK. This is only used by Gradle.
8 | # For customization when using a Version Control System, please read the
9 | # header note.
10 | sdk.dir=C\:\\Users\\lilba\\AppData\\Local\\Android\\Sdk
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_calendar.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/domain/model/HeroRemoteKeys.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.domain.model
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 | import dev.amal.borutoapp.util.Constants.HERO_REMOTE_KEYS_DATABASE_TABLE
6 |
7 | @Entity(tableName = HERO_REMOTE_KEYS_DATABASE_TABLE)
8 | data class HeroRemoteKeys(
9 | @PrimaryKey(autoGenerate = false)
10 | val id: Int,
11 | val prevPage: Int?,
12 | val nextPage: Int?,
13 | val lastUpdated: Long? = null
14 | )
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_bolt.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/domain/use_cases/get_all_heroes/GetAllHeroesUseCase.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.domain.use_cases.get_all_heroes
2 |
3 | import androidx.paging.PagingData
4 | import dev.amal.borutoapp.data.repository.Repository
5 | import dev.amal.borutoapp.domain.model.Hero
6 | import kotlinx.coroutines.flow.Flow
7 |
8 | class GetAllHeroesUseCase(
9 | private val repository: Repository
10 | ) {
11 | operator fun invoke(): Flow> =
12 | repository.getAllHeroes()
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/data/remote/BorutoApi.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.data.remote
2 |
3 | import dev.amal.borutoapp.domain.model.ApiResponse
4 | import retrofit2.http.GET
5 | import retrofit2.http.Query
6 |
7 | interface BorutoApi {
8 | @GET("/boruto/heroes")
9 | suspend fun getAllHeroes(
10 | @Query("page") page: Int = 1
11 | ): ApiResponse
12 |
13 | @GET("/boruto/heroes/search")
14 | suspend fun searchHeroes(
15 | @Query("name") name: String
16 | ): ApiResponse
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/domain/use_cases/search_heroes/SearchHeroesUseCase.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.domain.use_cases.search_heroes
2 |
3 | import androidx.paging.PagingData
4 | import dev.amal.borutoapp.data.repository.Repository
5 | import dev.amal.borutoapp.domain.model.Hero
6 | import kotlinx.coroutines.flow.Flow
7 |
8 | class SearchHeroesUseCase(
9 | private val repository: Repository
10 | ) {
11 | operator fun invoke(query: String): Flow> =
12 | repository.searchHeroes(query = query)
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/data/repository/LocalDataSourceImpl.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.data.repository
2 |
3 | import dev.amal.borutoapp.data.local.BorutoDatabase
4 | import dev.amal.borutoapp.domain.model.Hero
5 | import dev.amal.borutoapp.domain.repository.LocalDataSource
6 |
7 | class LocalDataSourceImpl(borutoDatabase: BorutoDatabase) : LocalDataSource {
8 |
9 | private val heroDao = borutoDatabase.heroDao()
10 |
11 | override suspend fun getSelectedHero(heroId: Int): Hero =
12 | heroDao.getSelectedHero(heroId = heroId)
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/data/local/BorutoDatabase.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.data.local
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import androidx.room.TypeConverters
6 | import dev.amal.borutoapp.data.local.dao.HeroDao
7 | import dev.amal.borutoapp.data.local.dao.HeroRemoteKeysDao
8 | import dev.amal.borutoapp.domain.model.Hero
9 | import dev.amal.borutoapp.domain.model.HeroRemoteKeys
10 |
11 | @Database(entities = [Hero::class, HeroRemoteKeys::class], version = 1)
12 | @TypeConverters(DatabaseConverter::class)
13 | abstract class BorutoDatabase : RoomDatabase() {
14 | abstract fun heroDao(): HeroDao
15 | abstract fun heroRemoteKeysDao(): HeroRemoteKeysDao
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/data/local/DatabaseConverter.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.data.local
2 |
3 | import androidx.room.TypeConverter
4 |
5 | class DatabaseConverter {
6 |
7 | private val separator = ","
8 |
9 | @TypeConverter
10 | fun convertListToString(list: List): String {
11 | val stringBuilder = StringBuilder()
12 | for (item in list) {
13 | stringBuilder.append(item).append(separator)
14 | }
15 | stringBuilder.setLength(stringBuilder.length - separator.length)
16 | return stringBuilder.toString()
17 | }
18 |
19 | @TypeConverter
20 | fun convertStringToList(string: String): List = string.split(separator)
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/domain/model/Hero.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.domain.model
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 | import dev.amal.borutoapp.util.Constants.HERO_DATABASE_TABLE
6 | import kotlinx.serialization.Serializable
7 |
8 | @Serializable
9 | @Entity(tableName = HERO_DATABASE_TABLE)
10 | data class Hero(
11 | @PrimaryKey(autoGenerate = false)
12 | val id: Int,
13 | val name: String,
14 | val image: String,
15 | val about: String,
16 | val rating: Double,
17 | val power: Int,
18 | val month: String,
19 | val day: String,
20 | val family: List,
21 | val abilities: List,
22 | val natureTypes: List
23 | )
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/data/local/dao/HeroRemoteKeysDao.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.data.local.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.OnConflictStrategy
6 | import androidx.room.Query
7 | import dev.amal.borutoapp.domain.model.HeroRemoteKeys
8 |
9 | @Dao
10 | interface HeroRemoteKeysDao {
11 | @Query("SELECT * FROM hero_remote_keys_table WHERE id =:heroId ")
12 | suspend fun getRemoteKeys(heroId: Int): HeroRemoteKeys?
13 |
14 | @Insert(onConflict = OnConflictStrategy.REPLACE)
15 | suspend fun addAllRemoteKeys(heroRemoteKeys: List)
16 |
17 | @Query("DELETE FROM hero_remote_keys_table")
18 | suspend fun deleteAllRemoteKeys()
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/data/local/dao/HeroDao.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.data.local.dao
2 |
3 | import androidx.paging.PagingSource
4 | import androidx.room.Dao
5 | import androidx.room.Insert
6 | import androidx.room.OnConflictStrategy
7 | import androidx.room.Query
8 | import dev.amal.borutoapp.domain.model.Hero
9 |
10 | @Dao
11 | interface HeroDao {
12 | @Query("SELECT * FROM hero_table ORDER BY id ASC")
13 | fun getAllHeroes(): PagingSource
14 |
15 | @Query("SELECT * FROM hero_table WHERE id=:heroId")
16 | fun getSelectedHero(heroId: Int): Hero
17 |
18 | @Insert(onConflict = OnConflictStrategy.REPLACE)
19 | suspend fun addHeroes(heroes: List)
20 |
21 | @Query("DELETE FROM hero_table")
22 | suspend fun deleteAllHeroes()
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/presentation/screens/welcome/WelcomeViewModel.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.presentation.screens.welcome
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import dagger.hilt.android.lifecycle.HiltViewModel
6 | import dev.amal.borutoapp.domain.use_cases.UseCases
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.launch
9 | import javax.inject.Inject
10 |
11 | @HiltViewModel
12 | class WelcomeViewModel @Inject constructor(
13 | private val useCases: UseCases
14 | ) : ViewModel() {
15 |
16 | fun saveOnBoardingState(completed: Boolean) {
17 | viewModelScope.launch(Dispatchers.IO) {
18 | useCases.saveOnBoardingUserCase(completed = completed)
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_placeholder.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
14 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/ic_placeholder.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
14 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/ui/theme/Dimensions.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.ui.theme
2 |
3 | import androidx.compose.ui.unit.dp
4 |
5 | val EXTRA_LARGE_PADDING = 40.dp
6 | val LARGE_PADDING = 20.dp
7 | val MEDIUM_PADDING = 16.dp
8 | val DEFAULT_PADDING = 12.dp
9 | val SMALL_PADDING = 10.dp
10 | val EXTRA_SMALL_PADDING = 6.dp
11 | val SUPER_EXTRA_SMALL_PADDING = 3.dp
12 |
13 | val PAGING_INDICATOR_WIDTH = 12.dp
14 | val PAGING_INDICATOR_SPACING = 8.dp
15 |
16 | val TOP_APP_BAR_HEIGHT = 56.dp
17 | val HERO_ITEM_HEIGHT = 500.dp
18 | val NAME_PLACEHOLDER_HEIGHT = 30.dp
19 | val ABOUT_PLACEHOLDER_HEIGHT = 15.dp
20 | val RATING_PLACEHOLDER_HEIGHT = 20.dp
21 | val NETWORK_ERROR_ICON_HEIGHT = 120.dp
22 |
23 | val DEFAULT_ICON_SIZE = 32.dp
24 |
25 | val MIN_SHEET_HEIGHT = 140.dp
26 | val EXPANDED_RADIUS_LEVEL = 0.dp
--------------------------------------------------------------------------------
/app/src/androidTest/java/dev/amal/borutoapp/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("dev.amal.borutoapp", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/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/java/dev/amal/borutoapp/domain/use_cases/UseCases.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.domain.use_cases
2 |
3 | import dev.amal.borutoapp.domain.use_cases.get_all_heroes.GetAllHeroesUseCase
4 | import dev.amal.borutoapp.domain.use_cases.get_selected_hero.GetSelectedHeroUseCase
5 | import dev.amal.borutoapp.domain.use_cases.read_onboarding.ReadOnBoardingUseCase
6 | import dev.amal.borutoapp.domain.use_cases.save_onboarding.SaveOnBoardingUseCase
7 | import dev.amal.borutoapp.domain.use_cases.search_heroes.SearchHeroesUseCase
8 |
9 | data class UseCases(
10 | val saveOnBoardingUserCase: SaveOnBoardingUseCase,
11 | val readOnBoardingUseCase: ReadOnBoardingUseCase,
12 | val getAllHeroesUseCase: GetAllHeroesUseCase,
13 | val searchHeroesUseCase: SearchHeroesUseCase,
14 | val getSelectedHeroUseCase: GetSelectedHeroUseCase
15 | )
16 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/util/Constants.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.util
2 |
3 | object Constants {
4 |
5 | // const val BASE_URL = "http://192.168.1.6:8080"
6 | const val BASE_URL = "https://ktor-boruto-server.herokuapp.com"
7 |
8 | const val DETAILS_ARGUMENT_KEY = "heroId"
9 |
10 | const val BORUTO_DATABASE = "boruto_database"
11 | const val HERO_DATABASE_TABLE = "hero_table"
12 | const val HERO_REMOTE_KEYS_DATABASE_TABLE = "hero_remote_keys_table"
13 |
14 | const val PREFERENCES_NAME = "boruto_preferences"
15 | const val PREFERENCES_KEY = "on_boarding_completed"
16 |
17 | const val ON_BOARDING_PAGE_COUNT = 3
18 | const val LAST_ON_BOARDING_PAGE = 2
19 |
20 | const val ITEMS_PER_PAGE = 3
21 | const val ABOUT_TEXT_MAX_LINES = 7
22 |
23 | const val MIN_BACKGROUND_IMAGE_HEIGHT = 0.5f
24 | }
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.ui.theme
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | body1 = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp
15 | )
16 | /* Other default text styles to override
17 | button = TextStyle(
18 | fontFamily = FontFamily.Default,
19 | fontWeight = FontWeight.W500,
20 | fontSize = 14.sp
21 | ),
22 | caption = TextStyle(
23 | fontFamily = FontFamily.Default,
24 | fontWeight = FontWeight.Normal,
25 | fontSize = 12.sp
26 | )
27 | */
28 | )
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_cake.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/domain/model/OnBoardingPage.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.domain.model
2 |
3 | import androidx.annotation.DrawableRes
4 | import dev.amal.borutoapp.R
5 |
6 | sealed class OnBoardingPage(
7 | @DrawableRes val image: Int,
8 | val title: String,
9 | val description: String
10 | ) {
11 | object First : OnBoardingPage(
12 | image = R.drawable.greetings,
13 | title = "Greetings",
14 | description = "Are you a Boruto fan? Because if you are then we have a great news for you!"
15 | )
16 |
17 | object Second : OnBoardingPage(
18 | image = R.drawable.explore,
19 | title = "Explore",
20 | description = "Find your favourite heroes and learn some of the things that you didn't know about."
21 | )
22 |
23 | object Third : OnBoardingPage(
24 | image = R.drawable.power,
25 | title = "Power",
26 | description = "Check out your hero's power and see how mush are they strong comparing to other."
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | BorutoApp
3 | Application Logo
4 | On Boarding Image
5 | Search Icon
6 | M14,21.288,21.416,26l-1.968-8.88L26,11.145l-8.628-.771L14,2l-3.372,8.375L2,11.145,8.552,17.12,6.584,26Z
7 | Hero Image
8 | Network Error Icon
9 | Search here...
10 | Close Icon
11 | Info Icon
12 | Power
13 | Month
14 | Birthday
15 | About
16 | Family
17 | Abilities
18 | Nature Types
19 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/presentation/screens/splash/SplashViewModel.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.presentation.screens.splash
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import dagger.hilt.android.lifecycle.HiltViewModel
6 | import dev.amal.borutoapp.domain.use_cases.UseCases
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.flow.MutableStateFlow
9 | import kotlinx.coroutines.flow.StateFlow
10 | import kotlinx.coroutines.flow.stateIn
11 | import kotlinx.coroutines.launch
12 | import javax.inject.Inject
13 |
14 | @HiltViewModel
15 | class SplashViewModel @Inject constructor(
16 | private val useCases: UseCases
17 | ) : ViewModel() {
18 |
19 | private val _onBoardingCompleted = MutableStateFlow(false)
20 | val onBoardingCompleted: StateFlow = _onBoardingCompleted
21 |
22 | init {
23 | viewModelScope.launch(Dispatchers.IO) {
24 | _onBoardingCompleted.value =
25 | useCases.readOnBoardingUseCase().stateIn(viewModelScope).value
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/presentation/screens/home/HomeTopBar.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.presentation.screens.home
2 |
3 | import androidx.compose.material.*
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.filled.Search
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.res.stringResource
8 | import dev.amal.borutoapp.R
9 | import dev.amal.borutoapp.ui.theme.topAppBarBgColor
10 | import dev.amal.borutoapp.ui.theme.topAppBarContentColor
11 |
12 | @Composable
13 | fun HomeTopBar(onSearchClicked: () -> Unit) {
14 | TopAppBar(
15 | title = {
16 | Text(
17 | text = "Explore",
18 | color = MaterialTheme.colors.topAppBarContentColor
19 | )
20 | },
21 | backgroundColor = MaterialTheme.colors.topAppBarBgColor,
22 | actions = {
23 | IconButton(onClick = onSearchClicked) {
24 | Icon(
25 | imageVector = Icons.Default.Search,
26 | contentDescription = stringResource(R.string.search_icon)
27 | )
28 | }
29 | }
30 | )
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/data/repository/Repository.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.data.repository
2 |
3 | import androidx.paging.PagingData
4 | import dev.amal.borutoapp.domain.model.Hero
5 | import dev.amal.borutoapp.domain.repository.DataStoreOperations
6 | import dev.amal.borutoapp.domain.repository.LocalDataSource
7 | import dev.amal.borutoapp.domain.repository.RemoteDataSource
8 | import kotlinx.coroutines.flow.Flow
9 | import javax.inject.Inject
10 |
11 | class Repository @Inject constructor(
12 | private val local: LocalDataSource,
13 | private val remote: RemoteDataSource,
14 | private val dataStore: DataStoreOperations
15 | ) {
16 |
17 | fun getAllHeroes(): Flow> =
18 | remote.getAllHeroes()
19 |
20 | fun searchHeroes(query: String): Flow> =
21 | remote.searchHeroes(query = query)
22 |
23 | suspend fun getSelectedHero(heroId: Int): Hero =
24 | local.getSelectedHero(heroId = heroId)
25 |
26 | suspend fun saveOnBoardingState(completed: Boolean) {
27 | dataStore.saveOnBoardingState(completed = completed)
28 | }
29 |
30 | fun readOnBoardingState(): Flow =
31 | dataStore.readOnBoardingState()
32 |
33 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
18 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/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 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/di/DatabaseModule.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.di
2 |
3 | import android.content.Context
4 | import androidx.room.Room
5 | import androidx.room.RoomDatabase
6 | import dagger.Module
7 | import dagger.Provides
8 | import dagger.hilt.InstallIn
9 | import dagger.hilt.android.qualifiers.ApplicationContext
10 | import dagger.hilt.components.SingletonComponent
11 | import dev.amal.borutoapp.data.local.BorutoDatabase
12 | import dev.amal.borutoapp.data.repository.LocalDataSourceImpl
13 | import dev.amal.borutoapp.domain.repository.LocalDataSource
14 | import dev.amal.borutoapp.util.Constants.BORUTO_DATABASE
15 | import javax.inject.Singleton
16 |
17 | @Module
18 | @InstallIn(SingletonComponent::class)
19 | object DatabaseModule {
20 |
21 | @Provides
22 | @Singleton
23 | fun provideDatabase(
24 | @ApplicationContext context: Context
25 | ): BorutoDatabase =
26 | Room.databaseBuilder(
27 | context,
28 | BorutoDatabase::class.java,
29 | BORUTO_DATABASE
30 | ).build()
31 |
32 | @Provides
33 | @Singleton
34 | fun provideLocalDataSource(
35 | database: BorutoDatabase
36 | ): LocalDataSource =
37 | LocalDataSourceImpl(borutoDatabase = database)
38 |
39 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.compose.animation.ExperimentalAnimationApi
7 | import androidx.compose.material.ExperimentalMaterialApi
8 | import androidx.navigation.NavHostController
9 | import androidx.navigation.compose.rememberNavController
10 | import coil.annotation.ExperimentalCoilApi
11 | import com.google.accompanist.pager.ExperimentalPagerApi
12 | import dagger.hilt.android.AndroidEntryPoint
13 | import dev.amal.borutoapp.navigation.SetupNavGraph
14 | import dev.amal.borutoapp.ui.theme.BorutoAppTheme
15 |
16 | @ExperimentalMaterialApi
17 | @ExperimentalCoilApi
18 | @ExperimentalAnimationApi
19 | @ExperimentalPagerApi
20 | @AndroidEntryPoint
21 | class MainActivity : ComponentActivity() {
22 |
23 | private lateinit var navController: NavHostController
24 |
25 | override fun onCreate(savedInstanceState: Bundle?) {
26 | super.onCreate(savedInstanceState)
27 | setContent {
28 | BorutoAppTheme {
29 | navController = rememberNavController()
30 | SetupNavGraph(navController = navController)
31 | }
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.ui.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.material.darkColors
6 | import androidx.compose.material.lightColors
7 | import androidx.compose.runtime.Composable
8 |
9 | private val DarkColorPalette = darkColors(
10 | primary = Purple200,
11 | primaryVariant = Purple700,
12 | secondary = Teal200
13 | )
14 |
15 | private val LightColorPalette = lightColors(
16 | primary = Purple500,
17 | primaryVariant = Purple700,
18 | secondary = Teal200
19 |
20 | /* Other default colors to override
21 | background = Color.White,
22 | surface = Color.White,
23 | onPrimary = Color.White,
24 | onSecondary = Color.Black,
25 | onBackground = Color.Black,
26 | onSurface = Color.Black,
27 | */
28 | )
29 |
30 | @Composable
31 | fun BorutoAppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
32 | val colors = if (darkTheme) {
33 | DarkColorPalette
34 | } else {
35 | LightColorPalette
36 | }
37 |
38 | MaterialTheme(
39 | colors = colors,
40 | typography = Typography,
41 | shapes = Shapes,
42 | content = content
43 | )
44 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_network_error.xml:
--------------------------------------------------------------------------------
1 |
6 |
11 |
16 |
19 |
22 |
25 |
28 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/presentation/screens/home/HomeScreen.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.presentation.screens.home
2 |
3 | import androidx.compose.material.MaterialTheme
4 | import androidx.compose.material.Scaffold
5 | import androidx.compose.runtime.Composable
6 | import androidx.hilt.navigation.compose.hiltViewModel
7 | import androidx.navigation.NavHostController
8 | import androidx.paging.compose.collectAsLazyPagingItems
9 | import coil.annotation.ExperimentalCoilApi
10 | import com.google.accompanist.systemuicontroller.rememberSystemUiController
11 | import dev.amal.borutoapp.navigation.Screen
12 | import dev.amal.borutoapp.presentation.common.ListContent
13 | import dev.amal.borutoapp.ui.theme.statusBarColor
14 |
15 | @ExperimentalCoilApi
16 | @Composable
17 | fun HomeScreen(
18 | navController: NavHostController,
19 | homeViewModel: HomeViewModel = hiltViewModel()
20 | ) {
21 | val allHeroes = homeViewModel.getAllHeroes.collectAsLazyPagingItems()
22 |
23 | val systemUiController = rememberSystemUiController()
24 | systemUiController.setStatusBarColor(
25 | color = MaterialTheme.colors.statusBarColor
26 | )
27 |
28 | Scaffold(
29 | topBar = {
30 | HomeTopBar(onSearchClicked = { navController.navigate(Screen.Search.route) })
31 | },
32 | content = { ListContent(heroes = allHeroes, navController = navController) }
33 | )
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/data/paging_source/SearchHeroesSource.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.data.paging_source
2 |
3 | import androidx.paging.PagingSource
4 | import androidx.paging.PagingState
5 | import dev.amal.borutoapp.data.remote.BorutoApi
6 | import dev.amal.borutoapp.domain.model.Hero
7 | import javax.inject.Inject
8 |
9 | class SearchHeroesSource @Inject constructor(
10 | private val borutoApi: BorutoApi,
11 | private val query: String
12 | ) : PagingSource() {
13 |
14 | override suspend fun load(params: LoadParams): LoadResult {
15 | return try {
16 | val apiResponse = borutoApi.searchHeroes(name = query)
17 | val heroes = apiResponse.heroes
18 | if (heroes.isNotEmpty()) {
19 | LoadResult.Page(
20 | data = heroes,
21 | prevKey = apiResponse.prevPage,
22 | nextKey = apiResponse.nextPage
23 | )
24 | } else {
25 | LoadResult.Page(
26 | data = emptyList(),
27 | prevKey = null,
28 | nextKey = null
29 | )
30 | }
31 | } catch (e: Exception) {
32 | LoadResult.Error(e)
33 | }
34 | }
35 |
36 | override fun getRefreshKey(state: PagingState): Int? =
37 | state.anchorPosition
38 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/presentation/components/OrderedList.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.presentation.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material.ContentAlpha
6 | import androidx.compose.material.MaterialTheme
7 | import androidx.compose.material.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.draw.alpha
11 | import androidx.compose.ui.graphics.Color
12 | import androidx.compose.ui.text.font.FontWeight
13 | import dev.amal.borutoapp.ui.theme.SMALL_PADDING
14 |
15 | @Composable
16 | fun OrderedList(
17 | title: String,
18 | items: List,
19 | textColor: Color
20 | ) {
21 | Column {
22 | Text(
23 | modifier = Modifier.padding(bottom = SMALL_PADDING),
24 | text = title,
25 | color = textColor,
26 | fontSize = MaterialTheme.typography.subtitle1.fontSize,
27 | fontWeight = FontWeight.Bold
28 | )
29 | items.forEachIndexed { index, item ->
30 | Text(
31 | modifier = Modifier.alpha(ContentAlpha.medium),
32 | text = "${index + 1}. $item",
33 | color = textColor,
34 | fontSize = MaterialTheme.typography.body1.fontSize
35 | )
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/presentation/screens/search/SearchViewModel.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.presentation.screens.search
2 |
3 | import androidx.compose.runtime.mutableStateOf
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import androidx.paging.PagingData
7 | import androidx.paging.cachedIn
8 | import dagger.hilt.android.lifecycle.HiltViewModel
9 | import dev.amal.borutoapp.domain.model.Hero
10 | import dev.amal.borutoapp.domain.use_cases.UseCases
11 | import kotlinx.coroutines.Dispatchers
12 | import kotlinx.coroutines.flow.MutableStateFlow
13 | import kotlinx.coroutines.flow.collect
14 | import kotlinx.coroutines.launch
15 | import javax.inject.Inject
16 |
17 | @HiltViewModel
18 | class SearchViewModel @Inject constructor(
19 | private val userCases: UseCases
20 | ) : ViewModel() {
21 |
22 | private val _searchQuery = mutableStateOf("")
23 | val searchQuery = _searchQuery
24 |
25 | private val _searchedHeroes = MutableStateFlow>(PagingData.empty())
26 | val searchedHeroes = _searchedHeroes
27 |
28 | fun updateSearchQuery(query: String) {
29 | _searchQuery.value = query
30 | }
31 |
32 | fun searchHeroes(query: String) {
33 | viewModelScope.launch(Dispatchers.IO) {
34 | userCases.searchHeroesUseCase(query = query).cachedIn(viewModelScope).collect {
35 | _searchedHeroes.value = it
36 | }
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/data/repository/RemoteDataSourceImpl.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.data.repository
2 |
3 | import androidx.paging.ExperimentalPagingApi
4 | import androidx.paging.Pager
5 | import androidx.paging.PagingConfig
6 | import androidx.paging.PagingData
7 | import dev.amal.borutoapp.data.local.BorutoDatabase
8 | import dev.amal.borutoapp.data.paging_source.HeroRemoteMediator
9 | import dev.amal.borutoapp.data.paging_source.SearchHeroesSource
10 | import dev.amal.borutoapp.data.remote.BorutoApi
11 | import dev.amal.borutoapp.domain.model.Hero
12 | import dev.amal.borutoapp.domain.repository.RemoteDataSource
13 | import dev.amal.borutoapp.util.Constants.ITEMS_PER_PAGE
14 | import kotlinx.coroutines.flow.Flow
15 |
16 | @ExperimentalPagingApi
17 | class RemoteDataSourceImpl(
18 | private val borutoApi: BorutoApi,
19 | private val borutoDatabase: BorutoDatabase
20 | ) : RemoteDataSource {
21 |
22 | private val heroDao = borutoDatabase.heroDao()
23 |
24 | override fun getAllHeroes(): Flow> {
25 | val pagingSourceFactory = { heroDao.getAllHeroes() }
26 | return Pager(
27 | config = PagingConfig(pageSize = ITEMS_PER_PAGE),
28 | remoteMediator = HeroRemoteMediator(
29 | borutoApi = borutoApi,
30 | borutoDatabase = borutoDatabase
31 | ),
32 | pagingSourceFactory = pagingSourceFactory
33 | ).flow
34 | }
35 |
36 | override fun searchHeroes(query: String): Flow> =
37 | Pager(
38 | config = PagingConfig(pageSize = ITEMS_PER_PAGE),
39 | pagingSourceFactory = {
40 | SearchHeroesSource(borutoApi = borutoApi, query = query)
41 | }
42 | ).flow
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/di/RepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.di
2 |
3 | import android.content.Context
4 | import dagger.Module
5 | import dagger.Provides
6 | import dagger.hilt.InstallIn
7 | import dagger.hilt.android.qualifiers.ApplicationContext
8 | import dagger.hilt.components.SingletonComponent
9 | import dev.amal.borutoapp.data.repository.DataStoreOperationsImpl
10 | import dev.amal.borutoapp.data.repository.Repository
11 | import dev.amal.borutoapp.domain.repository.DataStoreOperations
12 | import dev.amal.borutoapp.domain.use_cases.UseCases
13 | import dev.amal.borutoapp.domain.use_cases.get_all_heroes.GetAllHeroesUseCase
14 | import dev.amal.borutoapp.domain.use_cases.get_selected_hero.GetSelectedHeroUseCase
15 | import dev.amal.borutoapp.domain.use_cases.read_onboarding.ReadOnBoardingUseCase
16 | import dev.amal.borutoapp.domain.use_cases.save_onboarding.SaveOnBoardingUseCase
17 | import dev.amal.borutoapp.domain.use_cases.search_heroes.SearchHeroesUseCase
18 | import javax.inject.Singleton
19 |
20 | @Module
21 | @InstallIn(SingletonComponent::class)
22 | object RepositoryModule {
23 |
24 | @Provides
25 | @Singleton
26 | fun provideDataStoreOperations(
27 | @ApplicationContext context: Context
28 | ): DataStoreOperations = DataStoreOperationsImpl(context = context)
29 |
30 | @Provides
31 | @Singleton
32 | fun provideUserCases(repository: Repository): UseCases =
33 | UseCases(
34 | saveOnBoardingUserCase = SaveOnBoardingUseCase(repository),
35 | readOnBoardingUseCase = ReadOnBoardingUseCase(repository),
36 | getAllHeroesUseCase = GetAllHeroesUseCase(repository),
37 | searchHeroesUseCase = SearchHeroesUseCase(repository),
38 | getSelectedHeroUseCase = GetSelectedHeroUseCase(repository)
39 | )
40 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/presentation/screens/search/SearchScreen.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.presentation.screens.search
2 |
3 | import androidx.compose.material.MaterialTheme
4 | import androidx.compose.material.Scaffold
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.getValue
7 | import androidx.hilt.navigation.compose.hiltViewModel
8 | import androidx.navigation.NavHostController
9 | import androidx.paging.compose.collectAsLazyPagingItems
10 | import coil.annotation.ExperimentalCoilApi
11 | import com.google.accompanist.systemuicontroller.rememberSystemUiController
12 | import dev.amal.borutoapp.presentation.common.ListContent
13 | import dev.amal.borutoapp.ui.theme.statusBarColor
14 |
15 | @ExperimentalCoilApi
16 | @Composable
17 | fun SearchScreen(
18 | navController: NavHostController,
19 | searchViewModel: SearchViewModel = hiltViewModel()
20 | ) {
21 | val searchQuery by searchViewModel.searchQuery
22 | val heroes = searchViewModel.searchedHeroes.collectAsLazyPagingItems()
23 |
24 | val systemUiController = rememberSystemUiController()
25 | systemUiController.setStatusBarColor(
26 | color = MaterialTheme.colors.statusBarColor
27 | )
28 |
29 | Scaffold(
30 | topBar = {
31 | SearchTopBar(
32 | text = searchQuery,
33 | onTextChange = {
34 | searchViewModel.updateSearchQuery(query = it)
35 | },
36 | onSearchClicked = {
37 | searchViewModel.searchHeroes(query = it)
38 | },
39 | onCloseClicked = {
40 | navController.popBackStack()
41 | }
42 | )
43 | },
44 | content = {
45 | ListContent(heroes = heroes, navController = navController)
46 | }
47 | )
48 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_logo.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/data/repository/DataStoreOperationsImpl.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.data.repository
2 |
3 | import android.content.Context
4 | import androidx.datastore.core.DataStore
5 | import androidx.datastore.preferences.core.Preferences
6 | import androidx.datastore.preferences.core.booleanPreferencesKey
7 | import androidx.datastore.preferences.core.edit
8 | import androidx.datastore.preferences.core.emptyPreferences
9 | import androidx.datastore.preferences.preferencesDataStore
10 | import dev.amal.borutoapp.domain.repository.DataStoreOperations
11 | import dev.amal.borutoapp.util.Constants.PREFERENCES_KEY
12 | import dev.amal.borutoapp.util.Constants.PREFERENCES_NAME
13 | import kotlinx.coroutines.flow.Flow
14 | import kotlinx.coroutines.flow.catch
15 | import kotlinx.coroutines.flow.map
16 | import java.io.IOException
17 |
18 | val Context.datastore: DataStore by preferencesDataStore(name = PREFERENCES_NAME)
19 |
20 | class DataStoreOperationsImpl(context: Context) : DataStoreOperations {
21 |
22 | private object PreferencesKey {
23 | val onBoardingKey = booleanPreferencesKey(name = PREFERENCES_KEY)
24 | }
25 |
26 | private val dataStore = context.datastore
27 |
28 | override suspend fun saveOnBoardingState(completed: Boolean) {
29 | dataStore.edit { preferences ->
30 | preferences[PreferencesKey.onBoardingKey] = completed
31 | }
32 | }
33 |
34 | override fun readOnBoardingState(): Flow =
35 | dataStore.data
36 | .catch { exception ->
37 | if (exception is IOException) emit(emptyPreferences())
38 | else throw exception
39 | }
40 | .map { preferences ->
41 | val onBoardingState = preferences[PreferencesKey.onBoardingKey] ?: false
42 | onBoardingState
43 | }
44 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/amal/borutoapp/presentation/screens/details/DetailsViewModel.kt:
--------------------------------------------------------------------------------
1 | package dev.amal.borutoapp.presentation.screens.details
2 |
3 | import androidx.compose.runtime.MutableState
4 | import androidx.compose.runtime.State
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.lifecycle.SavedStateHandle
7 | import androidx.lifecycle.ViewModel
8 | import androidx.lifecycle.viewModelScope
9 | import dagger.hilt.android.lifecycle.HiltViewModel
10 | import dev.amal.borutoapp.domain.model.Hero
11 | import dev.amal.borutoapp.domain.use_cases.UseCases
12 | import dev.amal.borutoapp.util.Constants.DETAILS_ARGUMENT_KEY
13 | import kotlinx.coroutines.Dispatchers
14 | import kotlinx.coroutines.flow.*
15 | import kotlinx.coroutines.launch
16 | import javax.inject.Inject
17 |
18 | @HiltViewModel
19 | class DetailsViewModel @Inject constructor(
20 | private val useCases: UseCases,
21 | savedStateHandle: SavedStateHandle
22 | ) : ViewModel() {
23 |
24 | private val _selectedHero: MutableStateFlow = MutableStateFlow(null)
25 | val selectedHero: StateFlow = _selectedHero
26 |
27 | init {
28 | viewModelScope.launch(Dispatchers.IO) {
29 | val heroId = savedStateHandle.get(DETAILS_ARGUMENT_KEY)
30 | _selectedHero.value = heroId?.let {
31 | useCases.getSelectedHeroUseCase(heroId = heroId)
32 | }
33 | }
34 | }
35 |
36 | private val _uiEvent = MutableSharedFlow()
37 | val uiEvent: SharedFlow = _uiEvent.asSharedFlow()
38 |
39 | private val _colorPalette: MutableState