├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── themes.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
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── layout
│ │ │ │ ├── image_row.xml
│ │ │ │ ├── activity_main.xml
│ │ │ │ ├── fragment_image_api.xml
│ │ │ │ ├── fragment_arts.xml
│ │ │ │ ├── art_row.xml
│ │ │ │ └── fragment_art_details.xml
│ │ │ ├── values-night
│ │ │ │ └── themes.xml
│ │ │ ├── navigation
│ │ │ │ └── nav_graph.xml
│ │ │ ├── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ └── drawable
│ │ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── atilsamancioglu
│ │ │ │ └── artbookhilttesting
│ │ │ │ ├── util
│ │ │ │ ├── Util.kt
│ │ │ │ └── Resource.kt
│ │ │ │ ├── model
│ │ │ │ ├── ImageResponse.kt
│ │ │ │ └── ImageResult.kt
│ │ │ │ ├── ArtBookApplication.kt
│ │ │ │ ├── roomdb
│ │ │ │ ├── ArtDatabase.kt
│ │ │ │ ├── Art.kt
│ │ │ │ └── ArtDao.kt
│ │ │ │ ├── api
│ │ │ │ └── RetrofitAPI.kt
│ │ │ │ ├── repo
│ │ │ │ ├── ArtRepositoryInterface.kt
│ │ │ │ └── ArtRepository.kt
│ │ │ │ ├── view
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── ArtFragmentFactory.kt
│ │ │ │ ├── ArtFragment.kt
│ │ │ │ ├── ArtDetailsFragment.kt
│ │ │ │ └── ImageApiFragment.kt
│ │ │ │ ├── adapter
│ │ │ │ ├── ImageRecyclerAdapter.kt
│ │ │ │ └── ArtRecyclerAdapter.kt
│ │ │ │ ├── dependencyinjection
│ │ │ │ └── AppModule.kt
│ │ │ │ └── viewmodel
│ │ │ │ └── ArtViewModel.kt
│ │ └── AndroidManifest.xml
│ ├── debug
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── atilsamancioglu
│ │ │ │ └── artbookhilttesting
│ │ │ │ └── HiltTestActivity.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── atilsamancioglu
│ │ │ └── artbookhilttesting
│ │ │ ├── ExampleUnitTest.kt
│ │ │ ├── MainCoroutineRule.kt
│ │ │ ├── LiveDataUtil.kt
│ │ │ ├── repo
│ │ │ └── FakeArtRepository.kt
│ │ │ └── viewmodel
│ │ │ └── ArtViewModelTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── atilsamancioglu
│ │ └── artbookhilttesting
│ │ ├── HiltTestRunner.kt
│ │ ├── ExampleInstrumentedTest.kt
│ │ ├── dependencyinjection
│ │ └── TestAppModule.kt
│ │ ├── LiveDataTestingUtil.kt
│ │ ├── repo
│ │ └── FakeArtRepositoryAndroid.kt
│ │ ├── view
│ │ ├── ArtFragmentTest.kt
│ │ ├── ImageApiFragmentTest.kt
│ │ └── ArtDetailsFragmentTest.kt
│ │ ├── HiltExtension.kt
│ │ └── roomdb
│ │ └── ArtDaoTest.kt
├── proguard-rules.pro
└── build.gradle
├── .idea
├── .name
├── .gitignore
├── vcs.xml
├── compiler.xml
├── dictionaries
├── kotlinc.xml
├── render.experimental.xml
├── misc.xml
├── gradle.xml
├── jarRepositories.xml
├── $CACHE_FILE$
└── androidTestResultsUserPreferences.xml
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── settings.gradle
├── gradle.properties
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | ArtBookHiltTesting
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | ArtBookHiltTesting
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atilsamancioglu/IA27-ArtBookHiltTesting/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atilsamancioglu/IA27-ArtBookHiltTesting/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atilsamancioglu/IA27-ArtBookHiltTesting/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atilsamancioglu/IA27-ArtBookHiltTesting/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atilsamancioglu/IA27-ArtBookHiltTesting/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atilsamancioglu/IA27-ArtBookHiltTesting/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atilsamancioglu/IA27-ArtBookHiltTesting/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/atilsamancioglu/IA27-ArtBookHiltTesting/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/atilsamancioglu/IA27-ArtBookHiltTesting/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/atilsamancioglu/IA27-ArtBookHiltTesting/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/atilsamancioglu/IA27-ArtBookHiltTesting/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/dictionaries:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/render.experimental.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/atilsamancioglu/artbookhilttesting/util/Util.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.util
2 |
3 | object Util {
4 |
5 | const val API_KEY = "Replace with your own API KEY"
6 | const val BASE_URL = "https://pixabay.com"
7 |
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atilsamancioglu/artbookhilttesting/model/ImageResponse.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.model
2 |
3 | data class ImageResponse(
4 | val hits: List,
5 | val total: Int,
6 | val totalHits: Int
7 | )
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/atilsamancioglu/artbookhilttesting/ArtBookApplication.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class ArtBookApplication : Application()
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Jan 07 20:13:15 EET 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip
7 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/app/src/debug/java/com/atilsamancioglu/artbookhilttesting/HiltTestActivity.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting
2 |
3 | import androidx.appcompat.app.AppCompatActivity
4 | import dagger.hilt.android.AndroidEntryPoint
5 |
6 | @AndroidEntryPoint
7 | class HiltTestActivity : AppCompatActivity() {
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 |
--------------------------------------------------------------------------------
/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/atilsamancioglu/artbookhilttesting/roomdb/ArtDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.roomdb
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 |
6 | @Database(entities = [Art::class],version = 1)
7 | abstract class ArtDatabase : RoomDatabase() {
8 | abstract fun artDao() : ArtDao
9 | }
10 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 |
16 | include ':app'
17 | rootProject.name = "ArtBookHiltTesting"
--------------------------------------------------------------------------------
/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/com/atilsamancioglu/artbookhilttesting/roomdb/Art.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.roomdb
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity(tableName = "arts")
7 | data class Art(
8 | var name : String,
9 | var artistName : String,
10 | var year : Int,
11 | var imageUrl : String,
12 | @PrimaryKey(autoGenerate = true)
13 | val id : Int? = null
14 | )
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/atilsamancioglu/artbookhilttesting/roomdb/ArtDao.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.roomdb
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.room.*
5 |
6 | @Dao
7 | interface ArtDao {
8 |
9 | @Insert(onConflict = OnConflictStrategy.REPLACE)
10 | suspend fun insertArt(art : Art)
11 |
12 | @Delete
13 | suspend fun deleteArt(art: Art)
14 |
15 | @Query("SELECT * FROM arts")
16 | fun observeArts(): LiveData>
17 |
18 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/atilsamancioglu/artbookhilttesting/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting
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/com/atilsamancioglu/artbookhilttesting/api/RetrofitAPI.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.api
2 |
3 | import com.atilsamancioglu.artbookhilttesting.model.ImageResponse
4 | import com.atilsamancioglu.artbookhilttesting.util.Util.API_KEY
5 | import retrofit2.Response
6 | import retrofit2.http.GET
7 | import retrofit2.http.Query
8 |
9 | interface RetrofitAPI {
10 | @GET("/api/")
11 | suspend fun imageSearch(
12 | @Query("q") searchQuery: String,
13 | @Query("key") apiKey : String = API_KEY
14 | ) : Response
15 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atilsamancioglu/artbookhilttesting/HiltTestRunner.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting
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 |
10 | override fun newApplication(
11 | cl: ClassLoader?,
12 | className: String?,
13 | context: Context?
14 | ): Application {
15 | return super.newApplication(cl, HiltTestApplication::class.java.name, context)
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atilsamancioglu/artbookhilttesting/repo/ArtRepositoryInterface.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.repo
2 |
3 | import androidx.lifecycle.LiveData
4 | import com.atilsamancioglu.artbookhilttesting.model.ImageResponse
5 | import com.atilsamancioglu.artbookhilttesting.model.ImageResult
6 | import com.atilsamancioglu.artbookhilttesting.roomdb.Art
7 | import com.atilsamancioglu.artbookhilttesting.util.Resource
8 | import retrofit2.Response
9 |
10 | interface ArtRepositoryInterface {
11 |
12 | suspend fun insertArt(art : Art)
13 |
14 | suspend fun deleteArt(art: Art)
15 |
16 | fun getArt() : LiveData>
17 |
18 | suspend fun searchImage(imageString : String) : Resource
19 |
20 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/image_row.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/atilsamancioglu/artbookhilttesting/util/Resource.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.util
2 |
3 | data class Resource(val status: Status, val data: T?, val message: String?) {
4 |
5 | companion object {
6 |
7 | fun success(data: T?): Resource {
8 | return Resource(Status.SUCCESS, data, null)
9 | }
10 |
11 | fun error(msg: String, data: T?): Resource {
12 | return Resource(Status.ERROR, data, msg)
13 | }
14 |
15 | fun loading(data: T?): Resource {
16 | return Resource(Status.LOADING, data, null)
17 | }
18 |
19 | }
20 |
21 | }
22 |
23 | enum class Status {
24 | SUCCESS,
25 | ERROR,
26 | LOADING
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/java/com/atilsamancioglu/artbookhilttesting/view/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.view
2 |
3 | import androidx.appcompat.app.AppCompatActivity
4 | import android.os.Bundle
5 | import com.atilsamancioglu.artbookhilttesting.R
6 | import dagger.hilt.EntryPoint
7 | import dagger.hilt.android.AndroidEntryPoint
8 | import javax.inject.Inject
9 |
10 | @AndroidEntryPoint
11 | class MainActivity : AppCompatActivity() {
12 |
13 | @Inject
14 | lateinit var fragmentFactory: ArtFragmentFactory
15 |
16 | override fun onCreate(savedInstanceState: Bundle?) {
17 | super.onCreate(savedInstanceState)
18 | supportFragmentManager.fragmentFactory = fragmentFactory
19 | setContentView(R.layout.activity_main)
20 | }
21 | }
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
19 |
20 |
--------------------------------------------------------------------------------
/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/androidTest/java/com/atilsamancioglu/artbookhilttesting/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting
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("com.atilsamancioglu.artbookhilttesting", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atilsamancioglu/artbookhilttesting/dependencyinjection/TestAppModule.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.dependencyinjection
2 |
3 | import android.content.Context
4 | import androidx.room.Room
5 | import com.atilsamancioglu.artbookhilttesting.roomdb.ArtDatabase
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 javax.inject.Named
12 | import javax.inject.Singleton
13 |
14 | @Module
15 | @InstallIn(SingletonComponent::class)
16 | object TestAppModule {
17 |
18 | @Provides
19 | @Named("testDatabase")
20 | fun injectInMemoryRoom(@ApplicationContext context : Context) =
21 | Room.inMemoryDatabaseBuilder(context,ArtDatabase::class.java)
22 | .allowMainThreadQueries()
23 | .build()
24 |
25 |
26 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/java/com/atilsamancioglu/artbookhilttesting/model/ImageResult.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.model
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class ImageResult(
6 | val comments: Int,
7 | val downloads: Int,
8 | val favorites: Int,
9 | val id: Int,
10 | val imageHeight: Int,
11 | val imageSize: Int,
12 | val imageWidth : Int,
13 | val largeImageURL: String,
14 | val likes: Int,
15 | val pageURL : String,
16 | val previewHeight: Int,
17 | val previewURL: String,
18 | val previewWidth:Int,
19 | val tags: String,
20 | val type: String,
21 | val user: String,
22 | @SerializedName("user_id")
23 | val userId : Int,
24 | val userImageURL: String,
25 | val views : Int,
26 | val webformatHeight: Int,
27 | val webformatURL: String,
28 | val webformatWidth: Int
29 | )
30 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
21 |
--------------------------------------------------------------------------------
/app/src/test/java/com/atilsamancioglu/artbookhilttesting/MainCoroutineRule.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.ExperimentalCoroutinesApi
6 | import kotlinx.coroutines.test.TestCoroutineDispatcher
7 | import kotlinx.coroutines.test.TestCoroutineScope
8 | import kotlinx.coroutines.test.resetMain
9 | import kotlinx.coroutines.test.setMain
10 | import org.junit.rules.TestWatcher
11 | import org.junit.runner.Description
12 |
13 | @ExperimentalCoroutinesApi
14 | class MainCoroutineRule (
15 | private val dispatcher: CoroutineDispatcher = TestCoroutineDispatcher()
16 | ) : TestWatcher(), TestCoroutineScope by TestCoroutineScope(dispatcher) {
17 |
18 | override fun starting(description: Description?) {
19 | super.starting(description)
20 | Dispatchers.setMain(dispatcher)
21 | }
22 |
23 | override fun finished(description: Description?) {
24 | super.finished(description)
25 | cleanupTestCoroutines()
26 | Dispatchers.resetMain()
27 | }
28 |
29 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/atilsamancioglu/artbookhilttesting/LiveDataUtil.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.Observer
5 | import java.util.concurrent.CountDownLatch
6 | import java.util.concurrent.TimeUnit
7 | import java.util.concurrent.TimeoutException
8 |
9 | /* Copyright 2019 Google LLC.
10 | SPDX-License-Identifier: Apache-2.0 */
11 |
12 | fun LiveData.getOrAwaitValueTest(
13 | time: Long = 2,
14 | timeUnit: TimeUnit = TimeUnit.SECONDS
15 | ): T {
16 | var data: T? = null
17 | val latch = CountDownLatch(1)
18 | val observer = object : Observer {
19 | override fun onChanged(value: T) {
20 | data = value
21 | latch.countDown()
22 | this@getOrAwaitValueTest.removeObserver(this)
23 | }
24 |
25 | }
26 |
27 | this.observeForever(observer)
28 |
29 | // Don't wait indefinitely if the LiveData is not set.
30 | if (!latch.await(time, timeUnit)) {
31 | throw TimeoutException("LiveData value was never set.")
32 | }
33 |
34 | @Suppress("UNCHECKED_CAST")
35 | return data as T
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atilsamancioglu/artbookhilttesting/LiveDataTestingUtil.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.Observer
5 | import java.util.concurrent.CountDownLatch
6 | import java.util.concurrent.TimeUnit
7 | import java.util.concurrent.TimeoutException
8 |
9 |
10 | /* Copyright 2019 Google LLC.
11 | SPDX-License-Identifier: Apache-2.0 */
12 |
13 | fun LiveData.getOrAwaitValue(
14 | time: Long = 2,
15 | timeUnit: TimeUnit = TimeUnit.SECONDS
16 | ): T {
17 | var data: T? = null
18 | val latch = CountDownLatch(1)
19 | val observer = object : Observer {
20 | override fun onChanged(value: T) {
21 | data = value
22 | latch.countDown()
23 | this@getOrAwaitValue.removeObserver(this)
24 | }
25 |
26 | }
27 |
28 | this.observeForever(observer)
29 |
30 | // Don't wait indefinitely if the LiveData is not set.
31 | if (!latch.await(time, timeUnit)) {
32 | throw TimeoutException("LiveData value was never set.")
33 | }
34 |
35 | @Suppress("UNCHECKED_CAST")
36 | return data as T
37 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/atilsamancioglu/artbookhilttesting/repo/FakeArtRepository.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.repo
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import com.atilsamancioglu.artbookhilttesting.model.ImageResponse
6 | import com.atilsamancioglu.artbookhilttesting.roomdb.Art
7 | import com.atilsamancioglu.artbookhilttesting.util.Resource
8 |
9 | class FakeArtRepository : ArtRepositoryInterface {
10 |
11 | private val arts = mutableListOf()
12 | private val artsLiveData = MutableLiveData>(arts)
13 |
14 | override suspend fun insertArt(art: Art) {
15 | arts.add(art)
16 | refreshLiveData()
17 | }
18 |
19 | override suspend fun deleteArt(art: Art) {
20 | arts.remove(art)
21 | refreshLiveData()
22 | }
23 |
24 | override fun getArt(): LiveData> {
25 | return artsLiveData
26 | }
27 |
28 | override suspend fun searchImage(imageString: String): Resource {
29 | return Resource.success(ImageResponse(listOf(),0,0))
30 | }
31 |
32 | private fun refreshLiveData() {
33 | artsLiveData.postValue(arts)
34 | }
35 |
36 |
37 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atilsamancioglu/artbookhilttesting/repo/FakeArtRepositoryAndroid.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.repo
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import com.atilsamancioglu.artbookhilttesting.model.ImageResponse
6 | import com.atilsamancioglu.artbookhilttesting.roomdb.Art
7 | import com.atilsamancioglu.artbookhilttesting.util.Resource
8 |
9 | class FakeArtRepositoryAndroid : ArtRepositoryInterface {
10 |
11 | private val arts = mutableListOf()
12 | private val artsLiveData = MutableLiveData>(arts)
13 |
14 | override suspend fun insertArt(art: Art) {
15 | arts.add(art)
16 | refreshLiveData()
17 | }
18 |
19 | override suspend fun deleteArt(art: Art) {
20 | arts.remove(art)
21 | refreshLiveData()
22 | }
23 |
24 | override fun getArt(): LiveData> {
25 | return artsLiveData
26 | }
27 |
28 | override suspend fun searchImage(imageString: String): Resource {
29 | return Resource.success(ImageResponse(listOf(),0,0))
30 | }
31 |
32 | private fun refreshLiveData() {
33 | artsLiveData.postValue(arts)
34 | }
35 |
36 |
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atilsamancioglu/artbookhilttesting/view/ArtFragmentFactory.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.view
2 |
3 | import androidx.fragment.app.Fragment
4 | import androidx.fragment.app.FragmentFactory
5 | import com.atilsamancioglu.artbookhilttesting.adapter.ArtRecyclerAdapter
6 | import com.atilsamancioglu.artbookhilttesting.adapter.ImageRecyclerAdapter
7 | import com.bumptech.glide.RequestManager
8 | import kotlinx.coroutines.ExperimentalCoroutinesApi
9 | import javax.inject.Inject
10 |
11 | class ArtFragmentFactory @Inject constructor(
12 | private val imageRecyclerAdapter: ImageRecyclerAdapter,
13 | private val glide : RequestManager,
14 | private val artRecyclerAdapter: ArtRecyclerAdapter
15 | ) : FragmentFactory() {
16 |
17 | @OptIn(ExperimentalCoroutinesApi::class)
18 | override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
19 | return when(className){
20 | ImageApiFragment::class.java.name -> ImageApiFragment(imageRecyclerAdapter)
21 | ArtDetailsFragment::class.java.name -> ArtDetailsFragment(glide)
22 | ArtFragment::class.java.name -> ArtFragment(artRecyclerAdapter)
23 | else -> super.instantiate(classLoader, className)
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_image_api.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
19 |
20 |
26 |
27 |
32 |
33 |
--------------------------------------------------------------------------------
/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
22 | android.defaults.buildfeatures.buildconfig=true
23 | android.nonTransitiveRClass=false
24 | android.nonFinalResIds=false
--------------------------------------------------------------------------------
/.idea/jarRepositories.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 |
--------------------------------------------------------------------------------
/app/src/main/res/navigation/nav_graph.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
11 |
16 |
17 |
21 |
24 |
27 |
28 |
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/atilsamancioglu/artbookhilttesting/repo/ArtRepository.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.repo
2 |
3 | import androidx.lifecycle.LiveData
4 | import com.atilsamancioglu.artbookhilttesting.api.RetrofitAPI
5 | import com.atilsamancioglu.artbookhilttesting.model.ImageResponse
6 | import com.atilsamancioglu.artbookhilttesting.model.ImageResult
7 | import com.atilsamancioglu.artbookhilttesting.roomdb.Art
8 | import com.atilsamancioglu.artbookhilttesting.roomdb.ArtDao
9 | import com.atilsamancioglu.artbookhilttesting.util.Resource
10 | import retrofit2.Response
11 | import java.lang.Exception
12 | import javax.inject.Inject
13 |
14 | class ArtRepository @Inject constructor (
15 | private val artDao : ArtDao,
16 | private val retrofitApi : RetrofitAPI) : ArtRepositoryInterface {
17 |
18 | override suspend fun insertArt(art: Art) {
19 | artDao.insertArt(art)
20 | }
21 |
22 | override suspend fun deleteArt(art: Art) {
23 | artDao.deleteArt(art)
24 | }
25 |
26 | override fun getArt(): LiveData> {
27 | return artDao.observeArts()
28 | }
29 |
30 | override suspend fun searchImage(imageString: String): Resource {
31 | return try {
32 | val response = retrofitApi.imageSearch(imageString)
33 | if (response.isSuccessful) {
34 | response.body()?.let {
35 | return@let Resource.success(it)
36 | } ?: Resource.error("Error",null)
37 | } else {
38 | Resource.error("Error",null)
39 | }
40 | } catch (e: Exception) {
41 | Resource.error("No data!",null)
42 | }
43 | }
44 |
45 |
46 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atilsamancioglu/artbookhilttesting/view/ArtFragmentTest.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.view
2 |
3 | import androidx.navigation.NavController
4 | import androidx.navigation.Navigation
5 | import androidx.test.espresso.Espresso
6 | import androidx.test.espresso.action.ViewActions
7 | import androidx.test.espresso.matcher.ViewMatchers
8 | import androidx.test.filters.MediumTest
9 | import com.atilsamancioglu.artbookhilttesting.R
10 | import com.atilsamancioglu.artbookhilttesting.launchFragmentInHiltContainer
11 | import dagger.hilt.android.testing.HiltAndroidRule
12 | import dagger.hilt.android.testing.HiltAndroidTest
13 | import kotlinx.coroutines.ExperimentalCoroutinesApi
14 | import org.junit.Before
15 | import org.junit.Rule
16 | import org.junit.Test
17 | import org.mockito.Mockito
18 | import javax.inject.Inject
19 |
20 | @MediumTest
21 | @HiltAndroidTest
22 | @ExperimentalCoroutinesApi
23 | class ArtFragmentTest {
24 |
25 | @get:Rule
26 | var hiltRule = HiltAndroidRule(this)
27 |
28 | @Inject
29 | lateinit var fragmentFactory : ArtFragmentFactory
30 |
31 | @Before
32 | fun setup() {
33 | hiltRule.inject()
34 | }
35 |
36 | @Test
37 | fun testNavigationFromArtToArtDetails() {
38 | val navController = Mockito.mock(NavController::class.java)
39 |
40 | launchFragmentInHiltContainer(
41 | factory = fragmentFactory
42 | ) {
43 | Navigation.setViewNavController(requireView(),navController)
44 | }
45 |
46 | Espresso.onView(ViewMatchers.withId(R.id.fab)).perform(ViewActions.click())
47 | Mockito.verify(navController).navigate(
48 | ArtFragmentDirections.actionArtFragmentToArtDetailsFragment()
49 | )
50 | }
51 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_arts.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
16 |
17 |
25 |
26 |
27 |
28 |
39 |
40 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/test/java/com/atilsamancioglu/artbookhilttesting/viewmodel/ArtViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.viewmodel
2 |
3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4 | import com.atilsamancioglu.artbookhilttesting.MainCoroutineRule
5 | import com.atilsamancioglu.artbookhilttesting.getOrAwaitValueTest
6 | import com.atilsamancioglu.artbookhilttesting.repo.FakeArtRepository
7 | import com.atilsamancioglu.artbookhilttesting.util.Status
8 | import com.google.common.truth.Truth.assertThat
9 | import kotlinx.coroutines.ExperimentalCoroutinesApi
10 | import org.junit.Before
11 | import org.junit.Rule
12 | import org.junit.Test
13 |
14 | @ExperimentalCoroutinesApi
15 | class ArtViewModelTest {
16 |
17 | @get:Rule
18 | var instantTaskExecutorRule = InstantTaskExecutorRule()
19 |
20 | /*
21 | @get:Rule
22 | var mainCoroutineRule = MainCoroutineRule()
23 |
24 | */
25 |
26 | private lateinit var viewModel : ArtViewModel
27 |
28 | @Before
29 | fun setup() {
30 | viewModel = ArtViewModel(FakeArtRepository())
31 | }
32 |
33 | @Test
34 | fun `insert art without year returns error`() {
35 | viewModel.makeArt("Mona Lisa","Da Vinci","")
36 |
37 | val value = viewModel.insertArtMessage.getOrAwaitValueTest()
38 |
39 | assertThat(value.status).isEqualTo(Status.ERROR)
40 | }
41 |
42 |
43 | @Test
44 | fun `insert art without name returns error`() {
45 | viewModel.makeArt("","Da Vinci","1500")
46 |
47 | val value = viewModel.insertArtMessage.getOrAwaitValueTest()
48 |
49 | assertThat(value.status).isEqualTo(Status.ERROR)
50 | }
51 |
52 | @Test
53 | fun `insert art without artistName returns error`() {
54 | viewModel.makeArt("Mona Lisa","","1500")
55 |
56 | val value = viewModel.insertArtMessage.getOrAwaitValueTest()
57 |
58 | assertThat(value.status).isEqualTo(Status.ERROR)
59 | }
60 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/art_row.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
23 |
24 |
32 |
33 |
41 |
42 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/.idea/$CACHE_FILE$:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Android
10 |
11 |
12 | Code style issuesJava
13 |
14 |
15 | Control flow issuesJava
16 |
17 |
18 | CorrectnessLintAndroid
19 |
20 |
21 | Data flowJava
22 |
23 |
24 | Declaration redundancyJava
25 |
26 |
27 | Error handlingJava
28 |
29 |
30 | InitializationJava
31 |
32 |
33 | InternationalizationJava
34 |
35 |
36 | Java
37 |
38 |
39 | Java 9Java language level migration aidsJava
40 |
41 |
42 | Java language level migration aidsJava
43 |
44 |
45 | Kotlin
46 |
47 |
48 | LintAndroid
49 |
50 |
51 | Other problemsKotlin
52 |
53 |
54 | PerformanceJava
55 |
56 |
57 | Verbose or redundant code constructsJava
58 |
59 |
60 | VisibilityJava
61 |
62 |
63 |
64 |
65 | Android
66 |
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/.idea/androidTestResultsUserPreferences.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/app/src/main/java/com/atilsamancioglu/artbookhilttesting/adapter/ImageRecyclerAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.adapter
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import android.widget.ImageView
7 | import androidx.recyclerview.widget.AsyncListDiffer
8 | import androidx.recyclerview.widget.DiffUtil
9 | import androidx.recyclerview.widget.RecyclerView
10 | import com.atilsamancioglu.artbookhilttesting.R
11 | import com.bumptech.glide.RequestManager
12 | import javax.inject.Inject
13 |
14 | class ImageRecyclerAdapter @Inject constructor(
15 | val glide : RequestManager
16 | ) : RecyclerView.Adapter() {
17 |
18 | private var onItemClickListener : ((String) -> Unit)? = null
19 |
20 | class ImageViewHolder(itemView : View) : RecyclerView.ViewHolder(itemView)
21 |
22 | private val diffUtil = object : DiffUtil.ItemCallback() {
23 | override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
24 | return oldItem == newItem
25 | }
26 |
27 | override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
28 | return oldItem == newItem
29 | }
30 |
31 | }
32 |
33 | private val recyclerListDiffer = AsyncListDiffer(this,diffUtil)
34 |
35 | var images : List
36 | get() = recyclerListDiffer.currentList
37 | set(value) = recyclerListDiffer.submitList(value)
38 |
39 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder {
40 | val view = LayoutInflater.from(parent.context).inflate(R.layout.image_row,parent,false)
41 | return ImageViewHolder(view)
42 | }
43 |
44 |
45 | fun setOnItemClickListener(listener : (String) -> Unit) {
46 | onItemClickListener = listener
47 | }
48 |
49 |
50 | override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
51 | val imageView = holder.itemView.findViewById(R.id.singleArtImageView)
52 | val url = images[position]
53 | holder.itemView.apply {
54 | glide.load(url).into(imageView)
55 | setOnClickListener {
56 | onItemClickListener?.let {
57 | it(url)
58 | }
59 | }
60 | }
61 | }
62 |
63 | override fun getItemCount(): Int {
64 | return images.size
65 | }
66 |
67 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atilsamancioglu/artbookhilttesting/HiltExtension.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting
2 |
3 | import android.content.ComponentName
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import androidx.annotation.StyleRes
7 | import androidx.core.util.Preconditions
8 | import androidx.fragment.app.Fragment
9 | import androidx.fragment.app.FragmentFactory
10 | import androidx.fragment.app.testing.EmptyFragmentActivity
11 | import androidx.fragment.app.testing.FragmentScenario
12 | //import androidx.fragment.app.testing.FragmentScenario.EmptyFragmentActivity
13 | import androidx.test.core.app.ActivityScenario
14 | import androidx.test.core.app.ApplicationProvider
15 | import kotlinx.coroutines.ExperimentalCoroutinesApi
16 |
17 | /**
18 | * launchFragmentInContainer from the androidx.fragment:fragment-testing library
19 | * is NOT possible to use right now as it uses a hardcoded Activity under the hood
20 | * (i.e. [EmptyFragmentActivity]) which is not annotated with @AndroidEntryPoint.
21 | *
22 | * As a workaround, use this function that is equivalent. It requires you to add
23 | * [HiltTestActivity] in the debug folder and include it in the debug AndroidManifest.xml file
24 | * as can be found in this project.
25 | */
26 |
27 | inline fun launchFragmentInHiltContainer(
28 | fragmentArgs: Bundle? = null,
29 | @StyleRes themeResId: Int = R.style.FragmentScenarioEmptyFragmentActivityTheme,
30 | factory: FragmentFactory,
31 | crossinline action: T.() -> Unit = {}
32 | ) {
33 | val startActivityIntent = Intent.makeMainActivity(
34 | ComponentName(
35 | ApplicationProvider.getApplicationContext(),
36 | HiltTestActivity::class.java
37 | )
38 | ).putExtra(EmptyFragmentActivity.THEME_EXTRAS_BUNDLE_KEY, themeResId)
39 |
40 | ActivityScenario.launch(startActivityIntent).onActivity { activity ->
41 | activity.supportFragmentManager.fragmentFactory = factory
42 | val fragment: Fragment = activity.supportFragmentManager.fragmentFactory.instantiate(
43 | Preconditions.checkNotNull(T::class.java.classLoader),
44 | T::class.java.name
45 | )
46 | fragment.arguments = fragmentArgs
47 | activity.supportFragmentManager
48 | .beginTransaction()
49 | .add(android.R.id.content, fragment, "")
50 | .commitNow()
51 |
52 | (fragment as T).action()
53 | }
54 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/app/src/main/java/com/atilsamancioglu/artbookhilttesting/dependencyinjection/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.dependencyinjection
2 |
3 | import android.content.Context
4 | import androidx.room.Room
5 | import com.atilsamancioglu.artbookhilttesting.R
6 | import com.atilsamancioglu.artbookhilttesting.adapter.ArtRecyclerAdapter
7 | import com.atilsamancioglu.artbookhilttesting.adapter.ImageRecyclerAdapter
8 | import com.atilsamancioglu.artbookhilttesting.api.RetrofitAPI
9 | import com.atilsamancioglu.artbookhilttesting.repo.ArtRepository
10 | import com.atilsamancioglu.artbookhilttesting.repo.ArtRepositoryInterface
11 | import com.atilsamancioglu.artbookhilttesting.roomdb.ArtDao
12 | import com.atilsamancioglu.artbookhilttesting.roomdb.ArtDatabase
13 | import com.atilsamancioglu.artbookhilttesting.util.Util.BASE_URL
14 | import com.atilsamancioglu.artbookhilttesting.view.ArtFragmentFactory
15 | import com.bumptech.glide.Glide
16 | import com.bumptech.glide.RequestManager
17 | import com.bumptech.glide.request.RequestOptions
18 | import dagger.Module
19 | import dagger.Provides
20 | import dagger.hilt.InstallIn
21 | import dagger.hilt.android.qualifiers.ApplicationContext
22 | import dagger.hilt.components.SingletonComponent
23 | import retrofit2.Retrofit
24 | import retrofit2.converter.gson.GsonConverterFactory
25 | import javax.inject.Singleton
26 |
27 | @Module
28 | @InstallIn(SingletonComponent::class)
29 | object AppModule {
30 |
31 | @Singleton
32 | @Provides
33 | fun injectRoomDatabase(
34 | @ApplicationContext context: Context
35 | ) = Room.databaseBuilder(context,ArtDatabase::class.java,"ArtBookDB").build()
36 |
37 | @Singleton
38 | @Provides
39 | fun injectDao(
40 | database: ArtDatabase
41 | ) = database.artDao()
42 |
43 | @Singleton
44 | @Provides
45 | fun injectRetrofitAPI() : RetrofitAPI {
46 | return Retrofit.Builder()
47 | .addConverterFactory(GsonConverterFactory.create())
48 | .baseUrl(BASE_URL).build().create(RetrofitAPI::class.java)
49 | }
50 |
51 | @Singleton
52 | @Provides
53 | fun injectNormalRepo(dao : ArtDao, api: RetrofitAPI) = ArtRepository(dao,api) as ArtRepositoryInterface
54 |
55 | @Singleton
56 | @Provides
57 | fun injectGlide(@ApplicationContext context: Context) = Glide
58 | .with(context).setDefaultRequestOptions(
59 | RequestOptions().placeholder(R.drawable.ic_launcher_foreground)
60 | .error(R.drawable.ic_launcher_foreground)
61 | )
62 |
63 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atilsamancioglu/artbookhilttesting/adapter/ArtRecyclerAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.adapter
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import android.widget.ImageView
7 | import android.widget.TextView
8 | import androidx.recyclerview.widget.AsyncListDiffer
9 | import androidx.recyclerview.widget.DiffUtil
10 | import androidx.recyclerview.widget.RecyclerView
11 | import com.atilsamancioglu.artbookhilttesting.R
12 | import com.atilsamancioglu.artbookhilttesting.roomdb.Art
13 | import com.bumptech.glide.RequestManager
14 | import javax.inject.Inject
15 |
16 | class ArtRecyclerAdapter @Inject constructor(
17 | val glide : RequestManager
18 | ) : RecyclerView.Adapter() {
19 |
20 |
21 | class ArtViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
22 |
23 | private val diffUtil = object : DiffUtil.ItemCallback() {
24 | override fun areItemsTheSame(oldItem: Art, newItem: Art): Boolean {
25 | return oldItem == newItem
26 | }
27 |
28 | override fun areContentsTheSame(oldItem: Art, newItem: Art): Boolean {
29 | return oldItem == newItem
30 | }
31 |
32 |
33 | }
34 |
35 | private val recyclerListDiffer = AsyncListDiffer(this, diffUtil)
36 |
37 | var arts: List
38 | get() = recyclerListDiffer.currentList
39 | set(value) = recyclerListDiffer.submitList(value)
40 |
41 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArtViewHolder {
42 | val view = LayoutInflater.from(parent.context).inflate(R.layout.art_row, parent, false)
43 | return ArtViewHolder(view)
44 | }
45 |
46 |
47 | override fun onBindViewHolder(holder: ArtViewHolder, position: Int) {
48 | val imageView = holder.itemView.findViewById(R.id.artRowImageView)
49 | val nameText = holder.itemView.findViewById(R.id.artRowArtNameText)
50 | val artistNameText = holder.itemView.findViewById(R.id.artRowArtistNameText)
51 | val yearText = holder.itemView.findViewById(R.id.artRowYearText)
52 | val art = arts[position]
53 | holder.itemView.apply {
54 | glide.load(art.imageUrl).into(imageView)
55 | nameText.text = "Name: ${art.name}"
56 | artistNameText.text = "Artist Name: ${art.artistName}"
57 | yearText.text = "Year: ${art.year}"
58 | }
59 | }
60 |
61 | override fun getItemCount(): Int {
62 | return arts.size
63 | }
64 |
65 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atilsamancioglu/artbookhilttesting/view/ImageApiFragmentTest.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.view
2 |
3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4 | import androidx.navigation.NavController
5 | import androidx.navigation.Navigation
6 | import androidx.test.espresso.Espresso
7 | import androidx.test.espresso.action.ViewActions
8 | import androidx.test.espresso.action.ViewActions.click
9 | import androidx.test.espresso.contrib.RecyclerViewActions
10 | import androidx.test.espresso.matcher.ViewMatchers
11 | import androidx.test.filters.MediumTest
12 | import com.atilsamancioglu.artbookhilttesting.R
13 | import com.atilsamancioglu.artbookhilttesting.adapter.ImageRecyclerAdapter
14 | import com.atilsamancioglu.artbookhilttesting.getOrAwaitValue
15 | import com.atilsamancioglu.artbookhilttesting.launchFragmentInHiltContainer
16 | import com.atilsamancioglu.artbookhilttesting.repo.FakeArtRepositoryAndroid
17 | import com.atilsamancioglu.artbookhilttesting.viewmodel.ArtViewModel
18 | import com.google.common.truth.Truth.assertThat
19 | import dagger.hilt.android.testing.HiltAndroidRule
20 | import dagger.hilt.android.testing.HiltAndroidTest
21 | import kotlinx.coroutines.ExperimentalCoroutinesApi
22 | import org.junit.Before
23 | import org.junit.Rule
24 | import org.junit.Test
25 | import org.mockito.Mockito
26 | import javax.inject.Inject
27 |
28 | @MediumTest
29 | @HiltAndroidTest
30 | @ExperimentalCoroutinesApi
31 | class ImageApiFragmentTest {
32 |
33 | @get:Rule
34 | var instantTaskExecutorRule = InstantTaskExecutorRule()
35 |
36 | @get:Rule
37 | var hiltRule = HiltAndroidRule(this)
38 |
39 | @Inject
40 | lateinit var fragmentFactory : ArtFragmentFactory
41 |
42 | @Before
43 | fun setup() {
44 | hiltRule.inject()
45 | }
46 |
47 | @Test
48 | fun testSelectImage() {
49 | val navController = Mockito.mock(NavController::class.java)
50 | val selectedImageUrl = "atilsamancioglu.com"
51 | val testViewModel = ArtViewModel(FakeArtRepositoryAndroid())
52 |
53 | launchFragmentInHiltContainer(
54 | factory = fragmentFactory,
55 | ) {
56 | Navigation.setViewNavController(requireView(),navController)
57 | imageRecyclerAdapter.images = listOf(selectedImageUrl)
58 | viewModel = testViewModel
59 | }
60 |
61 | Espresso.onView(ViewMatchers.withId(R.id.imageRecyclerView)).perform(
62 | RecyclerViewActions.actionOnItemAtPosition(
63 | 0,click()
64 | )
65 |
66 | )
67 |
68 | Mockito.verify(navController).popBackStack()
69 |
70 | assertThat(testViewModel.selectedImageUrl.getOrAwaitValue()).isEqualTo(selectedImageUrl)
71 |
72 | }
73 | }
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/java/com/atilsamancioglu/artbookhilttesting/viewmodel/ArtViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.viewmodel
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.viewModelScope
7 | import com.atilsamancioglu.artbookhilttesting.model.ImageResponse
8 | import com.atilsamancioglu.artbookhilttesting.repo.ArtRepositoryInterface
9 | import com.atilsamancioglu.artbookhilttesting.roomdb.Art
10 | import com.atilsamancioglu.artbookhilttesting.util.Resource
11 | import dagger.hilt.android.lifecycle.HiltViewModel
12 | import kotlinx.coroutines.Dispatchers
13 | import kotlinx.coroutines.launch
14 | import kotlinx.coroutines.selects.whileSelect
15 | import kotlinx.coroutines.withContext
16 | import java.lang.Exception
17 | import javax.inject.Inject
18 |
19 | @HiltViewModel
20 | class ArtViewModel @Inject constructor(
21 | private val repository : ArtRepositoryInterface
22 | ) : ViewModel() {
23 |
24 | val artList = repository.getArt()
25 |
26 | private val images = MutableLiveData>()
27 | val imageList : LiveData>
28 | get() = images
29 |
30 | private val selectedImage = MutableLiveData()
31 | val selectedImageUrl : LiveData
32 | get() = selectedImage
33 |
34 | private var insertArtMsg = MutableLiveData>()
35 | val insertArtMessage : LiveData>
36 | get() = insertArtMsg
37 |
38 | //Solving the navigation bug
39 | fun resetInsertArtMsg() {
40 | insertArtMsg = MutableLiveData>()
41 | }
42 |
43 | fun setSelectedImage(url : String) {
44 | selectedImage.postValue(url)
45 | }
46 |
47 | fun deleteArt(art: Art) = viewModelScope.launch {
48 | repository.deleteArt(art)
49 | }
50 |
51 | fun insertArt(art: Art) = viewModelScope.launch {
52 | repository.insertArt(art)
53 | }
54 |
55 | fun makeArt(name : String, artistName : String, year : String) {
56 | if (name.isEmpty() || artistName.isEmpty() || year.isEmpty() ) {
57 | insertArtMsg.postValue(Resource.error("Enter name, artist, year", null))
58 | return
59 | }
60 | val yearInt = try {
61 | year.toInt()
62 | } catch (e: Exception) {
63 | insertArtMsg.postValue(Resource.error("Year should be number",null))
64 | return
65 | }
66 |
67 | val art = Art(name, artistName, yearInt,selectedImage.value?: "")
68 | insertArt(art)
69 | setSelectedImage("")
70 | insertArtMsg.postValue(Resource.success(art))
71 | }
72 |
73 | fun searchForImage (searchString : String) {
74 |
75 | if(searchString.isEmpty()) {
76 | return
77 | }
78 | images.value = Resource.loading(null)
79 | viewModelScope.launch {
80 | val response = repository.searchImage(searchString)
81 | images.value = response
82 | }
83 | }
84 |
85 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_art_details.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
21 |
22 |
32 |
33 |
45 |
46 |
57 |
58 |
69 |
70 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atilsamancioglu/artbookhilttesting/roomdb/ArtDaoTest.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.roomdb
2 |
3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4 | import androidx.fragment.app.testing.launchFragmentInContainer
5 | import androidx.navigation.NavController
6 | import androidx.navigation.Navigation
7 | import androidx.room.Room
8 | import androidx.test.core.app.ApplicationProvider
9 | import androidx.test.espresso.Espresso.onView
10 | import androidx.test.espresso.action.ViewActions.click
11 | import androidx.test.espresso.matcher.ViewMatchers.withId
12 | import androidx.test.ext.junit.runners.AndroidJUnit4
13 | import androidx.test.filters.SmallTest
14 | import com.atilsamancioglu.artbookhilttesting.R
15 | import com.atilsamancioglu.artbookhilttesting.adapter.ArtRecyclerAdapter
16 | import com.atilsamancioglu.artbookhilttesting.getOrAwaitValue
17 | import com.atilsamancioglu.artbookhilttesting.launchFragmentInHiltContainer
18 | import com.atilsamancioglu.artbookhilttesting.view.ArtFragment
19 | import com.atilsamancioglu.artbookhilttesting.view.ArtFragmentDirections
20 | import com.google.common.truth.Truth.assertThat
21 | import dagger.hilt.android.testing.HiltAndroidRule
22 | import dagger.hilt.android.testing.HiltAndroidTest
23 | import kotlinx.coroutines.ExperimentalCoroutinesApi
24 | import kotlinx.coroutines.test.runBlockingTest
25 | import kotlinx.coroutines.test.runTest
26 | import org.junit.After
27 | import org.junit.Before
28 | import org.junit.Rule
29 | import org.junit.Test
30 | import org.junit.runner.RunWith
31 | import org.mockito.Mockito.mock
32 | import org.mockito.Mockito.verify
33 | import javax.inject.Inject
34 | import javax.inject.Named
35 |
36 | @SmallTest
37 | @ExperimentalCoroutinesApi
38 | @HiltAndroidTest
39 | class ArtDaoTest {
40 |
41 | @get:Rule
42 | var instantTaskExecutorRule = InstantTaskExecutorRule()
43 |
44 | @get:Rule
45 | var hiltRule = HiltAndroidRule(this)
46 |
47 | @Inject
48 | @Named("testDatabase")
49 | lateinit var database : ArtDatabase
50 |
51 | private lateinit var dao : ArtDao
52 |
53 | @Before
54 | fun setup() {
55 | /*
56 | database = Room.inMemoryDatabaseBuilder(
57 | ApplicationProvider.getApplicationContext(),ArtDatabase::class.java)
58 | .allowMainThreadQueries() //this is a test case, we don't want other thread pools
59 | .build()
60 |
61 | */
62 | hiltRule.inject()
63 | dao = database.artDao()
64 | }
65 |
66 | @After
67 | fun teardown() {
68 | database.close()
69 | }
70 |
71 |
72 | @Test
73 | fun insertArtTesting() = runTest {
74 | val exampleArt = Art("Mona Lisa","Da Vinci",1700,"test.com",1)
75 | dao.insertArt(exampleArt)
76 |
77 | val list = dao.observeArts().getOrAwaitValue()
78 | assertThat(list).contains(exampleArt)
79 |
80 | }
81 |
82 | @Test
83 | fun deleteArtTesting() = runTest {
84 | val exampleArt = Art("Mona Lisa","Da Vinci",1700,"test.com",1)
85 | dao.insertArt(exampleArt)
86 | dao.deleteArt(exampleArt)
87 |
88 | val list = dao.observeArts().getOrAwaitValue()
89 | assertThat(list).doesNotContain(exampleArt)
90 |
91 | }
92 |
93 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atilsamancioglu/artbookhilttesting/view/ArtFragment.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.view
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import android.widget.Toast
6 | import androidx.fragment.app.Fragment
7 | import androidx.fragment.app.activityViewModels
8 | import androidx.fragment.app.viewModels
9 | import androidx.lifecycle.Observer
10 | import androidx.lifecycle.ViewModelProvider
11 | import androidx.navigation.findNavController
12 | import androidx.navigation.fragment.findNavController
13 | import androidx.recyclerview.widget.ItemTouchHelper
14 | import androidx.recyclerview.widget.LinearLayoutManager
15 | import androidx.recyclerview.widget.RecyclerView
16 | import com.atilsamancioglu.artbookhilttesting.R
17 | import com.atilsamancioglu.artbookhilttesting.adapter.ArtRecyclerAdapter
18 | import com.atilsamancioglu.artbookhilttesting.databinding.FragmentArtsBinding
19 | import com.atilsamancioglu.artbookhilttesting.util.Resource
20 | import com.atilsamancioglu.artbookhilttesting.util.Status
21 | import com.atilsamancioglu.artbookhilttesting.viewmodel.ArtViewModel
22 | import com.google.android.material.snackbar.Snackbar
23 | import dagger.hilt.android.AndroidEntryPoint
24 | import kotlinx.coroutines.ExperimentalCoroutinesApi
25 | import javax.inject.Inject
26 |
27 | @ExperimentalCoroutinesApi
28 | @AndroidEntryPoint
29 | class ArtFragment @Inject constructor(
30 | val artRecyclerAdapter: ArtRecyclerAdapter
31 | ) : Fragment(R.layout.fragment_arts) {
32 |
33 | //private val viewModel: ArtViewModel by viewModels()
34 | //private val viewModel: ArtViewModel by activityViewModels()
35 | lateinit var viewModel : ArtViewModel
36 |
37 | private var fragmentBinding : FragmentArtsBinding? = null
38 | private val swipeCallBack = object : ItemTouchHelper.SimpleCallback(0,ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
39 | override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
40 | return true
41 | }
42 |
43 | override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
44 | val layoutPosition = viewHolder.layoutPosition
45 | val selectedArt = artRecyclerAdapter.arts[layoutPosition]
46 | viewModel.deleteArt(selectedArt)
47 |
48 | }
49 |
50 | }
51 |
52 |
53 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
54 | super.onViewCreated(view, savedInstanceState)
55 | viewModel = ViewModelProvider(requireActivity()).get(ArtViewModel::class.java)
56 |
57 | val binding = FragmentArtsBinding.bind(view)
58 | fragmentBinding = binding
59 |
60 | subscribeToObservers()
61 |
62 | binding.recyclerViewArt.adapter = artRecyclerAdapter
63 | binding.recyclerViewArt.layoutManager = LinearLayoutManager(requireContext())
64 | ItemTouchHelper(swipeCallBack).attachToRecyclerView(binding.recyclerViewArt)
65 |
66 | binding.fab.setOnClickListener {
67 | findNavController().navigate(
68 | ArtFragmentDirections.actionArtFragmentToArtDetailsFragment()
69 | )
70 | }
71 |
72 | }
73 |
74 | private fun subscribeToObservers() {
75 | viewModel.artList.observe(viewLifecycleOwner, Observer {
76 | artRecyclerAdapter.arts = it
77 | })
78 | }
79 |
80 | override fun onDestroyView() {
81 | fragmentBinding = null
82 | super.onDestroyView()
83 | }
84 |
85 |
86 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atilsamancioglu/artbookhilttesting/view/ArtDetailsFragment.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.view
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import android.widget.Toast
6 | import androidx.activity.OnBackPressedCallback
7 | import androidx.fragment.app.Fragment
8 | import androidx.fragment.app.activityViewModels
9 | import androidx.fragment.app.viewModels
10 | import androidx.lifecycle.Observer
11 | import androidx.lifecycle.ViewModelProvider
12 | import androidx.navigation.Navigation
13 | import androidx.navigation.fragment.findNavController
14 | import com.atilsamancioglu.artbookhilttesting.R
15 | import com.atilsamancioglu.artbookhilttesting.databinding.FragmentArtDetailsBinding
16 | import com.atilsamancioglu.artbookhilttesting.util.Status
17 | import com.atilsamancioglu.artbookhilttesting.viewmodel.ArtViewModel
18 | import com.bumptech.glide.RequestManager
19 | import dagger.hilt.android.AndroidEntryPoint
20 | import javax.inject.Inject
21 |
22 | @AndroidEntryPoint
23 | class ArtDetailsFragment @Inject constructor(
24 | val glide : RequestManager
25 | ) : Fragment(R.layout.fragment_art_details) {
26 |
27 | //private val viewModel: ArtViewModel by activityViewModels()
28 | lateinit var viewModel : ArtViewModel
29 |
30 | private var fragmentBinding : FragmentArtDetailsBinding? = null
31 |
32 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
33 | super.onViewCreated(view, savedInstanceState)
34 | viewModel = ViewModelProvider(requireActivity()).get(ArtViewModel::class.java)
35 |
36 | val binding = FragmentArtDetailsBinding.bind(view)
37 | fragmentBinding = binding
38 |
39 | subscribeToObservers()
40 |
41 | binding.artImageView.setOnClickListener {
42 | findNavController().navigate(
43 | ArtDetailsFragmentDirections.actionArtDetailsFragmentToImageApiFragment()
44 | )
45 | }
46 |
47 |
48 | val callBack = object : OnBackPressedCallback(true){
49 | override fun handleOnBackPressed() {
50 | viewModel.setSelectedImage("")
51 | findNavController().popBackStack()
52 | }
53 | }
54 |
55 | requireActivity().onBackPressedDispatcher.addCallback(callBack)
56 |
57 | binding.saveButton.setOnClickListener {
58 | viewModel.makeArt(binding.nameText.text.toString(),
59 | binding.artistText.text.toString(),
60 | binding.yearText.text.toString())
61 |
62 | }
63 |
64 | }
65 |
66 | private fun subscribeToObservers() {
67 | viewModel.selectedImageUrl.observe(viewLifecycleOwner, Observer { url ->
68 | println(url)
69 | fragmentBinding?.let {
70 | glide.load(url).into(it.artImageView)
71 | }
72 | })
73 |
74 | viewModel.insertArtMessage.observe(viewLifecycleOwner, Observer {
75 | when (it.status) {
76 | Status.SUCCESS -> {
77 | Toast.makeText(requireActivity(),"Success",Toast.LENGTH_LONG).show()
78 | findNavController().navigateUp()
79 | viewModel.resetInsertArtMsg()
80 | }
81 |
82 | Status.ERROR -> {
83 | Toast.makeText(requireContext(),it.message ?: "Error",Toast.LENGTH_LONG).show()
84 | }
85 |
86 | Status.LOADING -> {
87 |
88 | }
89 | }
90 | })
91 | }
92 |
93 | override fun onDestroyView() {
94 | super.onDestroyView()
95 | fragmentBinding = null
96 |
97 | }
98 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atilsamancioglu/artbookhilttesting/view/ImageApiFragment.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.view
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import android.widget.Toast
6 | import androidx.core.widget.addTextChangedListener
7 | import androidx.fragment.app.Fragment
8 | import androidx.fragment.app.activityViewModels
9 | import androidx.fragment.app.viewModels
10 | import androidx.lifecycle.Observer
11 | import androidx.lifecycle.ViewModelProvider
12 | import androidx.lifecycle.lifecycleScope
13 | import androidx.navigation.fragment.findNavController
14 | import androidx.recyclerview.widget.GridLayoutManager
15 | import com.atilsamancioglu.artbookhilttesting.R
16 | import com.atilsamancioglu.artbookhilttesting.adapter.ImageRecyclerAdapter
17 | import com.atilsamancioglu.artbookhilttesting.databinding.FragmentArtsBinding
18 | import com.atilsamancioglu.artbookhilttesting.databinding.FragmentImageApiBinding
19 | import com.atilsamancioglu.artbookhilttesting.util.Status
20 | import com.atilsamancioglu.artbookhilttesting.viewmodel.ArtViewModel
21 | import dagger.hilt.android.AndroidEntryPoint
22 | import kotlinx.coroutines.Job
23 | import kotlinx.coroutines.delay
24 | import kotlinx.coroutines.launch
25 | import javax.inject.Inject
26 |
27 | @AndroidEntryPoint
28 | class ImageApiFragment @Inject constructor(
29 | val imageRecyclerAdapter: ImageRecyclerAdapter
30 | ) : Fragment(R.layout.fragment_image_api) {
31 |
32 | //private val viewModel: ArtViewModel by activityViewModels()
33 | lateinit var viewModel : ArtViewModel
34 |
35 | private var fragmentBinding : FragmentImageApiBinding? = null
36 |
37 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
38 | super.onViewCreated(view, savedInstanceState)
39 | viewModel = ViewModelProvider(requireActivity()).get(ArtViewModel::class.java)
40 |
41 | val binding = FragmentImageApiBinding.bind(view)
42 | fragmentBinding = binding
43 |
44 | var job: Job? = null
45 |
46 | binding.searchText.addTextChangedListener {
47 | job?.cancel()
48 | job = lifecycleScope.launch {
49 | delay(1000)
50 | it?.let {
51 | if (it.toString().isNotEmpty()) {
52 | viewModel.searchForImage(it.toString())
53 | }
54 | }
55 | }
56 | }
57 |
58 | subscribeToObservers()
59 |
60 | binding.imageRecyclerView.adapter = imageRecyclerAdapter
61 | binding.imageRecyclerView.layoutManager = GridLayoutManager(requireContext(),3)
62 |
63 | imageRecyclerAdapter.setOnItemClickListener {
64 | findNavController().popBackStack()
65 | viewModel.setSelectedImage(it)
66 | }
67 |
68 | }
69 |
70 | private fun subscribeToObservers() {
71 | viewModel.imageList.observe(viewLifecycleOwner, Observer {
72 | when(it.status) {
73 | Status.SUCCESS -> {
74 | val urls = it.data?.hits?.map { imageResult -> imageResult.previewURL }
75 | imageRecyclerAdapter.images = urls ?: listOf()
76 | fragmentBinding?.progressBar?.visibility = View.GONE
77 |
78 | }
79 |
80 | Status.ERROR -> {
81 | Toast.makeText(requireContext(),it.message ?: "Error",Toast.LENGTH_LONG).show()
82 | fragmentBinding?.progressBar?.visibility = View.GONE
83 |
84 | }
85 |
86 | Status.LOADING -> {
87 | fragmentBinding?.progressBar?.visibility = View.VISIBLE
88 |
89 | }
90 | }
91 |
92 | })
93 | }
94 |
95 | override fun onDestroyView() {
96 | fragmentBinding = null
97 | super.onDestroyView()
98 | }
99 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atilsamancioglu/artbookhilttesting/view/ArtDetailsFragmentTest.kt:
--------------------------------------------------------------------------------
1 | package com.atilsamancioglu.artbookhilttesting.view
2 |
3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4 | import androidx.navigation.NavController
5 | import androidx.navigation.Navigation
6 | import androidx.test.espresso.Espresso
7 | import androidx.test.espresso.Espresso.onView
8 | import androidx.test.espresso.Espresso.pressBack
9 | import androidx.test.espresso.action.ViewActions
10 | import androidx.test.espresso.action.ViewActions.click
11 | import androidx.test.espresso.action.ViewActions.replaceText
12 | import androidx.test.espresso.matcher.ViewMatchers
13 | import androidx.test.espresso.matcher.ViewMatchers.withId
14 | import androidx.test.filters.MediumTest
15 | import com.atilsamancioglu.artbookhilttesting.R
16 | import com.atilsamancioglu.artbookhilttesting.getOrAwaitValue
17 | import com.atilsamancioglu.artbookhilttesting.launchFragmentInHiltContainer
18 | import com.atilsamancioglu.artbookhilttesting.repo.FakeArtRepositoryAndroid
19 | import com.atilsamancioglu.artbookhilttesting.roomdb.Art
20 | import com.atilsamancioglu.artbookhilttesting.viewmodel.ArtViewModel
21 | import com.google.common.truth.Truth.assertThat
22 | import dagger.hilt.android.testing.HiltAndroidRule
23 | import dagger.hilt.android.testing.HiltAndroidTest
24 | import kotlinx.coroutines.ExperimentalCoroutinesApi
25 | import org.junit.Before
26 | import org.junit.Rule
27 | import org.junit.Test
28 | import org.mockito.Mockito
29 | import org.mockito.Mockito.verify
30 | import javax.inject.Inject
31 |
32 | @MediumTest
33 | @HiltAndroidTest
34 | @ExperimentalCoroutinesApi
35 | class ArtDetailsFragmentTest {
36 |
37 | @get:Rule
38 | var hiltRule = HiltAndroidRule(this)
39 |
40 | @get:Rule
41 | val instantTaskExecutorRule = InstantTaskExecutorRule()
42 |
43 | @Inject
44 | lateinit var fragmentFactory : ArtFragmentFactory
45 |
46 | @Before
47 | fun setup() {
48 | hiltRule.inject()
49 | }
50 |
51 | @Test
52 | fun testNavigationFromArtDetailsToImageAPI() {
53 | val navController = Mockito.mock(NavController::class.java)
54 |
55 | launchFragmentInHiltContainer(
56 | factory = fragmentFactory
57 | ) {
58 | Navigation.setViewNavController(requireView(),navController)
59 | }
60 |
61 | Espresso.onView(ViewMatchers.withId(R.id.artImageView)).perform(ViewActions.click())
62 | Mockito.verify(navController).navigate(
63 | ArtDetailsFragmentDirections.actionArtDetailsFragmentToImageApiFragment()
64 | )
65 | }
66 |
67 | @Test
68 | fun testOnBackPressed() {
69 | val navController = Mockito.mock(NavController::class.java)
70 | launchFragmentInHiltContainer(
71 | factory = fragmentFactory
72 | ) {
73 | Navigation.setViewNavController(requireView(),navController)
74 | }
75 |
76 | pressBack()
77 | verify(navController).popBackStack()
78 | }
79 |
80 | @Test
81 | fun testSave() {
82 | val testViewModel = ArtViewModel(FakeArtRepositoryAndroid())
83 | launchFragmentInHiltContainer(
84 | factory = fragmentFactory
85 | ) {
86 | viewModel = testViewModel
87 | }
88 |
89 | onView(withId(R.id.nameText)).perform(replaceText("Mona Lisa"))
90 | onView(withId(R.id.artistText)).perform(replaceText("Da Vinci"))
91 | onView(withId(R.id.yearText)).perform(replaceText("1700"))
92 | onView(withId(R.id.saveButton)).perform(click())
93 |
94 | assertThat(testViewModel.artList.getOrAwaitValue()).contains(
95 | Art(
96 | "Mona Lisa",
97 | "Da Vinci",
98 | 1700,"")
99 | )
100 |
101 | }
102 |
103 | }
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | id 'androidx.navigation.safeargs.kotlin'
5 | id 'com.google.dagger.hilt.android'
6 | id 'com.google.devtools.ksp'
7 | }
8 |
9 | android {
10 | compileSdk 34
11 |
12 | defaultConfig {
13 | applicationId "com.atilsamancioglu.artbookhilttesting"
14 | minSdkVersion 23
15 | targetSdkVersion 34
16 | versionCode 1
17 | versionName "1.0"
18 |
19 | testInstrumentationRunner "com.atilsamancioglu.artbookhilttesting.HiltTestRunner"
20 |
21 | ksp {
22 | arg('room.schemaLocation', "$projectDir/schemas")
23 | }
24 | }
25 |
26 | buildTypes {
27 | release {
28 | minifyEnabled false
29 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
30 | }
31 | }
32 | compileOptions {
33 | sourceCompatibility JavaVersion.VERSION_11
34 | targetCompatibility JavaVersion.VERSION_11
35 | }
36 | kotlinOptions {
37 | jvmTarget = '11'
38 | }
39 |
40 | buildFeatures {
41 | viewBinding true
42 | }
43 | namespace 'com.atilsamancioglu.artbookhilttesting'
44 |
45 | }
46 |
47 |
48 | dependencies {
49 |
50 | implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.0"
51 | implementation 'androidx.core:core-ktx:1.12.0'
52 | implementation 'androidx.appcompat:appcompat:1.6.1'
53 | implementation 'com.google.android.material:material:1.9.0'
54 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
55 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
56 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
57 |
58 | implementation "com.google.dagger:hilt-android:2.48"
59 | ksp "com.google.dagger:hilt-compiler:2.48"
60 | //implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03"
61 | ksp "androidx.hilt:hilt-compiler:1.0.0"
62 |
63 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
64 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
65 |
66 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2"
67 | implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
68 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.2"
69 | implementation "androidx.lifecycle:lifecycle-runtime:2.6.2"
70 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
71 | implementation "androidx.activity:activity-ktx:1.7.2"
72 |
73 | implementation "androidx.room:room-runtime:2.5.2"
74 | ksp "androidx.room:room-compiler:2.5.2"
75 | implementation "androidx.room:room-ktx:2.5.2"
76 | ksp("org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.5.0")
77 |
78 | implementation 'com.squareup.retrofit2:retrofit:2.9.0'
79 | implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
80 | implementation "androidx.navigation:navigation-fragment-ktx:2.7.3"
81 | implementation "androidx.navigation:navigation-ui-ktx:2.7.3"
82 | implementation 'com.github.bumptech.glide:glide:4.13.2'
83 | //kapt 'com.github.bumptech.glide:compiler:4.13.2'
84 | ksp 'com.github.bumptech.glide:ksp:4.14.2'
85 | implementation (platform("org.jetbrains.kotlin:kotlin-bom:1.8.0"))
86 |
87 |
88 | // TestImplementations
89 | implementation "androidx.test:core:1.5.0"
90 | testImplementation "junit:junit:4.13.2"
91 | testImplementation "org.hamcrest:hamcrest-all:1.3"
92 | testImplementation "androidx.arch.core:core-testing:2.2.0"
93 | testImplementation "org.robolectric:robolectric:4.8.1"
94 | testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4"
95 | testImplementation "com.google.truth:truth:1.1.3"
96 | testImplementation "org.mockito:mockito-core:4.7.0"
97 |
98 | // Android Test Implementations
99 | androidTestImplementation "junit:junit:4.13.2"
100 | //androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:2.12.1"
101 | androidTestImplementation "org.mockito:mockito-android:4.7.0"
102 | androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4"
103 | androidTestImplementation "androidx.arch.core:core-testing:2.2.0"
104 | androidTestImplementation "com.google.truth:truth:1.1.3"
105 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
106 | androidTestImplementation "org.mockito:mockito-core:4.7.0"
107 | androidTestImplementation 'com.google.dagger:hilt-android-testing:2.43.2'
108 | kspAndroidTest 'com.google.dagger:hilt-android-compiler:2.48'
109 | debugImplementation "androidx.fragment:fragment-testing:1.7.0-alpha05"
110 | //debugImplementation "androidx.fragment:fragment-testing:1.3.0-alpha08"
111 |
112 | androidTestImplementation("androidx.test.espresso:espresso-contrib:3.5.1") {
113 | exclude group : "org.checkerframework", module : "checker"
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------