Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/books list #24

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
/.idea/assetWizardSettings.xml
.DS_Store
/build
**/build
/captures
.externalNativeBuild
.cxx
Expand Down
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,39 @@ Using this endpoint, show a list of these items, with each row displaying at lea

Bonus Points:
- Unit tests

# The Solution:
## Project configuration
The project was refactored to a multi-module structure that can be used for a big, scalable application.
Currently it contains the following modules:
- app - the application module that holds the main Dagger graph, main activity and navigation graph
- component/base - module that can be shared with other modules that contains base application configuration, e.g. the network layer
- component/common - reusable general utilities, not coupled to the app business logic
- feature/books-list - Fragment that contains the books list

Changes in the project Gradle file were done only when necessary to support the new project structure.
When I create a project setup from scratch, I usually do a lot of things differently, e.g.:
- configure detekt
- configure code coverage measurement and enforcement
- update dependencies version
- use KSP instead of KAPT
- use src/main/kotlin and src/test/kotlin directories
- use Junit5 instead of Junit4

The Gradle files still may contain some code that should be cleaned up, e.g. unused dependencies should be removed and the order of dependencies should be updated (e.g. by sorting the alphabetically).
The project currently supports only debug build type. The release build type and code obfuscation have not been configured.

## MVP architecture
The presenter was created from scratch in a very simple way. It has a lifecycle connected to the Fragment lifecycle, so doesn't survive screen rotation.
It would be possible to make it live on the extended lifecycle scope, e.g. with using the 'https://github.com/konmik/nucleus' library that I used in the past, but it has been deprecated 5 years ago.
The presenter also currently doesn't support saving the state in a Bundle or SavedStateHandle.

## Libraries
I have used some additional "AndroidX" libraries, "Kluent" and "Mockito" for testing, "Moshi" for JSON parsing and kotlin coroutines for asynchronous operations. Using coroutines instead of RxJava should be recommended for new projects for better work with Android frameworks (including Compose), support for nullable values in streams and better error handling.

## UI
A very minimal time was spent on UI. It was designed just to be functional.
The whole list is loaded at once without pagination support.

## Tests
Due to the limited time spent on the project, only unit tests were created (but still some tests missing) and no integration tests.
32 changes: 10 additions & 22 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,9 @@ plugins {

android {
namespace 'com.example.otchallenge'
compileSdk 34

defaultConfig {
applicationId "com.example.otchallenge"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
Expand All @@ -24,37 +17,32 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'

buildFeatures {
// if it's not enabled on the app module, the app crashes with
// ClassNotFoundException: Didn't find class "androidx.databinding.DataBinderMapperImpl
dataBinding = true
}
}

dependencies {
implementation project(":component:base")
implementation project(":feature:books-list")

implementation libs.androidx.core.ktx
implementation libs.androidx.appcompat
implementation libs.material
implementation libs.androidx.activity
implementation libs.androidx.constraintlayout
implementation libs.androidx.navigation.fragment.ktx
implementation libs.androidx.navigation.ui.ktx

// dagger
implementation libs.dagger
kapt libs.dagger.compiler

//retrofit
implementation libs.retrofit
implementation libs.retrofit.rx.adapter

//glide
implementation libs.glide

//reactive x
implementation libs.rx.android
implementation libs.rx.java
implementation libs.rx.kotlin

testImplementation libs.junit
androidTestImplementation libs.androidx.junit
Expand Down
43 changes: 23 additions & 20 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
xmlns:tools="http://schemas.android.com/tools">

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AndroidOTChallenge"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<uses-permission android:name="android.permission.INTERNET" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
<application
android:name=".MyApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AndroidOTChallenge"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
39 changes: 28 additions & 11 deletions app/src/main/java/com/example/otchallenge/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,35 @@ import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.NavigationUI.setupActionBarWithNavController

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
(application as MyApplication).appComponent.inject(this)
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
}
private lateinit var navController: NavController

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}

navController = findNavController()
setupActionBarWithNavController(this, navController)
}

private fun findNavController(): NavController {
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
return navHostFragment.navController
}

override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp()
}
}
20 changes: 14 additions & 6 deletions app/src/main/java/com/example/otchallenge/MyApplication.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
package com.example.otchallenge

import android.app.Application
import com.example.otchallenge.bookslist.api.BooksListComponentProvider
import com.example.otchallenge.bookslist.internal.di.BooksListComponent
import com.example.otchallenge.di.AppComponent
import com.example.otchallenge.di.AppModule
import com.example.otchallenge.di.DaggerAppComponent

class MyApplication : Application() {
class MyApplication : Application(), BooksListComponentProvider {

lateinit var appComponent: AppComponent
private lateinit var appComponent: AppComponent

override fun onCreate() {
super.onCreate()
appComponent = DaggerAppComponent.builder().build()
}
override fun onCreate() {
super.onCreate()
appComponent = DaggerAppComponent.builder()
.appModule(AppModule(this))
.build()
}

override fun provideBooksListComponent(): BooksListComponent.Builder =
appComponent.booksListComponent()
}
29 changes: 18 additions & 11 deletions app/src/main/java/com/example/otchallenge/di/AppComponent.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package com.example.otchallenge.di

import com.example.otchallenge.MainActivity
import dagger.Component
import javax.inject.Singleton

@Singleton
@Component
interface AppComponent {
fun inject(activity: MainActivity)
}
package com.example.otchallenge.di

import com.example.otchallenge.base.internal.di.BaseModule
import com.example.otchallenge.bookslist.internal.di.BooksListComponent
import dagger.Component
import javax.inject.Singleton

@Singleton
@Component(
modules = [
AppModule::class,
BaseModule::class,
SubcomponentsModule::class,
]
)
interface AppComponent {
fun booksListComponent(): BooksListComponent.Builder
}
15 changes: 15 additions & 0 deletions app/src/main/java/com/example/otchallenge/di/AppModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.otchallenge.di

import android.app.Application
import android.content.Context
import dagger.Module
import dagger.Provides
import javax.inject.Singleton

@Module
internal class AppModule(private val application: Application) {

@Provides
@Singleton
fun provideContext(): Context = application
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.otchallenge.di

import com.example.otchallenge.bookslist.internal.di.BooksListComponent
import dagger.Module

/***
* this is a module which will register all the sub-components used in the project
*/
@Module(subcomponents = [BooksListComponent::class])
interface SubcomponentsModule
33 changes: 18 additions & 15 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />

</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
16 changes: 16 additions & 0 deletions app/src/main/res/navigation/nav_graph.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@id/booksListFragment">

<fragment
android:id="@+id/booksListFragment"
android:name="com.example.otchallenge.bookslist.api.BooksListFragment"
android:label="@string/app_name"
tools:layout="@layout/books_list_fragment">

</fragment>

</navigation>
2 changes: 1 addition & 1 deletion app/src/main/res/values-night/themes.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.AndroidOTChallenge" parent="Theme.Material3.DayNight.NoActionBar">
<style name="Base.Theme.AndroidOTChallenge" parent="Theme.Material3.DayNight">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>
Expand Down
5 changes: 0 additions & 5 deletions app/src/main/res/values/colors.xml

This file was deleted.

2 changes: 1 addition & 1 deletion app/src/main/res/values/themes.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.AndroidOTChallenge" parent="Theme.Material3.DayNight.NoActionBar">
<style name="Base.Theme.AndroidOTChallenge" parent="Theme.Material3.DayNight">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
Expand Down
17 changes: 0 additions & 17 deletions app/src/test/java/com/example/otchallenge/ExampleUnitTest.kt

This file was deleted.

Loading