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
3 changes: 3 additions & 0 deletions .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 43 additions & 13 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
compileSdkVersion 28
compileSdkVersion rootProject.ext.compileSdkVersion

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

defaultConfig {
applicationId "com.decathlon.android.apptest"
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName "1.0"
applicationId rootProject.ext.applicationId
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode rootProject.ext.appVersionCode
versionName rootProject.ext.appVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
Expand All @@ -23,12 +29,36 @@ android {
}

dependencies {

implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.core:core-ktx:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"

//Androidx libraries
implementation "androidx.appcompat:appcompat:$appCompatVersion"
implementation "androidx.core:core-ktx:$coreKtxVersion"
implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion"
implementation "androidx.legacy:legacy-support-v4:$androidxLegacySupportV4Version"
implementation "androidx.recyclerview:recyclerview:$recyclerViewVersion"

//Design Libraries
implementation "com.google.android.material:material:$materialVersion"

//Network libraries
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
implementation "com.squareup.retrofit2:converter-moshi:$retrofitVersion"
implementation "com.squareup.moshi:moshi-kotlin:$moshiVersion"
implementation "com.squareup.moshi:moshi:$moshiVersion"
implementation "com.squareup.okhttp3:logging-interceptor:$okHttpVersion"
implementation "com.github.bumptech.glide:glide:$glideVersion"
annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion"

//Rx Libraries
implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
implementation "com.jakewharton.rxbinding3:rxbinding:$rxBindingsVersion"

//Test libraries
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test:runner:$androidTestRunnerVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@ package com.decathlon.android.apptest

import androidx.test.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4

import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Assert.*

/**
* Instrumented test, which will execute on an Android device.
*
Expand Down
7 changes: 6 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.decathlon.android.apptest">

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".GithubSearchActivity">
<activity
android:screenOrientation="portrait"
android:name=".githubsearch.GithubSearchActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.decathlon.android.apptest.common.base

interface BasePresenter {
fun onDestroy()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.decathlon.android.apptest.common.base

interface BaseView<T> {
var presenter: T
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.decathlon.android.apptest.common.base

import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import io.reactivex.observers.DisposableSingleObserver
import io.reactivex.schedulers.Schedulers

/**
* Abstract class for a UseCase that returns an instance of a [Single].
*/
abstract class SingleUseCase<T, in Params> {

private val disposables = CompositeDisposable()

/**
* Builds a [Single] which will be used when the current [SingleUseCase] is executed.
*/
protected abstract fun buildUseCaseObservable(params: Params? = null): Single<T>

/**
* Executes the current use case.
*
* @param observer {@link DisposableSingleObserver} which will be listening to the single build
* by {@link #buildUseCaseObservable(Params)} ()} method.
* @param params Parameters (Optional) used to build/execute this use case.
*/
open fun execute(singleObserver: DisposableSingleObserver<T>, params: Params? = null) {
val single = this.buildUseCaseObservable(params)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
addDisposable(single.subscribeWith(singleObserver))
}

/**
* Dispose from current [CompositeDisposable].
*/
fun dispose() {
if (!disposables.isDisposed) {
disposables.dispose()
}
}

/**
* Dispose from current [CompositeDisposable].
*/
private fun addDisposable(disposable: Disposable) {
disposables.add(disposable)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.decathlon.android.apptest.common.base

import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import io.reactivex.observers.DisposableObserver
import io.reactivex.schedulers.Schedulers

/**
* Abstract class for a UseCase that returns an instance of a [Observable].
*/
abstract class UseCase<T, in Params> {

private val disposables = CompositeDisposable()

/**
* Builds an {@link Observable} which will be used when executing the current {@link UseCase}.
*/
abstract fun buildUseCaseObservable(params: Params?): Observable<T>

/**
* Executes the current use case.
*
* @param observer {@link DisposableObserver} which will be listening to the observable build
* by {@link #buildUseCaseObservable(Params)} ()} method.
* @param params Parameters (Optional) used to build/execute this use case.
*/
fun execute(observer: DisposableObserver<T>, params: Params?) {
val observable = this.buildUseCaseObservable(params)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
addDisposable(observable.subscribeWith(observer))
}

/**
* Dispose from current [CompositeDisposable].
*/
fun dispose() {
if (!disposables.isDisposed) {
disposables.dispose()
}
}

/**
* Dispose from current [CompositeDisposable].
*/
private fun addDisposable(disposable: Disposable) {
disposables.add(disposable)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.decathlon.android.apptest.common.exception

class EmptyBodyException : Exception()
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.decathlon.android.apptest.common.exception

class ForbiddenException : Exception()
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.decathlon.android.apptest.common.exception

import java.io.IOException

class NoConnectivityException : IOException()
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.decathlon.android.apptest.common.exception

class UnknownErrorException : Exception()
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.decathlon.android.apptest.common.network

import android.content.Context
import com.decathlon.android.apptest.common.exception.NoConnectivityException
import okhttp3.Interceptor
import okhttp3.Response

class ConnectivityInterceptor(private val context: Context) : Interceptor {

override fun intercept(chain: Interceptor.Chain): Response {
if (!NetworkUtil.isOnline(context)) {
throw NoConnectivityException()
}

val builder = chain.request().newBuilder()
return chain.proceed(builder.build())
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.decathlon.android.apptest.common.network

object NetworkConstants {

const val NETWORK_TIMEOUT_IN_SECONDS: Long = 50
const val BASE_URL = "https://api.github.com"
const val ACCEPT_HEADER = "Accept"
const val REQUEST_GITHUB_V3_API = "application/vnd.github.mercy-preview+json"
const val PARAM_PAGE_NUMBER_KEY = "page"
const val PARAM_PAGE_NUMBER_VALUE = "1"
const val PARAM_PAGE_RESULT_KEY = "per_page"
const val PARAM_PAGE_RESULT_VALUE = "10"
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.decathlon.android.apptest.common.network

import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkInfo

class NetworkUtil {
companion object {
fun isOnline(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE)
return if (connectivityManager is ConnectivityManager) {
val networkInfo: NetworkInfo? = connectivityManager.activeNetworkInfo
networkInfo?.isConnected ?: false
} else false
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.decathlon.android.apptest.common.usecase

import com.decathlon.android.apptest.common.base.SingleUseCase
import com.decathlon.android.apptest.data.entity.search.SearchResult
import com.decathlon.android.apptest.data.repository.search.SearchRepository
import io.reactivex.Single

class SearchGitHubRepositories(private val searchRepository: SearchRepository) : SingleUseCase<SearchResult, String>() {
override fun buildUseCaseObservable(params: String?): Single<SearchResult> {
return params?.let { searchRepository.searchOnGithub(it) } as Single<SearchResult>
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.decathlon.android.apptest.data

import android.content.Context
import com.decathlon.android.apptest.BuildConfig
import com.decathlon.android.apptest.common.network.ConnectivityInterceptor
import com.decathlon.android.apptest.common.network.NetworkConstants
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import java.util.concurrent.TimeUnit

object ServiceGenerator {
private val httpLoggingInterceptor: HttpLoggingInterceptor
private val moshi: Moshi

init {
moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()

httpLoggingInterceptor = HttpLoggingInterceptor()
httpLoggingInterceptor.level =
if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
}

fun <S> createService(serviceClass: Class<S>, context: Context): S {
val okHttpClient = OkHttpClient()
.newBuilder()
.addInterceptor {
val originalRequest = it.request()
val builder = originalRequest
.newBuilder()
.addHeader(NetworkConstants.ACCEPT_HEADER, NetworkConstants.REQUEST_GITHUB_V3_API)
val newRequest = builder.build()
it.proceed(newRequest)
}
.addInterceptor(httpLoggingInterceptor)
.addInterceptor(ConnectivityInterceptor(context))
.connectTimeout(NetworkConstants.NETWORK_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS)
.writeTimeout(NetworkConstants.NETWORK_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS)
.readTimeout(NetworkConstants.NETWORK_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS)
.build()

val retrofit = Retrofit.Builder()
.baseUrl(NetworkConstants.BASE_URL)
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()

return retrofit.create(serviceClass)
}
}
Loading