Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
version: 2.1

orbs:
android: circleci/[email protected]

jobs:
build:
executor: android/android

steps:
- checkout

- run:
name: Chmod permissions for gradlew
command: sudo chmod +x ./gradlew

- run:
name: Run ktlint
command: ./gradlew ktlintCheck
- store_artifacts:
path: build/reports

- run:
name: Run detekt
command: ./gradlew detektAll
- store_artifacts:
path: build/staticAnalysis

- run:
name: Run Domain Unit Tests
command: ./gradlew :domain:test
- store_artifacts:
path: domain/build/reports
- store_test_results:
path: domain/build/test-results
28 changes: 28 additions & 0 deletions buildSrc/src/main/kotlin/Libs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@
* `$ ./gradlew buildSrcVersions`
*/
object Libs {
/**
* https://github.com/Kotlin/kotlinx.coroutines
*/
const val kotlinx_coroutines_android: String =
"org.jetbrains.kotlinx:kotlinx-coroutines-android:" +
Versions.org_jetbrains_kotlinx_kotlinx_coroutines

/**
* https://github.com/Kotlin/kotlinx.coroutines
*/
const val kotlinx_coroutines_test: String = "org.jetbrains.kotlinx:kotlinx-coroutines-test:" +
Versions.org_jetbrains_kotlinx_kotlinx_coroutines

/**
* https://kotlinlang.org/
*/
Expand Down Expand Up @@ -71,8 +84,23 @@ object Libs {
*/
const val material: String = "com.google.android.material:material:" + Versions.material

/**
* https://github.com/pinterest/ktlint
*/
const val ktlint: String = "com.pinterest:ktlint:" + Versions.ktlint

/**
* https://developer.android.com/studio
*/
const val aapt2: String = "com.android.tools.build:aapt2:" + Versions.aapt2

/**
* http://mockk.io
*/
const val mockk: String = "io.mockk:mockk:" + Versions.mockk

/**
* http://github.com/google/truth
*/
const val truth: String = "com.google.truth:truth:" + Versions.truth
}
8 changes: 8 additions & 0 deletions buildSrc/src/main/kotlin/Versions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import org.gradle.plugin.use.PluginDependencySpec
* YOU are responsible for updating manually the dependency version.
*/
object Versions {
const val org_jetbrains_kotlinx_kotlinx_coroutines: String = "1.4.0" // available: "1.4.2"

const val org_jetbrains_kotlin: String = "1.4.21"

const val com_android_tools_build_gradle: String = "4.1.1"
Expand All @@ -31,8 +33,14 @@ object Versions {

const val material: String = "1.2.1"

const val ktlint: String = "0.38.1" // available: "0.40.0"

const val aapt2: String = "4.1.1-6503028"

const val mockk: String = "1.10.3-jdk8" // available: "1.10.3"

const val truth: String = "1.0.1" // available: "1.1"

/**
* Current version: "6.5"
* See issue 19: How to update Gradle itself?
Expand Down
6 changes: 5 additions & 1 deletion domain/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@ plugins {
}

dependencies {

implementation(Libs.kotlinx_coroutines_android)
testImplementation(Libs.junit_junit)
testImplementation(Libs.mockk)
testImplementation(Libs.truth)
testImplementation(Libs.kotlinx_coroutines_test)
}
4 changes: 0 additions & 4 deletions domain/src/main/java/com/kryptkode/domain/Domain.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.kryptkode.domain.charactersearch.entities

data class Character(
val name: String,
val birthYear: String,
val height: String,
val url: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.kryptkode.domain.charactersearch.entities

data class Film(
val title: String,
val openingCrawl: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.kryptkode.domain.charactersearch.entities

data class Planet(
val name: String,
val population: String
) {
companion object {
val NO_PLANET = Planet("", "")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.kryptkode.domain.charactersearch.entities

data class Specie(
val name: String,
val language: String,
val homeWorld: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.kryptkode.domain.charactersearch.repo

import com.kryptkode.domain.charactersearch.entities.Film
import com.kryptkode.domain.charactersearch.entities.Planet
import com.kryptkode.domain.charactersearch.entities.Specie
import kotlinx.coroutines.flow.Flow

interface CharacterDetailRepository {
fun fetchPlanet(planetUrl: String): Flow<Planet>

fun fetchSpecies(urls: List<String>): Flow<List<Specie>>

fun fetchFilms(urls: List<String>): Flow<List<Film>>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.kryptkode.domain.charactersearch.repo

import com.kryptkode.domain.charactersearch.entities.Character
import kotlinx.coroutines.flow.Flow

interface SearchCharactersRepository {
fun searchCharacters(characterName: String): Flow<List<Character>>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.kryptkode.domain.charactersearch.usecases

import com.kryptkode.domain.charactersearch.entities.Film
import com.kryptkode.domain.charactersearch.repo.CharacterDetailRepository
import com.kryptkode.domain.dispatchers.AppDispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn

class FetchFilmsUseCase(
private val appDispatchers: AppDispatchers,
private val repository: CharacterDetailRepository) {

fun fetchFilms(urls: List<String>): Flow<List<Film>> {
if (urls.isEmpty()) {
return flowOf(emptyList())
}
return repository.fetchFilms(urls).flowOn(appDispatchers.io)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.kryptkode.domain.charactersearch.usecases

import com.kryptkode.domain.charactersearch.entities.Planet
import com.kryptkode.domain.charactersearch.entities.Planet.Companion.NO_PLANET
import com.kryptkode.domain.charactersearch.repo.CharacterDetailRepository
import com.kryptkode.domain.dispatchers.AppDispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn

class FetchPlanetUseCase(private val dispatchers: AppDispatchers,
private val repository: CharacterDetailRepository) {

fun fetchPlanet(planetUrl: String): Flow<Planet> {
if(planetUrl.isEmpty()){
return flowOf(NO_PLANET)
}
return repository.fetchPlanet(planetUrl).flowOn(dispatchers.io)
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.kryptkode.domain.charactersearch.usecases

import com.kryptkode.domain.charactersearch.entities.Specie
import com.kryptkode.domain.charactersearch.repo.CharacterDetailRepository
import com.kryptkode.domain.dispatchers.AppDispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn

class FetchSpeciesUseCase(
private val dispatchers: AppDispatchers,
private val repository: CharacterDetailRepository
) {
fun fetchSpecies(urls: List<String>): Flow<List<Specie>> {
if (urls.isEmpty()) {
return flowOf(emptyList())
}
return repository.fetchSpecies(urls).flowOn(dispatchers.io)
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.kryptkode.domain.charactersearch.usecases

import com.kryptkode.domain.charactersearch.entities.Character
import com.kryptkode.domain.charactersearch.repo.SearchCharactersRepository
import com.kryptkode.domain.dispatchers.AppDispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn

class SearchCharactersUseCase(
private val appDispatchers: AppDispatchers,
private val searchCharactersRepository: SearchCharactersRepository
) {
fun searchCharacters(query: String): Flow<List<Character>> {
return searchCharactersRepository.searchCharacters(query).flowOn(appDispatchers.io)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.kryptkode.domain.dispatchers

import kotlinx.coroutines.CoroutineDispatcher

interface AppDispatchers {
val main: CoroutineDispatcher
val io: CoroutineDispatcher
val default: CoroutineDispatcher
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.kryptkode.domain.charactersearch.usecases

import com.google.common.truth.Truth.assertThat
import com.kryptkode.domain.charactersearch.entities.Film
import com.kryptkode.domain.charactersearch.repo.CharacterDetailRepository
import com.kryptkode.domain.dispatchers.AppDispatchers
import com.kryptkode.domain.utils.DataFactory.randomString
import com.kryptkode.domain.utils.MockDataFactory.makeFilm
import io.mockk.every
import io.mockk.mockk
import io.mockk.unmockkAll
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineDispatcher
import org.junit.After
import org.junit.Before
import org.junit.Test

@ExperimentalCoroutinesApi
class FetchFilmsUseCaseTest {

private lateinit var dispatchers: AppDispatchers
private lateinit var repository: CharacterDetailRepository
private lateinit var SUT: FetchFilmsUseCase

@Before
fun setUp() {
dispatchers = mockk()
repository = mockk()
SUT = FetchFilmsUseCase(dispatchers, repository)
}

@After
fun tearDown() {
unmockkAll()
}

@Test
fun `fetching films calls repository with passed parameter`() {
stubDispatchers()
stubRepo()
val testQuery = listOf(randomString(), randomString())
SUT.fetchFilms(testQuery)
verify(exactly = 1) {
repository.fetchFilms(testQuery)
}
}

@Test
fun `fetching films returns data`() = runBlocking {
stubDispatchers()
val testCharacters = listOf(makeFilm(), makeFilm())
stubRepo(testCharacters)

val testQuery = listOf(randomString(), randomString())
val result = SUT.fetchFilms(testQuery).first()

assertThat(result).isEqualTo(testCharacters)
}

@Test
fun `fetching films with empty urls returns empty films list`() = runBlocking {
val result = SUT.fetchFilms(emptyList())
assertThat(result.first()).isEmpty()
}

@Test
fun `fetching films runs on io dispatcher`() {
stubDispatchers()
stubRepo()

val testUrls = listOf(randomString(), randomString())
SUT.fetchFilms(testUrls)
verify(exactly = 1) {
dispatchers.io
}
}

private fun stubDispatchers() {
every {
dispatchers.default
} returns TestCoroutineDispatcher()

every {
dispatchers.io
} returns TestCoroutineDispatcher()

every {
dispatchers.main
} returns TestCoroutineDispatcher()
}

private fun stubRepo(list: List<Film> = emptyList()) {
every {
repository.fetchFilms(any())
} returns flowOf(list)
}
}
Loading