Skip to content
Merged
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
29 changes: 21 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,39 @@
# BetterMe Android Test Case
# BetterMe Android Test Task

###### Movies client with offline-mode

Create the application which allows user to review the list of ongoing movies and add them to bookmarks with the following requirements:

![Sample of the UI](/ui_sample.png)
Create the application which allows user to review the list of ongoing movies and add them to
bookmarks with the following requirements:

#### Technical requirements

1. Provide proper logic for movies persistence (there are two sources: remote and local) and
retrieval (you might be interested in `MoviesRepository`), cover this logic with unit tests.
retrieval (you might be interested in `MoviesRepository`), cover this logic with unit tests.

2. There are pieces of code in this project which make some smell, some of them make this project
not work properly. Even though, it's compiling.
not work properly. Even though, it's compiling.

3. Make sure, the movies list is displayed properly, and favorites functionality works fine as well.

4. Provide error handling where it's needed (wrap exceptions, provide error placeholders on the UI layer, etc).
4. Provide error handling where it's needed (wrap exceptions, provide error placeholders on the UI
layer, etc).

5. Implement movie details screen / dialog / bottom sheet.

#### Notes

- There are two options of the UI layer implementation - Compose or XML.
In case of Compose you should update `TaskConstants.TASK_VARIANCE` accordingly.
There is no difference in requirements for both options - just choose whatever is more convenient
for you.
- Just imagine `MoviesRestStore` is a component which interacts with the real Movies database API
(even though it's not :D).
(even though it's not :D).
- You can change any code in this project as you wish

#### Sample of the UI (XML):

![Sample of the UI](/ui_sample_xml.png)

#### Sample of the UI (Compose):

![Sample of the UI](/ui_sample_compose.png)
85 changes: 1 addition & 84 deletions base/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,17 @@ apply plugin: 'com.android.application'
apply from: '../sharedconfigs-include.gradle'

android {
namespace 'app.bettermetesttask'

defaultConfig {
applicationId "app.androidtesttask"
versionCode 1
}

signingConfigs {
release {
Properties rProps = loadSigningProperties('debug_signing.properties')
storeFile = file(projectDir.canonicalPath + rProps['STORE_FILE'])
storePassword = rProps['STORE_PASSWORD']
keyAlias = rProps['KEY_ALIAS']
keyPassword = rProps['KEY_PASSWORD']
}
}

buildTypes {
debug {
multiDexEnabled true
minifyEnabled false
signingConfig signingConfigs.release
crunchPngs false
debuggable true
}
Expand All @@ -32,44 +22,17 @@ android {
shrinkResources true
debuggable false
crunchPngs true
signingConfig signingConfigs.release
matchingFallbacks = ['release']
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}

packagingOptions {
// Multiple libs contain this file -> cause build error
exclude 'LICENSE.txt'
exclude 'META-INF/rxjava.properties'
exclude 'META-INF/LICENSE.md'
exclude 'META-INF/LICENSE-notice.md'
}

lintOptions {
disable 'InvalidPackage'
disable 'GoogleAppIndexingWarning'
disable 'RestrictedApi'
abortOnError true
checkDependencies true
}

kapt {
// Avoid kapt mechanism that replaces every unknown type with NonExistentClass (for better dagger errors)
correctErrorTypes = true
javacOptions {
// Increase the max count of errors from annotation processors.
// Default is 100.
option("-Xmaxerrs", 500)
}
}
namespace 'app.bettermetesttask'
}

dependencies {
// Network
implementation project(":network-core")

// Domain
implementation project(":domain-core")
implementation project(":domain-movies")
Expand All @@ -81,8 +44,6 @@ dependencies {
implementation project(":feature-common")
implementation project(":feature-movies")

implementation kotlinDependencies.kotlinStdLib

implementation androidXDependencies.appCompat
implementation androidXDependencies.constraintLayout
implementation androidXDependencies.design
Expand All @@ -98,52 +59,8 @@ dependencies {
implementation diDependencies.daggerAndroidSupport
kapt diDependencies.daggerAndroidProcessor

implementation developmentDependencies.stetho
implementation developmentDependencies.stethoOkHttp

debugImplementation developmentDependencies.chucker
releaseImplementation developmentDependencies.chuckerNoOp
implementation developmentDependencies.timber
implementation developmentDependencies.threeTenABP

implementation dataDependencies.moshi
implementation dataDependencies.okHttp
implementation dataDependencies.okHttpInterceptor
implementation dataDependencies.retrofit
implementation dataDependencies.retrofitMoshiConverter

implementation dataDependencies.room
kapt dataDependencies.roomCompiler

testImplementation project(":tests-common")
testImplementation project(":tests-android-common")
}

Properties loadSigningProperties(String propertyFileName) {
Properties rProps = new Properties()
def rpFile = file(propertyFileName)
if (rpFile.canRead()) {
rProps.load(new FileInputStream(rpFile))

if (rProps != null && rProps.containsKey('STORE_FILE') && rProps.containsKey('STORE_PASSWORD') &&
rProps.containsKey('KEY_ALIAS') && rProps.containsKey('KEY_PASSWORD')) {
return rProps
} else {
throw new Exception(propertyFileName + " found but some entries are missing")
}
} else {
throw new FileNotFoundException(propertyFileName + " not found")
}
}

android.applicationVariants.all { variant ->
if (variant.buildType.isMinifyEnabled()) {
variant.assembleProvider.get().doLast {
copy {
from variant.mappingFile
into "${rootDir}/base/mappings"
rename { String filename -> "mapping-${variant.versionName}.txt" }
}
}
}
}
14 changes: 14 additions & 0 deletions base/src/main/java/app/bettermetesttask/constants/TaskConstants.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package app.bettermetesttask.constants

enum class TaskVariance {
COMPOSE,
XML,
}

/**
* The task variation - XML means that MoviesFragment is used as an entry point,
* while COMPOSE means that MoviesComposeFragment is used instead.
*/
object TaskConstants {
val TASK_VARIANCE = TaskVariance.XML
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package app.bettermetesttask.initializers

import android.app.Application
import app.bettermetesttask.domaincore.utils.coroutines.AppDispatchers
import app.bettermetesttask.domaincore.utils.coroutines.DispatcherProvider
import app.bettermetesttask.featurecommon.initializers.AppInitializer
import kotlinx.coroutines.Dispatchers
import javax.inject.Inject

class DispatchersInitializer @Inject constructor() : AppInitializer {
override fun init(application: Application) {
AppDispatchers.setInstance(object : DispatcherProvider {
override fun main() = Dispatchers.Main
override fun io() = Dispatchers.IO
override fun default() = Dispatchers.Default
override fun unconfined() = Dispatchers.Unconfined
})
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import app.bettermetesttask.datamovies.injection.MoviesDataModule
import app.bettermetesttask.featurecommon.injection.modules.CommonModule
import app.bettermetesttask.injection.modules.AppModule
import app.bettermetesttask.injection.modules.HomeActivityModule
import app.bettermetesttask.networkcore.injection.NetworkCoreModule
import dagger.BindsInstance
import dagger.Component
import dagger.android.support.AndroidSupportInjectionModule
Expand All @@ -16,7 +15,7 @@ import javax.inject.Singleton
@Singleton
@Component(
modules = [AppModule::class, AndroidSupportInjectionModule::class, HomeActivityModule::class,
CommonModule::class, NetworkCoreModule::class, MoviesDataModule::class]
CommonModule::class, MoviesDataModule::class]
)
interface AppComponent {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@ abstract class AppModule {

@Provides
fun provideAppInitializers(
stethoInitializer: StethoInitializer,
timberInitializer: TimberInitializer,
appSchedulersInitializer: AppSchedulersInitializer,
appDispatchersInitializer: DispatchersInitializer,
appLifecycleObserversInitializer: AppLifecycleObserversInitializer
) = AppInitializers(
stethoInitializer, timberInitializer, appSchedulersInitializer, appLifecycleObserversInitializer
timberInitializer, appDispatchersInitializer, appLifecycleObserversInitializer
)

@Provides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,31 @@ package app.bettermetesttask.navigation

import androidx.navigation.NavController
import app.bettermetesttask.R
import app.bettermetesttask.constants.TaskConstants
import app.bettermetesttask.constants.TaskVariance
import app.bettermetesttask.featurecommon.utils.navigation.executeSafeNavAction
import dagger.Lazy
import javax.inject.Inject

interface HomeNavigator {

fun navigateToMain()

}

class HomeNavigatorImpl @Inject constructor(
private val navController: Lazy<NavController>
private val navController: Lazy<NavController>,
) : HomeNavigator {

override fun navigateToMain() {
executeSafeNavAction {
navController.get().navigate(R.id.action_show_movies)
when (TaskConstants.TASK_VARIANCE) {
TaskVariance.COMPOSE -> {
navController.get().navigate(R.id.action_show_compose_movies)
}

TaskVariance.XML -> {
navController.get().navigate(R.id.action_show_movies)
}
}
}
}
}
8 changes: 8 additions & 0 deletions base/src/main/res/navigation/root_graph.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
app:startDestination="@+id/fragmentSplash">

<include app:graph="@navigation/movies_graph" />
<include app:graph="@navigation/movies_graph_compose" />

<fragment
android:id="@+id/fragmentSplash"
Expand All @@ -17,5 +18,12 @@
app:popUpTo="@id/root_graph"
app:popUpToInclusive="true" />

<action
android:id="@+id/action_show_compose_movies"
app:destination="@+id/movies_graph_compose"
app:launchSingleTop="true"
app:popUpTo="@id/root_graph"
app:popUpToInclusive="true" />

</fragment>
</navigation>

This file was deleted.

Loading