├── .github
└── workflows
│ └── build.yaml
├── .gitignore
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
├── src
│ ├── development
│ │ └── res
│ │ │ └── values
│ │ │ └── strings.xml
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── pratama
│ │ │ │ └── baseandroid
│ │ │ │ ├── MyApp.kt
│ │ │ │ ├── data
│ │ │ │ ├── datasource
│ │ │ │ │ ├── local
│ │ │ │ │ │ ├── NewsLocalDatasource.kt
│ │ │ │ │ │ ├── NewsLocalDatasourceImpl.kt
│ │ │ │ │ │ ├── dao
│ │ │ │ │ │ │ └── NewsDao.kt
│ │ │ │ │ │ ├── db
│ │ │ │ │ │ │ └── AppDatabase.kt
│ │ │ │ │ │ └── entity
│ │ │ │ │ │ │ └── NewsEntity.kt
│ │ │ │ │ └── remote
│ │ │ │ │ │ ├── NewsRemoteDatasource.kt
│ │ │ │ │ │ ├── NewsRemoteDatasourceImpl.kt
│ │ │ │ │ │ ├── interceptor
│ │ │ │ │ │ ├── HeaderInterceptor.kt
│ │ │ │ │ │ ├── TokenInterceptor.kt
│ │ │ │ │ │ └── TokenRefreshAuthenticator.kt
│ │ │ │ │ │ ├── model
│ │ │ │ │ │ ├── NewsResponse.kt
│ │ │ │ │ │ └── TopHeadlineResponse.kt
│ │ │ │ │ │ └── service
│ │ │ │ │ │ └── NewsApiServices.kt
│ │ │ │ └── repository
│ │ │ │ │ ├── NewsRepository.kt
│ │ │ │ │ └── NewsRepositoryImpl.kt
│ │ │ │ ├── di
│ │ │ │ └── module
│ │ │ │ │ ├── ActivityModule.kt
│ │ │ │ │ └── ApplicationModule.kt
│ │ │ │ ├── domain
│ │ │ │ ├── entity
│ │ │ │ │ ├── News.kt
│ │ │ │ │ └── NewsSource.kt
│ │ │ │ └── usecase
│ │ │ │ │ ├── GetTopHeadlineUseCase.kt
│ │ │ │ │ └── GetTopHeadlineUseCaseFlow.kt
│ │ │ │ ├── ui
│ │ │ │ ├── detailpage
│ │ │ │ │ └── DetailNewsFragment.kt
│ │ │ │ ├── dto
│ │ │ │ │ ├── NewsDto.kt
│ │ │ │ │ └── NewsSourceDto.kt
│ │ │ │ ├── homepage
│ │ │ │ │ ├── HomePageActivity.kt
│ │ │ │ │ ├── HomePageModule.kt
│ │ │ │ │ ├── ListNewsFragment.kt
│ │ │ │ │ ├── ListNewsViewModel.kt
│ │ │ │ │ └── rvitem
│ │ │ │ │ │ └── NewsItem.kt
│ │ │ │ └── splash
│ │ │ │ │ └── SplashActivity.kt
│ │ │ │ └── utility
│ │ │ │ └── ThreadInfoLogger.kt
│ │ └── res
│ │ │ ├── anim
│ │ │ ├── enter_from_left.xml
│ │ │ ├── enter_from_right.xml
│ │ │ ├── exit_to_left.xml
│ │ │ └── exit_to_right.xml
│ │ │ ├── drawable-v24
│ │ │ └── ic_launcher_foreground.xml
│ │ │ ├── drawable
│ │ │ ├── ic_launcher_background.xml
│ │ │ └── splashscreen.xml
│ │ │ ├── font
│ │ │ └── architects_daughter.xml
│ │ │ ├── layout
│ │ │ ├── activity_home.xml
│ │ │ ├── activity_home_page.xml
│ │ │ ├── activity_main.xml
│ │ │ ├── activity_splash.xml
│ │ │ ├── fragment_detail_news.xml
│ │ │ ├── fragment_list_news.xml
│ │ │ ├── layout_item_article.xml
│ │ │ ├── layout_item_loadmore_loading.xml
│ │ │ └── rv_item_news.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.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
│ │ │ ├── navigation
│ │ │ └── home_nav_graph.xml
│ │ │ ├── values-v21
│ │ │ └── styles.xml
│ │ │ └── values
│ │ │ ├── colors.xml
│ │ │ ├── font_certs.xml
│ │ │ ├── preloaded_fonts.xml
│ │ │ ├── strings.xml
│ │ │ └── styles.xml
│ └── test
│ │ └── java
│ │ └── com
│ │ └── pratama
│ │ └── baseandroid
│ │ ├── data
│ │ ├── datasource
│ │ │ ├── local
│ │ │ │ └── NewsLocalDatasourceImplTest.kt
│ │ │ └── remote
│ │ │ │ └── NewsRemoteDatasourceImplTest.kt
│ │ └── repository
│ │ │ └── NewsRepositoryImplTest.kt
│ │ └── domain
│ │ └── usecase
│ │ └── GetTopHeadlineUseCaseTest.kt
└── version.properties
├── build-system
└── dependencies.gradle
├── build.gradle
├── buildSrc
├── .gitignore
├── build.gradle.kts
└── src
│ └── main
│ └── java
│ └── Dependencies.kt
├── codecov.yml
├── config
├── detekt.gradle
├── detekt_config.yml
└── jacoco.gradle
├── core-android
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── pratama
│ │ └── baseandroid
│ │ └── coreandroid
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ └── java
│ │ └── com
│ │ └── pratama
│ │ └── baseandroid
│ │ └── coreandroid
│ │ ├── BaseActivity.kt
│ │ ├── BaseFragment.kt
│ │ ├── BaseViewModel.kt
│ │ ├── base
│ │ ├── BaseActivityBinding.kt
│ │ ├── BaseFragmentBinding.kt
│ │ └── network
│ │ │ ├── NetworkboundResource.kt
│ │ │ └── Resource.kt
│ │ ├── exception
│ │ └── Failure.kt
│ │ ├── extensions
│ │ ├── Activity.kt
│ │ ├── Fragment.kt
│ │ └── View.kt
│ │ ├── functional
│ │ └── Either.kt
│ │ ├── network
│ │ ├── NetworkChecker.kt
│ │ └── NetworkCheckerImpl.kt
│ │ └── usecase
│ │ └── UseCase.kt
│ └── test
│ └── java
│ └── com
│ └── pratama
│ └── baseandroid
│ └── coreandroid
│ └── ExampleUnitTest.kt
├── dep.txt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── plugins
├── .gitignore
├── build.gradle
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
├── settings.gradle
└── src
│ └── main
│ └── java
│ └── com
│ └── pratama
│ └── hello
│ └── HelloPlugin.kt
├── routing
├── .gitignore
├── build.gradle
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── com
│ └── pratama
│ └── baseandroid
│ └── routing
│ └── Routes.kt
├── secrets.properties
├── secrets.properties.example
├── settings.gradle
├── ss
└── ss1.png
└── travis_bak.yml
/.github/workflows/build.yaml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on: [pull_request, push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Checkout
10 | uses: actions/checkout@v2
11 |
12 | - name: Add Permission
13 | run: chmod +x gradlew
14 |
15 | - name: Gradle Wrapper Validation
16 | uses: gradle/wrapper-validation-action@v1
17 |
18 | - uses: actions/cache@v2
19 | with:
20 | path: |
21 | ~/.gradle/caches
22 | ~/.gradle/wrapper
23 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
24 | restore-keys: |
25 | ${{ runner.os }}-gradle-
26 |
27 | - name: Build Debug Apk
28 | run: ./gradlew assembleDebug
29 |
30 | - name: Run test & Coverage
31 | run: ./gradlew check jacocoTestReportDebug
32 |
33 | # - name: Merge Report
34 | # run: ./gradlew mergeJacocoReports && ./gradlew jacocoTestReportMerged
35 |
36 | - name: Publish CodeCoverage
37 | run: bash <(curl -s https://codecov.io/bash) -f jacocoReport/**/*.xml
38 | # - name: Upload APK
39 | # uses: actions/upload-artifact@v1
40 | # with:
41 | # name: app
42 | # path: app/build/outputs/apk
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .idea
3 | .gradle
4 | /local.properties
5 | /.idea/workspace.xml
6 | /.idea/libraries
7 | .DS_Store
8 | /build
9 | /captures
10 | .externalNativeBuild
11 | *.jks
12 | jacocoReport
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Base Android Project with Kotlin
3 | MVVM Base Android Kotlin Project
4 |
5 | [](https://github.com/pratamawijaya/BaseKotlinAndroid/actions/workflows/build.yaml)
6 |
7 | [](https://codecov.io/gh/pratamawijaya/BaseKotlinAndroid)
8 |
9 |
10 |
11 | ## Tech Stack
12 | - Kotlin
13 | - AndroidX
14 | - Coroutine
15 | - Retrofit / OkHttp
16 | - Gson
17 | - Groupie as Recyclerview lib
18 | - sdp ssp
19 | - Hilt as Dependency Injection lib
20 | - Picasso as Image loader
21 |
22 | ## Extra
23 | - `./gradlew detekt` static analysis with detekt
24 | - `./gradlew testDebugUnitTestCoverage` for codeCoverage
25 | - `./gradlew check` for running all
26 |
27 | ## How to use
28 | - Register and create your api key here https://newsapi.org/register
29 | - Copy secrets.properties.example to secrets.properties and put ur API KEY
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /production
3 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id "com.android.application"
3 | id "kotlin-android"
4 | id "kotlin-kapt"
5 | id "dagger.hilt.android.plugin"
6 | id "kotlin-parcelize"
7 | id "androidx.navigation.safeargs.kotlin"
8 | }
9 |
10 | android {
11 |
12 | defaultConfig {
13 | applicationId "com.pratama.baseandroid"
14 | minSdkVersion 22
15 | compileSdkVersion 33
16 | targetSdkVersion 33
17 | versionCode 1
18 | versionName "1.0"
19 |
20 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
21 | }
22 |
23 | buildTypes {
24 | debug {
25 | applicationIdSuffix ".debug"
26 | minifyEnabled false
27 | testCoverageEnabled true
28 | }
29 | release {
30 | minifyEnabled false
31 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
32 | }
33 | }
34 |
35 | buildFeatures {
36 | viewBinding true
37 | }
38 |
39 | compileOptions {
40 | sourceCompatibility JavaVersion.VERSION_1_8
41 | targetCompatibility JavaVersion.VERSION_1_8
42 | }
43 |
44 | kotlinOptions {
45 | jvmTarget = JavaVersion.VERSION_1_8.toString()
46 | }
47 | }
48 |
49 | dependencies {
50 | implementation fileTree(dir: "libs", include: ["*.jar"])
51 |
52 | implementation project(":core-android")
53 | implementation project(":routing")
54 |
55 | implementation supportLibraries.kotlin_stdlib
56 | implementation supportLibraries.core_ktx
57 | implementation supportLibraries.appCompat
58 | implementation AndroidLib.androidx_constraintlayout
59 |
60 | implementation AndroidLib.retrofit_android
61 | implementation(AndroidLib.gson_converter)
62 | implementation AndroidLib.okhttp_logging
63 |
64 | implementation(AndroidLib.groupie)
65 | implementation(AndroidLib.groupie_viewbinding)
66 |
67 | implementation(AndroidLib.room)
68 | implementation(AndroidLib.room_coroutine)
69 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$Versions.kotlin"
70 | implementation 'androidx.legacy:legacy-support-v4:1.0.0'
71 | kapt(AndroidLib.room_compiler)
72 |
73 | // Kotlin
74 | implementation "androidx.navigation:navigation-fragment-ktx:$Versions.android_navigation"
75 | implementation "androidx.navigation:navigation-ui-ktx:$Versions.android_navigation"
76 |
77 | implementation(AndroidLib.viewmodel_ktx)
78 | implementation(AndroidLib.viewmodel_runtime)
79 | implementation(AndroidLib.viewmodel_extension)
80 |
81 | kapt(AndroidLib.viewmodel_compiler)
82 |
83 | implementation 'com.intuit.sdp:sdp-android:1.0.6'
84 | implementation 'com.intuit.ssp:ssp-android:1.0.6'
85 |
86 | implementation(AndroidLib.timber)
87 |
88 | implementation AndroidLib.hilt
89 | kapt AndroidLib.hilt_processor_compiler
90 |
91 | def fragment_version = "1.5.5"
92 |
93 | testImplementation 'junit:junit:4.13.2'
94 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
95 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
96 |
97 | testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4"
98 | testImplementation "io.mockk:mockk:$Versions.mockk"
99 | testImplementation "androidx.arch.core:core-testing:2.2.0"
100 |
101 | }
102 |
--------------------------------------------------------------------------------
/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
22 |
--------------------------------------------------------------------------------
/app/src/development/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Base Kotlin Development
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
16 |
19 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/MyApp.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid
2 |
3 | import android.app.Application
4 | import com.github.ajalt.timberkt.Timber
5 | import dagger.hilt.android.HiltAndroidApp
6 |
7 | @HiltAndroidApp
8 | class MyApp : Application() {
9 | override fun onCreate() {
10 | super.onCreate()
11 |
12 | // plant timber
13 | if (BuildConfig.DEBUG) {
14 | Timber.plant(Timber.DebugTree())
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/data/datasource/local/NewsLocalDatasource.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.data.datasource.local
2 |
3 | import com.pratama.baseandroid.domain.entity.News
4 |
5 | interface NewsLocalDatasource {
6 | suspend fun insertNews(news: List)
7 | suspend fun getAllNews(): List
8 | }
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/data/datasource/local/NewsLocalDatasourceImpl.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.data.datasource.local
2 |
3 | import com.github.ajalt.timberkt.d
4 | import com.pratama.baseandroid.data.datasource.local.db.AppDatabase
5 | import com.pratama.baseandroid.data.datasource.local.entity.toNews
6 | import com.pratama.baseandroid.data.datasource.local.entity.toNewsEntity
7 | import com.pratama.baseandroid.domain.entity.News
8 |
9 | class NewsLocalDatasourceImpl(private val appDatabase: AppDatabase) : NewsLocalDatasource {
10 |
11 | override suspend fun insertNews(news: List) {
12 | news.map {
13 | d { "insert to local data ${it.title}" }
14 | appDatabase.newsDao().insert(it.toNewsEntity())
15 | }
16 | }
17 |
18 | override suspend fun getAllNews(): List {
19 | val localNews = appDatabase.newsDao().getAllNews()
20 | d { "local news size ${localNews.size}" }
21 | val listNews = mutableListOf()
22 | localNews.map {
23 | listNews.add(it.toNews())
24 | }
25 | return listNews
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/data/datasource/local/dao/NewsDao.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.data.datasource.local.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.Query
6 | import com.pratama.baseandroid.data.datasource.local.entity.NewsEntity
7 |
8 | @Dao
9 | interface NewsDao {
10 | @Query("SELECT * FROM News")
11 | suspend fun getAllNews(): List
12 |
13 | @Insert
14 | suspend fun insert(news: NewsEntity)
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/data/datasource/local/db/AppDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.data.datasource.local.db
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import com.pratama.baseandroid.data.datasource.local.dao.NewsDao
6 | import com.pratama.baseandroid.data.datasource.local.entity.NewsEntity
7 |
8 | @Database(entities = [NewsEntity::class], version = 1)
9 | abstract class AppDatabase : RoomDatabase() {
10 | abstract fun newsDao(): NewsDao
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/data/datasource/local/entity/NewsEntity.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.data.datasource.local.entity
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 | import com.pratama.baseandroid.domain.entity.News
6 | import com.pratama.baseandroid.domain.entity.NewsSource
7 |
8 | @Entity(tableName = "News")
9 | data class NewsEntity(
10 | val title: String?,
11 | val author: String?,
12 | val description: String?,
13 | val url: String?,
14 | val urlToImage: String?,
15 | val publishedAt: String?,
16 | val source: String?
17 | ) {
18 | @PrimaryKey(autoGenerate = true)
19 | var newsId: Int = 0
20 | }
21 |
22 | fun News.toNewsEntity(): NewsEntity {
23 | return NewsEntity(
24 | title = this.title,
25 | author = this.author,
26 | description = this.description,
27 | url = this.url,
28 | urlToImage = this.urlToImage,
29 | publishedAt = this.publishedAt,
30 | source = this.source.name
31 | )
32 | }
33 |
34 | fun NewsEntity.toNews(): News {
35 | return News(
36 | source = NewsSource("", this.source ?: ""),
37 | author = this.author ?: "",
38 | title = this.title ?: "",
39 | description = this.description ?: "",
40 | urlToImage = this.urlToImage ?: "",
41 | url = this.url ?: "",
42 | publishedAt = this.publishedAt ?: ""
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/data/datasource/remote/NewsRemoteDatasource.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.data.datasource.remote
2 |
3 | import com.pratama.baseandroid.domain.entity.News
4 |
5 | interface NewsRemoteDatasource {
6 | suspend fun getTopHeadlines(category: String, country: String): List
7 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/data/datasource/remote/NewsRemoteDatasourceImpl.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.data.datasource.remote
2 |
3 | import com.pratama.baseandroid.data.datasource.remote.model.toNewsList
4 | import com.pratama.baseandroid.data.datasource.remote.service.NewsApiServices
5 | import com.pratama.baseandroid.domain.entity.News
6 | import javax.inject.Inject
7 |
8 | class NewsRemoteDatasourceImpl @Inject constructor(private val services: NewsApiServices) :
9 | NewsRemoteDatasource {
10 |
11 | override suspend fun getTopHeadlines(category: String, country: String): List {
12 | return services.getTopHeadlines(country, category).toNewsList()
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/data/datasource/remote/interceptor/HeaderInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.data.datasource.remote.interceptor
2 |
3 | import okhttp3.Interceptor
4 | import okhttp3.Response
5 |
6 | class HeaderInterceptor : Interceptor {
7 | override fun intercept(chain: Interceptor.Chain): Response {
8 | val request = chain.request().newBuilder()
9 | .addHeader("X-Api-Key", "4b4df2ea3a154950852b6fda536cfb7f").build()
10 | return chain.proceed(request)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/data/datasource/remote/interceptor/TokenInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.data.datasource.remote.interceptor
2 |
3 | import okhttp3.Interceptor
4 | import okhttp3.Request
5 | import okhttp3.Response
6 |
7 | /**
8 | * https://www.lordcodes.com/articles/authorization-of-web-requests-for-okhttp-and-retrofit
9 | */
10 | class TokenInterceptor : Interceptor {
11 | override fun intercept(chain: Interceptor.Chain): Response {
12 | val newRequest = chain.request().signedRequest()
13 | return chain.proceed(newRequest)
14 | }
15 |
16 | private fun Request.signedRequest(): Request {
17 | // todo: setup repository for fetch access token
18 | // val accessToken = authorizationRepository.fetchFreshAccessToken()
19 | return newBuilder()
20 | .header("Authorization", "Bearer MyToken")
21 | .build()
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/data/datasource/remote/interceptor/TokenRefreshAuthenticator.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.data.datasource.remote.interceptor
2 |
3 | import com.github.ajalt.timberkt.e
4 | import okhttp3.Authenticator
5 | import okhttp3.Request
6 | import okhttp3.Response
7 | import okhttp3.Route
8 | import java.io.IOException
9 |
10 | class TokenRefreshAuthenticator : Authenticator {
11 | override fun authenticate(route: Route?, response: Response): Request? = when {
12 | response.retryCount > 2 -> null
13 | else -> response.createSignedRequest()
14 | }
15 |
16 | private fun Response.createSignedRequest(): Request? = try {
17 | // todo: setup auth repo
18 | // val accessToken = authenticationRepository.fetchFreshAccessToken()
19 | request.signWithToken("my_token")
20 | } catch (error: IOException) {
21 | e { "Failed to resign request" }
22 | null
23 | }
24 |
25 | private fun Request.signWithToken(accessToken: String) =
26 | newBuilder()
27 | .header("Authorization", "Bearer $accessToken")
28 | .build()
29 | }
30 |
31 | private val Response.retryCount: Int
32 | get() {
33 | var currentResponse = priorResponse
34 | var result = 0
35 | while (currentResponse != null) {
36 | result++
37 | currentResponse = currentResponse.priorResponse
38 | }
39 | return result
40 | }
41 |
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/data/datasource/remote/model/NewsResponse.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.data.datasource.remote.model
2 |
3 | import com.pratama.baseandroid.domain.entity.News
4 | import com.pratama.baseandroid.domain.entity.NewsSource
5 |
6 | data class NewsResponse(
7 | val source: SourceResponse,
8 | val author: String? = "",
9 | val title: String? = "",
10 | val description: String? = "",
11 | val url: String? = "",
12 | val urlToImage: String? = "",
13 | val publishedAt: String? = ""
14 | )
15 |
16 | data class SourceResponse(
17 | val id: String? = "",
18 | val name: String? = ""
19 | )
20 |
21 |
22 | fun toNews(newsResponse: NewsResponse): News {
23 | return News(
24 | author = newsResponse.author ?: "",
25 | title = newsResponse.title ?: "",
26 | description = newsResponse.description ?: "",
27 | url = newsResponse.url ?: "",
28 | urlToImage = newsResponse.urlToImage ?: "",
29 | publishedAt = newsResponse.publishedAt ?: "",
30 | source = toNewsSource(newsResponse.source)
31 | )
32 | }
33 |
34 | fun toNewsSource(source: SourceResponse): NewsSource {
35 | return NewsSource(
36 | id = source.id ?: "",
37 | name = source.name ?: ""
38 | )
39 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/data/datasource/remote/model/TopHeadlineResponse.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.data.datasource.remote.model
2 |
3 | import com.pratama.baseandroid.domain.entity.News
4 |
5 | data class TopHeadlineResponse(
6 | val status: String,
7 | val totalResults: Int,
8 | val articles: List
9 | )
10 |
11 | fun TopHeadlineResponse.toNewsList(): List {
12 | val listNews = mutableListOf()
13 | articles.map {
14 | listNews.add(toNews((it)))
15 | }
16 | return listNews
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/data/datasource/remote/service/NewsApiServices.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.data.datasource.remote.service
2 |
3 | import com.pratama.baseandroid.data.datasource.remote.model.TopHeadlineResponse
4 | import retrofit2.http.GET
5 | import retrofit2.http.Query
6 |
7 | interface NewsApiServices {
8 |
9 | @GET("top-headlines")
10 | suspend fun getTopHeadlines(
11 | @Query("country") country: String,
12 | @Query("category") category: String
13 | ): TopHeadlineResponse
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/data/repository/NewsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.data.repository
2 |
3 | import com.pratama.baseandroid.coreandroid.exception.Failure
4 | import com.pratama.baseandroid.coreandroid.functional.Either
5 | import com.pratama.baseandroid.domain.entity.News
6 |
7 | interface NewsRepository {
8 | suspend fun getTopHeadlines(country: String, category: String): Either>
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/data/repository/NewsRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.data.repository
2 |
3 | import com.github.ajalt.timberkt.d
4 | import com.github.ajalt.timberkt.e
5 | import com.pratama.baseandroid.coreandroid.exception.Failure
6 | import com.pratama.baseandroid.coreandroid.functional.Either
7 | import com.pratama.baseandroid.coreandroid.network.NetworkChecker
8 | import com.pratama.baseandroid.data.datasource.local.NewsLocalDatasource
9 | import com.pratama.baseandroid.data.datasource.remote.NewsRemoteDatasource
10 | import com.pratama.baseandroid.data.datasource.remote.model.toNewsList
11 | import com.pratama.baseandroid.domain.entity.News
12 | import com.pratama.baseandroid.utility.ThreadInfoLogger
13 | import java.io.IOException
14 | import java.lang.Exception
15 | import javax.inject.Inject
16 |
17 | class NewsRepositoryImpl @Inject constructor(
18 | private val remote: NewsRemoteDatasource,
19 | private val local: NewsLocalDatasource,
20 | private val networkChecker: NetworkChecker
21 | ) : NewsRepository {
22 |
23 | override suspend fun getTopHeadlines(
24 | country: String,
25 | category: String
26 | ): Either> {
27 | return try {
28 | if (networkChecker.isNetworkConnected()) {
29 | d { "connection : connect to internet" }
30 | // connected to internet
31 | ThreadInfoLogger.logThreadInfo("get top headlines repository")
32 | val response = remote.getTopHeadlines(category = category, country = country)
33 |
34 | local.insertNews(response)
35 |
36 | Either.Right(response)
37 | } else {
38 | d { "connection : disconnect" }
39 | // not connected
40 | val localNews = local.getAllNews()
41 | d { "get data from local: ${localNews.size}" }
42 | if (localNews.isEmpty()) {
43 | Either.Left(Failure.LocalDataNotFound)
44 | } else {
45 | Either.Right(localNews)
46 | }
47 | }
48 | } catch (ex: IOException) {
49 | Either.Left(Failure.ServerError(ex.localizedMessage))
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/di/module/ActivityModule.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.di.module
2 |
3 | import android.content.Context
4 | import com.pratama.baseandroid.coreandroid.network.NetworkChecker
5 | import com.pratama.baseandroid.coreandroid.network.NetworkCheckerImpl
6 | import com.pratama.baseandroid.data.datasource.local.NewsLocalDatasource
7 | import com.pratama.baseandroid.data.datasource.local.NewsLocalDatasourceImpl
8 | import com.pratama.baseandroid.data.datasource.local.db.AppDatabase
9 | import com.pratama.baseandroid.data.datasource.remote.NewsRemoteDatasource
10 | import com.pratama.baseandroid.data.datasource.remote.NewsRemoteDatasourceImpl
11 | import com.pratama.baseandroid.data.datasource.remote.service.NewsApiServices
12 | import com.pratama.baseandroid.data.repository.NewsRepository
13 | import com.pratama.baseandroid.data.repository.NewsRepositoryImpl
14 | import dagger.Module
15 | import dagger.Provides
16 | import dagger.hilt.InstallIn
17 | import dagger.hilt.android.components.ActivityComponent
18 | import dagger.hilt.android.qualifiers.ApplicationContext
19 | import dagger.hilt.android.scopes.ActivityScoped
20 |
21 | @Module
22 | @InstallIn(ActivityComponent::class)
23 | class ActivityModule {
24 |
25 | @Provides
26 | @ActivityScoped
27 | fun provideNewsRemoteDatasource(services: NewsApiServices): NewsRemoteDatasource {
28 | return NewsRemoteDatasourceImpl(services)
29 | }
30 |
31 | @Provides
32 | @ActivityScoped
33 | fun provideNewsLocalDatasource(appDatabase: AppDatabase): NewsLocalDatasource {
34 | return NewsLocalDatasourceImpl(appDatabase)
35 | }
36 |
37 |
38 | @Provides
39 | @ActivityScoped
40 | fun provideNetworkChecker(@ApplicationContext ctx: Context): NetworkChecker {
41 | return NetworkCheckerImpl(ctx)
42 | }
43 |
44 | @Provides
45 | @ActivityScoped
46 | fun provideNewsRepository(
47 | remote: NewsRemoteDatasource,
48 | local: NewsLocalDatasource,
49 | networkCheck: NetworkChecker
50 | ): NewsRepository {
51 | return NewsRepositoryImpl(remote = remote, local = local, networkChecker = networkCheck)
52 | }
53 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/di/module/ApplicationModule.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.di.module
2 |
3 | import android.content.Context
4 | import androidx.room.Room
5 | import com.pratama.baseandroid.BuildConfig
6 | import com.pratama.baseandroid.data.datasource.local.db.AppDatabase
7 | import com.pratama.baseandroid.data.datasource.remote.interceptor.HeaderInterceptor
8 | import com.pratama.baseandroid.data.datasource.remote.service.NewsApiServices
9 | import dagger.Module
10 | import dagger.Provides
11 | import dagger.hilt.InstallIn
12 | import dagger.hilt.android.qualifiers.ApplicationContext
13 | import dagger.hilt.components.SingletonComponent
14 | import okhttp3.OkHttpClient
15 | import okhttp3.logging.HttpLoggingInterceptor
16 | import retrofit2.Retrofit
17 | import retrofit2.converter.gson.GsonConverterFactory
18 | import javax.inject.Singleton
19 |
20 | @Module
21 | @InstallIn(SingletonComponent::class)
22 | class ApplicationModule {
23 |
24 | @Provides
25 | @Singleton
26 | fun provideAppDatabase(@ApplicationContext ctx: Context): AppDatabase {
27 | return Room.databaseBuilder(
28 | ctx,
29 | AppDatabase::class.java, "my_app_database"
30 | ).build()
31 | }
32 |
33 | @Provides
34 | @Singleton
35 | fun provideOkHttpClient(headerInterceptor: HeaderInterceptor) = if (BuildConfig.DEBUG) {
36 | val loggingInterceptor = HttpLoggingInterceptor()
37 | loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
38 | OkHttpClient.Builder()
39 | .addInterceptor(loggingInterceptor)
40 | .addInterceptor(headerInterceptor)
41 | .build()
42 | } else {
43 | OkHttpClient.Builder()
44 | .addInterceptor(headerInterceptor)
45 | .build()
46 | }
47 |
48 | @Provides
49 | @Singleton
50 | fun provideHeaderInterceptor(): HeaderInterceptor {
51 | return HeaderInterceptor()
52 | }
53 |
54 | @Provides
55 | @Singleton
56 | fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit = Retrofit.Builder()
57 | .baseUrl("https://newsapi.org/v2/")
58 | .addConverterFactory(GsonConverterFactory.create())
59 | .client(okHttpClient)
60 | .build()
61 |
62 | @Provides
63 | @Singleton
64 | fun provideNewsApiServices(retrofit: Retrofit): NewsApiServices =
65 | retrofit.create(NewsApiServices::class.java)
66 |
67 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/domain/entity/News.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.domain.entity
2 |
3 | import com.pratama.baseandroid.ui.dto.NewsDto
4 |
5 | data class News(
6 | val source: NewsSource,
7 | val author: String,
8 | val title: String,
9 | val description: String,
10 | val url: String,
11 | val urlToImage: String,
12 | val publishedAt: String
13 | )
14 |
15 |
16 | fun News.toDto(): NewsDto {
17 | return with(this) {
18 | NewsDto(source.toDto(), author, title, description, url, urlToImage, publishedAt)
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/domain/entity/NewsSource.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.domain.entity
2 |
3 | import com.pratama.baseandroid.ui.dto.NewsSourceDto
4 |
5 | data class NewsSource(
6 | val id: String,
7 | val name: String
8 | )
9 |
10 | fun NewsSource.toDto(): NewsSourceDto {
11 | return NewsSourceDto(id, name)
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/domain/usecase/GetTopHeadlineUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.domain.usecase
2 |
3 | import com.pratama.baseandroid.coreandroid.exception.Failure
4 | import com.pratama.baseandroid.coreandroid.functional.Either
5 | import com.pratama.baseandroid.coreandroid.usecase.UseCase
6 | import com.pratama.baseandroid.data.repository.NewsRepository
7 | import com.pratama.baseandroid.domain.entity.News
8 | import com.pratama.baseandroid.utility.ThreadInfoLogger
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.withContext
11 | import javax.inject.Inject
12 |
13 | class GetTopHeadlineUseCase @Inject constructor(private val repository: NewsRepository) :
14 | UseCase, GetTopHeadlineUseCase.TopHeadlineParam>() {
15 |
16 | override suspend fun run(params: TopHeadlineParam): Either> =
17 | withContext(Dispatchers.IO) {
18 | ThreadInfoLogger.logThreadInfo("get top headline usecase")
19 | repository.getTopHeadlines(params.country, params.category)
20 | }
21 |
22 | data class TopHeadlineParam(
23 | val country: String,
24 | val category: String
25 | )
26 |
27 |
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/domain/usecase/GetTopHeadlineUseCaseFlow.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.domain.usecase
2 |
3 | class GetTopHeadlineUseCaseFlow
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/ui/detailpage/DetailNewsFragment.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.ui.detailpage
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.ViewGroup
6 | import androidx.navigation.fragment.navArgs
7 | import com.pratama.baseandroid.coreandroid.base.BaseFragmentBinding
8 | import com.pratama.baseandroid.databinding.FragmentDetailNewsBinding
9 | import com.pratama.baseandroid.ui.dto.NewsDto
10 |
11 | class DetailNewsFragment : BaseFragmentBinding() {
12 |
13 | private val args: DetailNewsFragmentArgs by navArgs()
14 |
15 | private var newsDto: NewsDto? = null
16 |
17 | override fun onCreate(savedInstanceState: Bundle?) {
18 | super.onCreate(savedInstanceState)
19 |
20 | newsDto = args.newsDto
21 | }
22 |
23 | override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentDetailNewsBinding
24 | get() = FragmentDetailNewsBinding::inflate
25 |
26 | override fun setupView(binding: FragmentDetailNewsBinding) {
27 | with(binding) {
28 | newsDto?.let {
29 | newsTitle.text = it.title
30 | }
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/ui/dto/NewsDto.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.ui.dto
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 |
6 | @Parcelize
7 | data class NewsDto(
8 | val source: NewsSourceDto,
9 | val author: String,
10 | val title: String,
11 | val description: String,
12 | val url: String,
13 | val urlToImage: String,
14 | val publishedAt: String
15 | ) : Parcelable
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/ui/dto/NewsSourceDto.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.ui.dto
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 |
6 | @Parcelize
7 | data class NewsSourceDto(
8 | val id: String,
9 | val name: String
10 | ) : Parcelable
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/ui/homepage/HomePageActivity.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.ui.homepage
2 |
3 | import android.util.Log
4 | import android.view.LayoutInflater
5 | import com.github.ajalt.timberkt.d
6 | import com.pratama.baseandroid.coreandroid.base.BaseActivityBinding
7 | import com.pratama.baseandroid.databinding.ActivityHomeBinding
8 | import dagger.hilt.android.AndroidEntryPoint
9 |
10 | @AndroidEntryPoint
11 | class HomePageActivity : BaseActivityBinding() {
12 |
13 | override val bindingInflater: (LayoutInflater) -> ActivityHomeBinding
14 | get() = ActivityHomeBinding::inflate
15 |
16 | override fun setupView(binding: ActivityHomeBinding) {
17 | d { "setup view" }
18 | }
19 |
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/ui/homepage/HomePageModule.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.ui.homepage
2 |
3 | import com.pratama.baseandroid.data.repository.NewsRepository
4 | import com.pratama.baseandroid.domain.usecase.GetTopHeadlineUseCase
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.android.components.ActivityComponent
9 |
10 | @Module
11 | @InstallIn(ActivityComponent::class)
12 | class HomePageModule {
13 |
14 | @Provides
15 | fun provideTopHeadLineUseCase(repository: NewsRepository): GetTopHeadlineUseCase {
16 | return GetTopHeadlineUseCase(repository)
17 | }
18 |
19 | @Provides
20 | fun provideListNewsViewModel(useCase: GetTopHeadlineUseCase): ListNewsViewModel {
21 | return ListNewsViewModel(useCase)
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/ui/homepage/ListNewsFragment.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.ui.homepage
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.navigation.fragment.findNavController
6 | import androidx.recyclerview.widget.LinearLayoutManager
7 | import com.github.ajalt.timberkt.d
8 | import com.pratama.baseandroid.coreandroid.base.BaseFragmentBinding
9 | import com.pratama.baseandroid.coreandroid.extensions.toGone
10 | import com.pratama.baseandroid.coreandroid.extensions.toVisible
11 | import com.pratama.baseandroid.databinding.FragmentListNewsBinding
12 | import com.pratama.baseandroid.domain.entity.News
13 | import com.pratama.baseandroid.domain.entity.toDto
14 | import com.pratama.baseandroid.ui.homepage.rvitem.NewsItem
15 | import com.pratama.baseandroid.utility.ThreadInfoLogger
16 | import com.xwray.groupie.GroupAdapter
17 | import com.xwray.groupie.GroupieViewHolder
18 | import dagger.hilt.android.AndroidEntryPoint
19 | import javax.inject.Inject
20 |
21 | @AndroidEntryPoint
22 | class ListNewsFragment : BaseFragmentBinding(), NewsItem.NewsListener {
23 |
24 | @Inject
25 | lateinit var listNewsViewModel: ListNewsViewModel
26 |
27 | private val listNewsAdapter = GroupAdapter()
28 |
29 | override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentListNewsBinding =
30 | FragmentListNewsBinding::inflate
31 |
32 | override fun setupView(binding: FragmentListNewsBinding) = with(binding) {
33 | // setupRecyclerview
34 | rvListNews.layoutManager = LinearLayoutManager(requireActivity())
35 | rvListNews.adapter = listNewsAdapter
36 |
37 | setListener(binding)
38 |
39 | callData()
40 |
41 | listNewsViewModel.uiState().observe(viewLifecycleOwner, { state ->
42 | when (state) {
43 |
44 | is ListNewsViewModel.ListNewsState.Loading -> {
45 | loadingIndicator.toVisible()
46 | }
47 |
48 | is ListNewsViewModel.ListNewsState.NewsLoaded -> {
49 | loadingIndicator.toGone()
50 | swipeRefreshLayout.isRefreshing = false
51 |
52 | ThreadInfoLogger.logThreadInfo("show news viewmodel")
53 |
54 | state.news.map {
55 | d { "news loaded -> ${it.title}" }
56 | listNewsAdapter.add(NewsItem(it, this@ListNewsFragment))
57 | }
58 | }
59 | else ->{
60 |
61 | }
62 | }
63 | })
64 | }
65 |
66 | override fun onNewsSelected(news: News) {
67 | findNavController().navigate(
68 | ListNewsFragmentDirections.actionListNewsFragmentToDetailNewsFragment(
69 | news.toDto()
70 | )
71 | )
72 | }
73 |
74 | private fun setListener(binding: FragmentListNewsBinding) {
75 | binding.swipeRefreshLayout.setOnRefreshListener {
76 | listNewsAdapter.clear()
77 |
78 | callData()
79 | }
80 | }
81 |
82 | private fun callData() {
83 | listNewsViewModel.getTopHeadlinesByCountry(country = "us", category = "technology")
84 | }
85 |
86 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/ui/homepage/ListNewsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.ui.homepage
2 |
3 | import androidx.lifecycle.viewModelScope
4 | import com.pratama.baseandroid.coreandroid.BaseViewModel
5 | import com.pratama.baseandroid.coreandroid.exception.Failure
6 | import com.pratama.baseandroid.domain.entity.News
7 | import com.pratama.baseandroid.domain.usecase.GetTopHeadlineUseCase
8 | import com.pratama.baseandroid.utility.ThreadInfoLogger
9 | import kotlinx.coroutines.launch
10 | import javax.inject.Inject
11 |
12 | class ListNewsViewModel @Inject constructor(private val getTopHeadlineUseCase: GetTopHeadlineUseCase) :
13 | BaseViewModel() {
14 |
15 | sealed class ListNewsState {
16 | object Loading : ListNewsState()
17 | data class NewsLoaded(val news: List) : ListNewsState()
18 | data class Error(val message: String) : ListNewsState()
19 | }
20 |
21 | fun getTopHeadlinesByCountry(country: String, category: String) {
22 | viewModelScope.launch {
23 | uiState.postValue(ListNewsState.Loading)
24 |
25 | ThreadInfoLogger.logThreadInfo("get top headlines viewmodel")
26 | val result =
27 | getTopHeadlineUseCase.run(GetTopHeadlineUseCase.TopHeadlineParam(country, category))
28 |
29 | result.fold({ failure ->
30 | when (failure) {
31 | is Failure.ServerError -> {
32 | uiState.postValue(ListNewsState.Error("Server Error"))
33 | }
34 | else -> uiState.postValue(ListNewsState.Error("Unknown Error"))
35 | }
36 |
37 | }, { result ->
38 | if (!result.isNullOrEmpty()) {
39 | uiState.postValue(ListNewsState.NewsLoaded(result))
40 | }
41 | })
42 | }
43 | }
44 |
45 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/ui/homepage/rvitem/NewsItem.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.ui.homepage.rvitem
2 |
3 | import android.view.View
4 | import com.pratama.baseandroid.R
5 | import com.pratama.baseandroid.coreandroid.extensions.loadFromUrl
6 | import com.pratama.baseandroid.databinding.RvItemNewsBinding
7 | import com.pratama.baseandroid.domain.entity.News
8 | import com.xwray.groupie.viewbinding.BindableItem
9 |
10 | class NewsItem(private val news: News, val listener: NewsItem.NewsListener) :
11 | BindableItem() {
12 |
13 | interface NewsListener {
14 | fun onNewsSelected(news: News)
15 | }
16 |
17 | override fun bind(viewBinding: RvItemNewsBinding, position: Int) = with(viewBinding) {
18 | newsTitle.text = news.title
19 | newsThumbnail.loadFromUrl(news.urlToImage)
20 | newsSource.text = "Source ${news.source.name}"
21 |
22 | this.root.setOnClickListener { listener.onNewsSelected(news) }
23 | }
24 |
25 | override fun getLayout(): Int = R.layout.rv_item_news
26 |
27 | override fun initializeViewBinding(view: View): RvItemNewsBinding = RvItemNewsBinding.bind(view)
28 |
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/ui/splash/SplashActivity.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.ui.splash
2 |
3 | import android.content.Intent
4 | import androidx.appcompat.app.AppCompatActivity
5 | import android.os.Bundle
6 | import com.pratama.baseandroid.R
7 | import com.pratama.baseandroid.ui.homepage.HomePageActivity
8 |
9 | class SplashActivity : AppCompatActivity() {
10 | override fun onCreate(savedInstanceState: Bundle?) {
11 | super.onCreate(savedInstanceState)
12 | setContentView(R.layout.activity_splash)
13 |
14 | startActivity(Intent(this, HomePageActivity::class.java))
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/pratama/baseandroid/utility/ThreadInfoLogger.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.utility
2 |
3 | import com.github.ajalt.timberkt.Timber
4 |
5 | object ThreadInfoLogger {
6 | fun logThreadInfo(message: String) {
7 | Timber.d { "logthread : $message : Thread name : ${Thread.currentThread().name}" }
8 | }
9 | }
--------------------------------------------------------------------------------
/app/src/main/res/anim/enter_from_left.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/enter_from_right.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/exit_to_left.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/exit_to_right.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/splashscreen.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | -
9 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/font/architects_daughter.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_home.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_home_page.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_splash.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 | tools:context=".ui.splash.SplashActivity">
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_detail_news.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_list_news.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
13 |
16 |
17 |
23 |
24 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_item_article.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
18 |
19 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_item_loadmore_loading.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/rv_item_news.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
12 |
20 |
21 |
31 |
--------------------------------------------------------------------------------
/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/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pratamawijaya/BaseKotlinAndroid/ab535de8cd6aa9c468d69d3bebc54f6bd77fe250/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pratamawijaya/BaseKotlinAndroid/ab535de8cd6aa9c468d69d3bebc54f6bd77fe250/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pratamawijaya/BaseKotlinAndroid/ab535de8cd6aa9c468d69d3bebc54f6bd77fe250/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pratamawijaya/BaseKotlinAndroid/ab535de8cd6aa9c468d69d3bebc54f6bd77fe250/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pratamawijaya/BaseKotlinAndroid/ab535de8cd6aa9c468d69d3bebc54f6bd77fe250/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pratamawijaya/BaseKotlinAndroid/ab535de8cd6aa9c468d69d3bebc54f6bd77fe250/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pratamawijaya/BaseKotlinAndroid/ab535de8cd6aa9c468d69d3bebc54f6bd77fe250/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pratamawijaya/BaseKotlinAndroid/ab535de8cd6aa9c468d69d3bebc54f6bd77fe250/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pratamawijaya/BaseKotlinAndroid/ab535de8cd6aa9c468d69d3bebc54f6bd77fe250/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pratamawijaya/BaseKotlinAndroid/ab535de8cd6aa9c468d69d3bebc54f6bd77fe250/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/navigation/home_nav_graph.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
16 |
17 |
22 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #6200EE
4 | #3700B3
5 | #03DAC5
6 | #FFFFFF
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/font_certs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - @array/com_google_android_gms_fonts_certs_dev
5 | - @array/com_google_android_gms_fonts_certs_prod
6 |
7 |
8 | -
9 | MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
10 |
11 |
12 |
13 | -
14 | MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/values/preloaded_fonts.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - @font/architects_daughter
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Base Android
3 |
4 | Hello blank fragment
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/test/java/com/pratama/baseandroid/data/datasource/local/NewsLocalDatasourceImplTest.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.data.datasource.local
2 |
3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4 | import com.pratama.baseandroid.data.datasource.local.dao.NewsDao
5 | import com.pratama.baseandroid.data.datasource.local.dao.NewsDao_Impl
6 | import com.pratama.baseandroid.data.datasource.local.db.AppDatabase
7 | import com.pratama.baseandroid.data.datasource.local.entity.NewsEntity
8 | import com.pratama.baseandroid.data.datasource.local.entity.toNewsEntity
9 | import com.pratama.baseandroid.domain.entity.News
10 | import com.pratama.baseandroid.domain.entity.NewsSource
11 | import io.mockk.MockKAnnotations
12 | import io.mockk.coEvery
13 | import io.mockk.coVerify
14 | import io.mockk.every
15 | import io.mockk.impl.annotations.MockK
16 | import kotlinx.coroutines.test.runBlockingTest
17 | import org.junit.Assert.*
18 | import org.junit.Before
19 | import org.junit.Rule
20 | import org.junit.Test
21 | import org.junit.runner.RunWith
22 | import org.junit.runners.JUnit4
23 |
24 | @RunWith(JUnit4::class)
25 | class NewsLocalDatasourceImplTest {
26 |
27 | @Rule
28 | @JvmField
29 | val instantExecutorRule = InstantTaskExecutorRule()
30 |
31 | @MockK
32 | lateinit var appDatabase: AppDatabase
33 |
34 | @MockK
35 | lateinit var newsDao: NewsDao
36 |
37 | lateinit var newsLocalDatasource: NewsLocalDatasource
38 |
39 | @Before
40 | fun setUp() {
41 | MockKAnnotations.init(this)
42 | newsLocalDatasource = NewsLocalDatasourceImpl(appDatabase)
43 | }
44 |
45 | @Test
46 | fun `test insertNews`() = runBlockingTest {
47 | // given
48 | val news = generateFakeNews()
49 |
50 | coEvery { appDatabase.newsDao().insert(news[0].toNewsEntity()) } returns Unit
51 |
52 | newsLocalDatasource.insertNews(news)
53 |
54 | coVerify { appDatabase.newsDao().insert(news[0].toNewsEntity()) }
55 |
56 | }
57 |
58 | @Test
59 | fun `test getAllNews`() = runBlockingTest {
60 | coEvery { appDatabase.newsDao().getAllNews() } returns generateFakeNewsEntity()
61 |
62 | val result = newsLocalDatasource.getAllNews()
63 |
64 | assertEquals(result.size, 1)
65 | }
66 |
67 | private fun generateFakeNewsEntity(): List {
68 | return listOf(
69 | NewsEntity(
70 | title = "title",
71 | author = "author",
72 | description = "desc",
73 | urlToImage = "url",
74 | url = "url",
75 | publishedAt = "",
76 | source = "source"
77 | )
78 | )
79 |
80 | }
81 |
82 | private fun generateFakeNews(): List {
83 | return listOf(
84 | News(
85 | source = NewsSource(id = "id", name = ""),
86 | author = "author",
87 | title = "title", description = "", url = "url", urlToImage = "", publishedAt = ""
88 | )
89 | )
90 | }
91 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/pratama/baseandroid/data/datasource/remote/NewsRemoteDatasourceImplTest.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.data.datasource.remote
2 |
3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4 | import com.pratama.baseandroid.data.datasource.remote.model.NewsResponse
5 | import com.pratama.baseandroid.data.datasource.remote.model.SourceResponse
6 | import com.pratama.baseandroid.data.datasource.remote.model.TopHeadlineResponse
7 | import com.pratama.baseandroid.data.datasource.remote.service.NewsApiServices
8 | import io.mockk.MockKAnnotations
9 | import io.mockk.coEvery
10 | import io.mockk.coVerify
11 | import io.mockk.impl.annotations.MockK
12 | import kotlinx.coroutines.test.runBlockingTest
13 | import org.junit.Assert.assertEquals
14 | import org.junit.Before
15 | import org.junit.Rule
16 | import org.junit.Test
17 | import org.junit.runner.RunWith
18 | import org.junit.runners.JUnit4
19 |
20 | @RunWith(JUnit4::class)
21 | class NewsRemoteDatasourceImplTest {
22 |
23 | @Rule
24 | @JvmField
25 | val instantExecutorRule = InstantTaskExecutorRule()
26 |
27 | lateinit var newsRemoteDatasourceImpl: NewsRemoteDatasourceImpl
28 |
29 | @MockK
30 | lateinit var service: NewsApiServices
31 |
32 | @Before
33 | fun setUp() {
34 | MockKAnnotations.init(this)
35 | newsRemoteDatasourceImpl = NewsRemoteDatasourceImpl(service)
36 | }
37 |
38 | @Test
39 | fun `test getTopHeadlines should return list news`() = runBlockingTest {
40 | // given
41 | val category = "technology"
42 | val country = "us"
43 |
44 | coEvery { service.getTopHeadlines(country, category) } returns generateFakeNews()
45 |
46 | // when
47 | val result = newsRemoteDatasourceImpl.getTopHeadlines(category, country)
48 |
49 | coVerify { service.getTopHeadlines(category = category, country = country) }
50 |
51 | assertEquals(1, result.size)
52 | }
53 |
54 | private fun generateFakeNews(): TopHeadlineResponse {
55 | return TopHeadlineResponse(
56 | status = "success",
57 | totalResults = 1,
58 | articles = listOf(
59 | NewsResponse(
60 | source = SourceResponse(id = "1", name = "ok")
61 | )
62 | )
63 | )
64 | }
65 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/pratama/baseandroid/data/repository/NewsRepositoryImplTest.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.data.repository
2 |
3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4 | import com.pratama.baseandroid.coreandroid.functional.Either
5 | import com.pratama.baseandroid.coreandroid.network.NetworkChecker
6 | import com.pratama.baseandroid.data.datasource.local.NewsLocalDatasource
7 | import com.pratama.baseandroid.data.datasource.remote.NewsRemoteDatasource
8 | import com.pratama.baseandroid.domain.entity.News
9 | import com.pratama.baseandroid.domain.entity.NewsSource
10 | import io.mockk.*
11 | import io.mockk.impl.annotations.MockK
12 | import io.mockk.impl.annotations.SpyK
13 | import kotlinx.coroutines.test.runBlockingTest
14 | import org.junit.Assert.*
15 | import org.junit.Before
16 | import org.junit.Rule
17 | import org.junit.Test
18 | import org.junit.runner.RunWith
19 | import org.junit.runners.JUnit4
20 | import java.io.IOException
21 |
22 | @RunWith(JUnit4::class)
23 | class NewsRepositoryImplTest {
24 |
25 | @Rule
26 | @JvmField
27 | val instantExecutorRule = InstantTaskExecutorRule()
28 |
29 | lateinit var newsRepository: NewsRepository
30 |
31 | @MockK
32 | lateinit var newsRemoteDatasource: NewsRemoteDatasource
33 |
34 | @MockK
35 | lateinit var newsLocalDatasource: NewsLocalDatasource
36 |
37 | @MockK
38 | lateinit var networkChecker: NetworkChecker
39 |
40 | @Before
41 | fun setUp() {
42 | MockKAnnotations.init(this)
43 | newsRepository =
44 | NewsRepositoryImpl(newsRemoteDatasource, newsLocalDatasource, networkChecker)
45 | }
46 |
47 | @Test
48 | fun `test getTopHeadlines throw exceptions`() = runBlockingTest {
49 | every { networkChecker.isNetworkConnected() } throws IOException()
50 |
51 | val result = newsRepository.getTopHeadlines(country = "us", category = "technology")
52 |
53 | assertTrue(result.isLeft)
54 | }
55 |
56 | @Test
57 | fun `test getTopHeadlines network is connected should return data`() = runBlockingTest {
58 | // given
59 | val country = "us"
60 | val category = "technology"
61 |
62 | // network down
63 | every { networkChecker.isNetworkConnected() } returns true
64 | coEvery { newsLocalDatasource.insertNews(generateFakeData()) } returns Unit
65 |
66 | coEvery {
67 | newsRemoteDatasource.getTopHeadlines(
68 | category,
69 | country
70 | )
71 | } returns generateFakeData()
72 |
73 | val result = newsRepository.getTopHeadlines(country, category)
74 |
75 | coVerify { newsLocalDatasource.insertNews(generateFakeData()) }
76 |
77 | assertTrue(result.isRight)
78 | }
79 |
80 | @Test
81 | fun `test getTopHeadlines network is down should return local data`() = runBlockingTest {
82 | // given
83 | val country = "us"
84 | val category = "technology"
85 |
86 | // network down
87 | every { networkChecker.isNetworkConnected() } returns false
88 |
89 | // local return data
90 | coEvery { newsLocalDatasource.getAllNews() } returns generateFakeData()
91 |
92 | // when
93 | val result = newsRepository.getTopHeadlines(country, category)
94 | // then
95 | assertTrue(result.isRight)
96 | }
97 |
98 | private fun generateFakeData(): List {
99 | return listOf(
100 | News(
101 | source = NewsSource(id = "anu", name = "title"),
102 | author = "",
103 | title = "title",
104 | description = "",
105 | url = "",
106 | urlToImage = "",
107 | publishedAt = ""
108 | )
109 | )
110 | }
111 |
112 | @Test
113 | fun `test getTopHeadlines network is down and local data null should return failure`() =
114 | runBlockingTest {
115 | // given
116 | val country = "us"
117 | val category = "technology"
118 |
119 | // network down
120 | every { networkChecker.isNetworkConnected() } returns false
121 | coEvery { newsLocalDatasource.getAllNews() } returns emptyList()
122 |
123 | // when
124 | val result = newsRepository.getTopHeadlines(country, category)
125 | // then
126 | assertTrue(result.isLeft)
127 | }
128 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/pratama/baseandroid/domain/usecase/GetTopHeadlineUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.domain.usecase
2 |
3 | import com.pratama.baseandroid.coreandroid.exception.Failure
4 | import com.pratama.baseandroid.coreandroid.functional.Either
5 | import com.pratama.baseandroid.data.repository.NewsRepository
6 | import com.pratama.baseandroid.domain.entity.News
7 | import com.pratama.baseandroid.domain.entity.NewsSource
8 | import io.mockk.MockKAnnotations
9 | import io.mockk.coEvery
10 | import io.mockk.impl.annotations.MockK
11 | import kotlinx.coroutines.ExperimentalCoroutinesApi
12 | import kotlinx.coroutines.launch
13 | import kotlinx.coroutines.test.TestCoroutineDispatcher
14 | import kotlinx.coroutines.test.TestCoroutineScope
15 | import org.junit.Assert.assertTrue
16 | import org.junit.Before
17 | import org.junit.Test
18 | import org.junit.runner.RunWith
19 | import org.junit.runners.JUnit4
20 |
21 | @RunWith(JUnit4::class)
22 | class GetTopHeadlineUseCaseTest {
23 |
24 | @ExperimentalCoroutinesApi
25 | private val testDispatcher = TestCoroutineDispatcher()
26 |
27 | @ExperimentalCoroutinesApi
28 | private val testScope = TestCoroutineScope(testDispatcher)
29 |
30 | @MockK
31 | lateinit var newsRepo: NewsRepository
32 |
33 | lateinit var useCase: GetTopHeadlineUseCase
34 |
35 | @Before
36 | fun setUp() {
37 | MockKAnnotations.init(this)
38 | useCase = GetTopHeadlineUseCase(newsRepo)
39 | }
40 |
41 | @Test
42 | fun `test usecaseRun return failure`() {
43 | // given
44 | coEvery {
45 | newsRepo.getTopHeadlines(
46 | country = "us",
47 | category = "tech"
48 | )
49 | } returns generateFailure()
50 |
51 | val params = GetTopHeadlineUseCase.TopHeadlineParam(country = "us", category = "tech")
52 |
53 | testScope.launch {
54 | val result = useCase.run(params)
55 | assertTrue(result.isLeft)
56 | }
57 | }
58 |
59 | @Test
60 | fun `test usecaseRun return value`() {
61 | // given
62 | coEvery {
63 | newsRepo.getTopHeadlines(
64 | country = "us",
65 | category = "tech"
66 | )
67 | } returns generateFakeNews()
68 |
69 | val params = GetTopHeadlineUseCase.TopHeadlineParam(country = "us", category = "tech")
70 |
71 | testScope.launch {
72 | val result = useCase.run(params)
73 | assertTrue(result.isRight)
74 | }
75 | }
76 |
77 | private fun generateFailure(): Either> {
78 | return Either.Left(Failure.ServerError("error"))
79 | }
80 |
81 | private fun generateFakeNews(): Either> {
82 | return Either.Right(
83 | listOf(
84 | News(
85 | source = NewsSource(id = "1", name = "title"),
86 | author = "",
87 | title = "",
88 | description = "",
89 | urlToImage = "",
90 | url = "",
91 | publishedAt = ""
92 | )
93 | )
94 | )
95 | }
96 | }
--------------------------------------------------------------------------------
/app/version.properties:
--------------------------------------------------------------------------------
1 | #Fri Jul 26 10:34:44 WIB 2019
2 | AI_VERSION_CODE=2
3 |
--------------------------------------------------------------------------------
/build-system/dependencies.gradle:
--------------------------------------------------------------------------------
1 | ext {
2 | minSDK = 21
3 | targetSDK = 33
4 |
5 | gradle_tools = "7.2.2"
6 | kotlinVersion = "1.7.21"
7 | hiltVersion = "2.42"
8 | android_navigation = "2.3.2"
9 | coreKtxVersion = "1.9.0"
10 | appCompatVersion ="1.6.1"
11 |
12 |
13 | supportLibraries = [
14 | kotlin_stdlib : "org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}",
15 | core_ktx : "androidx.core:core-ktx:${coreKtxVersion}",
16 | appCompat: "androidx.appcompat:appcompat:${appCompatVersion}"
17 | ]
18 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | apply from: "build-system/dependencies.gradle"
5 | repositories {
6 | google()
7 | jcenter()
8 | }
9 | dependencies {
10 | classpath "com.android.tools.build:gradle:$gradle_tools"
11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
12 | classpath "com.google.dagger:hilt-android-gradle-plugin:$hiltVersion"
13 | classpath 'com.github.ben-manes:gradle-versions-plugin:+'
14 | classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$android_navigation"
15 | classpath "com.vanniktech:gradle-android-junit-jacoco-plugin:0.16.0"
16 | }
17 | }
18 |
19 | plugins {
20 | id("io.gitlab.arturbosch.detekt").version("1.15.0")
21 | id("io.github.cdsap.talaiot.plugin.base") version "1.5.1"
22 | id("com.osacky.doctor").version("0.8.1")
23 | id("com.autonomousapps.dependency-analysis") version "1.19.0"
24 | // id("com.dropbox.affectedmoduledetector").version("0.2.1")
25 | }
26 | apply plugin: "com.vanniktech.android.junit.jacoco"
27 |
28 | talaiot {
29 | publishers {
30 | jsonPublisher = true
31 | }
32 | }
33 |
34 | allprojects {
35 |
36 | apply from: "$rootDir/config/detekt.gradle"
37 | apply from: "$rootDir/config/jacoco.gradle"
38 |
39 | repositories {
40 | google()
41 | jcenter()
42 | }
43 | }
44 |
45 | task clean(type: Delete) {
46 | delete rootProject.buildDir
47 | }
48 |
49 |
50 |
--------------------------------------------------------------------------------
/buildSrc/.gitignore:
--------------------------------------------------------------------------------
1 | build
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `kotlin-dsl`
3 | }
4 |
5 | repositories {
6 | jcenter()
7 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/Dependencies.kt:
--------------------------------------------------------------------------------
1 | object AndroidConfig {
2 | const val compileSdkVersion = 30
3 | // const val buildToolsVersion = "30.0.3"
4 | const val minSdkVersion = 21
5 | const val targetSdkVersion = 29
6 | }
7 |
8 | object Versions {
9 | const val kotlin = "1.7.21"
10 | const val gradle = "7.2.2"
11 | const val junit = "4.12"
12 | const val core_ktx = "1.9.0"
13 | const val androidx_appcompat = "1.6.1"
14 | const val androidx_constraintlayout = "2.1.4"
15 | const val routing_navigator = "1.0.0"
16 | const val coroutines = "1.6.4"
17 | const val retrofit = "2.9.0"
18 | const val retrofit_moshi = "2.6.2"
19 | const val logging_interceptor = "4.8.0"
20 | const val hilt = "2.42"
21 | const val groupie = "2.9.0"
22 | const val room = "2.2.5"
23 | const val android_lifecycle = "2.5.1"
24 | const val timber = "1.5.1"
25 | const val android_navigation = "2.3.2"
26 | const val detekt = "1.15.0"
27 | const val mockk = "1.10.5"
28 | }
29 |
30 |
31 | object AndroidLib {
32 | val kotlin_stdlib = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}"
33 | val androidx_core = "androidx.core:core-ktx:${Versions.core_ktx}"
34 | val androidx_appcompat = "androidx.appcompat:appcompat:${Versions.androidx_appcompat}"
35 | val androidx_constraintlayout =
36 | "androidx.constraintlayout:constraintlayout:${Versions.androidx_constraintlayout}"
37 | val routing_navigator = "com.github.florent37:navigator:${Versions.routing_navigator}"
38 | val coroutines_core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}"
39 | val coroutines_android =
40 | "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines}"
41 | val retrofit_android = "com.squareup.retrofit2:retrofit:${Versions.retrofit}"
42 | val moshi_converter = "com.squareup.retrofit2:converter-moshi:${Versions.retrofit_moshi}"
43 | const val gson_converter = "com.squareup.retrofit2:converter-gson:${Versions.retrofit}"
44 | val okhttp_logging = "com.squareup.okhttp3:logging-interceptor:${Versions.logging_interceptor}"
45 | val hilt = "com.google.dagger:hilt-android:${Versions.hilt}"
46 | val hilt_processor_compiler = "com.google.dagger:hilt-android-compiler:${Versions.hilt}"
47 | val groupie = "com.xwray:groupie:${Versions.groupie}"
48 | val groupie_viewbinding = "com.xwray:groupie-viewbinding:${Versions.groupie}"
49 | val room = "androidx.room:room-runtime:${Versions.room}"
50 | val room_compiler = "androidx.room:room-compiler:${Versions.room}"
51 | val room_coroutine = "androidx.room:room-ktx:${Versions.room}"
52 | const val viewmodel_ktx =
53 | "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.android_lifecycle}"
54 | const val viewmodel_runtime =
55 | "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.android_lifecycle}"
56 | const val viewmodel_extension = "androidx.lifecycle:lifecycle-extensions:2.2.0"
57 | const val viewmodel_compiler =
58 | "androidx.lifecycle:lifecycle-compiler:${Versions.android_lifecycle}"
59 | const val timber = "com.github.ajalt:timberkt:1.5.1"
60 | }
61 |
62 | object AndroidTestLib {
63 | val junit_lib = "junit:junit:${Versions.junit}"
64 | val android_test_junit = "androidx.test.ext:junit:1.1.1"
65 | val android_test_espresso_core = "androidx.test.espresso:espresso-core:3.2.0"
66 | }
67 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | codecov:
2 | token: 9b83d005-a653-407b-a348-2d5610e1b364
--------------------------------------------------------------------------------
/config/detekt.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'io.gitlab.arturbosch.detekt'
2 |
3 | detekt {
4 | failFast = true
5 | buildUponDefaultConfig = true // preconfigure defaults
6 | config = files("$rootDir/config/detekt_config.yml")
7 |
8 | reports {
9 | html.enabled = true // observe findings in your browser with structure and code snippets
10 | xml.enabled = true // checkstyle like format mainly for integrations like Jenkins
11 | txt.enabled = true
12 | // similar to the console output, contains issue signature to manually edit baseline files
13 | }
14 | }
15 |
16 | // Groovy dsl
17 | tasks.detekt.jvmTarget = "1.8"
18 |
19 |
--------------------------------------------------------------------------------
/config/detekt_config.yml:
--------------------------------------------------------------------------------
1 | build:
2 | maxIssues: 10
3 | excludeCorrectable: false
4 | weights:
5 | # complexity: 2
6 | # LongParameterList: 1
7 | # style: 1
8 | # comments: 1
9 |
10 | config:
11 | validation: true
12 | # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
13 | excludes: ''
14 |
15 | processors:
16 | active: true
17 | exclude:
18 | - 'DetektProgressListener'
19 | # - 'FunctionCountProcessor'
20 | # - 'PropertyCountProcessor'
21 | # - 'ClassCountProcessor'
22 | # - 'PackageCountProcessor'
23 | # - 'KtFileCountProcessor'
24 |
25 | console-reports:
26 | active: true
27 | exclude:
28 | - 'ProjectStatisticsReport'
29 | - 'ComplexityReport'
30 | - 'NotificationReport'
31 | # - 'FindingsReport'
32 | - 'FileBasedFindingsReport'
33 |
34 | comments:
35 | active: true
36 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
37 | AbsentOrWrongFileLicense:
38 | active: false
39 | licenseTemplateFile: 'license.template'
40 | CommentOverPrivateFunction:
41 | active: false
42 | CommentOverPrivateProperty:
43 | active: false
44 | EndOfSentenceFormat:
45 | active: false
46 | endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
47 | UndocumentedPublicClass:
48 | active: false
49 | searchInNestedClass: true
50 | searchInInnerClass: true
51 | searchInInnerObject: true
52 | searchInInnerInterface: true
53 | UndocumentedPublicFunction:
54 | active: false
55 | UndocumentedPublicProperty:
56 | active: false
57 |
58 | complexity:
59 | active: true
60 | ComplexCondition:
61 | active: true
62 | threshold: 4
63 | ComplexInterface:
64 | active: false
65 | threshold: 10
66 | includeStaticDeclarations: false
67 | includePrivateDeclarations: false
68 | ComplexMethod:
69 | active: true
70 | threshold: 15
71 | ignoreSingleWhenExpression: false
72 | ignoreSimpleWhenEntries: false
73 | ignoreNestingFunctions: false
74 | nestingFunctions: [run, let, apply, with, also, use, forEach, isNotNull, ifNull]
75 | LabeledExpression:
76 | active: false
77 | ignoredLabels: []
78 | LargeClass:
79 | active: true
80 | threshold: 600
81 | LongMethod:
82 | active: true
83 | threshold: 60
84 | LongParameterList:
85 | active: true
86 | functionThreshold: 6
87 | constructorThreshold: 7
88 | ignoreDefaultParameters: false
89 | ignoreDataClasses: true
90 | ignoreAnnotated: []
91 | MethodOverloading:
92 | active: false
93 | threshold: 6
94 | NestedBlockDepth:
95 | active: true
96 | threshold: 6
97 | StringLiteralDuplication:
98 | active: false
99 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
100 | threshold: 3
101 | ignoreAnnotation: true
102 | excludeStringsWithLessThan5Characters: true
103 | ignoreStringsRegex: '$^'
104 | TooManyFunctions:
105 | active: true
106 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
107 | thresholdInFiles: 11
108 | thresholdInClasses: 11
109 | thresholdInInterfaces: 11
110 | thresholdInObjects: 11
111 | thresholdInEnums: 11
112 | ignoreDeprecated: false
113 | ignorePrivate: false
114 | ignoreOverridden: false
115 |
116 | coroutines:
117 | active: true
118 | GlobalCoroutineUsage:
119 | active: false
120 | RedundantSuspendModifier:
121 | active: false
122 |
123 | empty-blocks:
124 | active: true
125 | EmptyCatchBlock:
126 | active: true
127 | allowedExceptionNameRegex: '_|(ignore|expected).*'
128 | EmptyClassBlock:
129 | active: false
130 | EmptyDefaultConstructor:
131 | active: true
132 | EmptyDoWhileBlock:
133 | active: true
134 | EmptyElseBlock:
135 | active: true
136 | EmptyFinallyBlock:
137 | active: true
138 | EmptyForBlock:
139 | active: true
140 | EmptyFunctionBlock:
141 | active: true
142 | ignoreOverridden: false
143 | EmptyIfBlock:
144 | active: true
145 | EmptyInitBlock:
146 | active: true
147 | EmptyKtFile:
148 | active: true
149 | EmptySecondaryConstructor:
150 | active: true
151 | EmptyTryBlock:
152 | active: true
153 | EmptyWhenBlock:
154 | active: true
155 | EmptyWhileBlock:
156 | active: true
157 |
158 | exceptions:
159 | active: true
160 | ExceptionRaisedInUnexpectedLocation:
161 | active: false
162 | methodNames: [toString, hashCode, equals, finalize]
163 | InstanceOfCheckForException:
164 | active: false
165 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
166 | NotImplementedDeclaration:
167 | active: false
168 | PrintStackTrace:
169 | active: false
170 | RethrowCaughtException:
171 | active: false
172 | ReturnFromFinally:
173 | active: false
174 | ignoreLabeled: false
175 | SwallowedException:
176 | active: false
177 | ignoredExceptionTypes:
178 | - InterruptedException
179 | - NumberFormatException
180 | - ParseException
181 | - MalformedURLException
182 | allowedExceptionNameRegex: '_|(ignore|expected).*'
183 | ThrowingExceptionFromFinally:
184 | active: false
185 | ThrowingExceptionInMain:
186 | active: false
187 | ThrowingExceptionsWithoutMessageOrCause:
188 | active: false
189 | exceptions:
190 | - IllegalArgumentException
191 | - IllegalStateException
192 | - IOException
193 | ThrowingNewInstanceOfSameException:
194 | active: false
195 | TooGenericExceptionCaught:
196 | active: true
197 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
198 | exceptionNames:
199 | - ArrayIndexOutOfBoundsException
200 | - Error
201 | - Exception
202 | - IllegalMonitorStateException
203 | - NullPointerException
204 | - IndexOutOfBoundsException
205 | - RuntimeException
206 | - Throwable
207 | allowedExceptionNameRegex: '_|(ignore|expected).*'
208 | TooGenericExceptionThrown:
209 | active: true
210 | exceptionNames:
211 | - Error
212 | - Exception
213 | - Throwable
214 | - RuntimeException
215 |
216 | formatting:
217 | active: true
218 | android: false
219 | autoCorrect: true
220 | AnnotationOnSeparateLine:
221 | active: false
222 | autoCorrect: true
223 | ChainWrapping:
224 | active: true
225 | autoCorrect: true
226 | CommentSpacing:
227 | active: true
228 | autoCorrect: true
229 | EnumEntryNameCase:
230 | active: false
231 | autoCorrect: true
232 | Filename:
233 | active: true
234 | FinalNewline:
235 | active: true
236 | autoCorrect: true
237 | insertFinalNewLine: true
238 | ImportOrdering:
239 | active: false
240 | autoCorrect: true
241 | layout: 'idea'
242 | Indentation:
243 | active: false
244 | autoCorrect: true
245 | indentSize: 4
246 | continuationIndentSize: 4
247 | MaximumLineLength:
248 | active: true
249 | maxLineLength: 120
250 | ModifierOrdering:
251 | active: true
252 | autoCorrect: true
253 | MultiLineIfElse:
254 | active: true
255 | autoCorrect: true
256 | NoBlankLineBeforeRbrace:
257 | active: true
258 | autoCorrect: true
259 | NoConsecutiveBlankLines:
260 | active: true
261 | autoCorrect: true
262 | NoEmptyClassBody:
263 | active: true
264 | autoCorrect: true
265 | NoEmptyFirstLineInMethodBlock:
266 | active: false
267 | autoCorrect: true
268 | NoLineBreakAfterElse:
269 | active: true
270 | autoCorrect: true
271 | NoLineBreakBeforeAssignment:
272 | active: true
273 | autoCorrect: true
274 | NoMultipleSpaces:
275 | active: true
276 | autoCorrect: true
277 | NoSemicolons:
278 | active: true
279 | autoCorrect: true
280 | NoTrailingSpaces:
281 | active: true
282 | autoCorrect: true
283 | NoUnitReturn:
284 | active: true
285 | autoCorrect: true
286 | NoUnusedImports:
287 | active: true
288 | autoCorrect: true
289 | NoWildcardImports:
290 | active: true
291 | PackageName:
292 | active: true
293 | autoCorrect: true
294 | ParameterListWrapping:
295 | active: true
296 | autoCorrect: true
297 | indentSize: 4
298 | SpacingAroundColon:
299 | active: true
300 | autoCorrect: true
301 | SpacingAroundComma:
302 | active: true
303 | autoCorrect: true
304 | SpacingAroundCurly:
305 | active: true
306 | autoCorrect: true
307 | SpacingAroundDot:
308 | active: true
309 | autoCorrect: true
310 | SpacingAroundDoubleColon:
311 | active: false
312 | autoCorrect: true
313 | SpacingAroundKeyword:
314 | active: true
315 | autoCorrect: true
316 | SpacingAroundOperators:
317 | active: true
318 | autoCorrect: true
319 | SpacingAroundParens:
320 | active: true
321 | autoCorrect: true
322 | SpacingAroundRangeOperator:
323 | active: true
324 | autoCorrect: true
325 | SpacingBetweenDeclarationsWithAnnotations:
326 | active: false
327 | autoCorrect: true
328 | SpacingBetweenDeclarationsWithComments:
329 | active: false
330 | autoCorrect: true
331 | StringTemplate:
332 | active: true
333 | autoCorrect: true
334 |
335 | naming:
336 | active: true
337 | ClassNaming:
338 | active: true
339 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
340 | classPattern: '[A-Z][a-zA-Z0-9]*'
341 | ConstructorParameterNaming:
342 | active: true
343 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
344 | parameterPattern: '[a-z][A-Za-z0-9]*'
345 | privateParameterPattern: '[a-z][A-Za-z0-9]*'
346 | excludeClassPattern: '$^'
347 | ignoreOverridden: true
348 | EnumNaming:
349 | active: true
350 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
351 | enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
352 | ForbiddenClassName:
353 | active: false
354 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
355 | forbiddenName: []
356 | FunctionMaxLength:
357 | active: false
358 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
359 | maximumFunctionNameLength: 30
360 | FunctionMinLength:
361 | active: false
362 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
363 | minimumFunctionNameLength: 3
364 | FunctionNaming:
365 | active: true
366 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
367 | functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)'
368 | excludeClassPattern: '$^'
369 | ignoreOverridden: true
370 | ignoreAnnotated: ['Composable']
371 | FunctionParameterNaming:
372 | active: true
373 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
374 | parameterPattern: '[a-z][A-Za-z0-9]*'
375 | excludeClassPattern: '$^'
376 | ignoreOverridden: true
377 | InvalidPackageDeclaration:
378 | active: false
379 | rootPackage: ''
380 | MatchingDeclarationName:
381 | active: true
382 | mustBeFirst: true
383 | MemberNameEqualsClassName:
384 | active: true
385 | ignoreOverridden: true
386 | ObjectPropertyNaming:
387 | active: true
388 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
389 | constantPattern: '[A-Za-z][_A-Za-z0-9]*'
390 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
391 | privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
392 | PackageNaming:
393 | active: true
394 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
395 | packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
396 | TopLevelPropertyNaming:
397 | active: true
398 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
399 | constantPattern: '[A-Z][_A-Z0-9]*'
400 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
401 | privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
402 | VariableMaxLength:
403 | active: false
404 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
405 | maximumVariableNameLength: 64
406 | VariableMinLength:
407 | active: false
408 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
409 | minimumVariableNameLength: 1
410 | VariableNaming:
411 | active: true
412 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
413 | variablePattern: '[a-z][A-Za-z0-9]*'
414 | privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
415 | excludeClassPattern: '$^'
416 | ignoreOverridden: true
417 |
418 | performance:
419 | active: true
420 | ArrayPrimitive:
421 | active: true
422 | ForEachOnRange:
423 | active: true
424 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
425 | SpreadOperator:
426 | active: true
427 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
428 | UnnecessaryTemporaryInstantiation:
429 | active: true
430 |
431 | potential-bugs:
432 | active: true
433 | Deprecation:
434 | active: false
435 | DuplicateCaseInWhenExpression:
436 | active: true
437 | EqualsAlwaysReturnsTrueOrFalse:
438 | active: true
439 | EqualsWithHashCodeExist:
440 | active: true
441 | ExplicitGarbageCollectionCall:
442 | active: true
443 | HasPlatformType:
444 | active: false
445 | IgnoredReturnValue:
446 | active: false
447 | restrictToAnnotatedMethods: true
448 | returnValueAnnotations: ['*.CheckReturnValue', '*.CheckResult']
449 | ImplicitDefaultLocale:
450 | active: false
451 | ImplicitUnitReturnType:
452 | active: false
453 | allowExplicitReturnType: true
454 | InvalidRange:
455 | active: true
456 | IteratorHasNextCallsNextMethod:
457 | active: true
458 | IteratorNotThrowingNoSuchElementException:
459 | active: true
460 | LateinitUsage:
461 | active: false
462 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
463 | excludeAnnotatedProperties: []
464 | ignoreOnClassesPattern: ''
465 | MapGetWithNotNullAssertionOperator:
466 | active: false
467 | MissingWhenCase:
468 | active: true
469 | RedundantElseInWhen:
470 | active: true
471 | UnconditionalJumpStatementInLoop:
472 | active: false
473 | UnnecessaryNotNullOperator:
474 | active: false
475 | UnnecessarySafeCall:
476 | active: false
477 | UnreachableCode:
478 | active: true
479 | UnsafeCallOnNullableType:
480 | active: true
481 | UnsafeCast:
482 | active: false
483 | UselessPostfixExpression:
484 | active: false
485 | WrongEqualsTypeParameter:
486 | active: true
487 |
488 | style:
489 | active: true
490 | CollapsibleIfStatements:
491 | active: false
492 | DataClassContainsFunctions:
493 | active: false
494 | conversionFunctionPrefix: 'to'
495 | DataClassShouldBeImmutable:
496 | active: false
497 | EqualsNullCall:
498 | active: true
499 | EqualsOnSignatureLine:
500 | active: false
501 | ExplicitCollectionElementAccessMethod:
502 | active: false
503 | ExplicitItLambdaParameter:
504 | active: false
505 | ExpressionBodySyntax:
506 | active: false
507 | includeLineWrapping: false
508 | ForbiddenComment:
509 | active: false
510 | values: ['TODO:', 'FIXME:', 'STOPSHIP:']
511 | allowedPatterns: ''
512 | ForbiddenImport:
513 | active: false
514 | imports: []
515 | forbiddenPatterns: ''
516 | ForbiddenMethodCall:
517 | active: false
518 | methods: ['kotlin.io.println', 'kotlin.io.print']
519 | ForbiddenPublicDataClass:
520 | active: false
521 | ignorePackages: ['*.internal', '*.internal.*']
522 | ForbiddenVoid:
523 | active: false
524 | ignoreOverridden: false
525 | ignoreUsageInGenerics: false
526 | FunctionOnlyReturningConstant:
527 | active: true
528 | ignoreOverridableFunction: true
529 | excludedFunctions: 'describeContents'
530 | excludeAnnotatedFunction: ['dagger.Provides']
531 | LibraryCodeMustSpecifyReturnType:
532 | active: true
533 | LoopWithTooManyJumpStatements:
534 | active: true
535 | maxJumpCount: 1
536 | MagicNumber:
537 | active: true
538 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
539 | ignoreNumbers: ['-1', '0', '1', '2']
540 | ignoreHashCodeFunction: true
541 | ignorePropertyDeclaration: false
542 | ignoreLocalVariableDeclaration: false
543 | ignoreConstantDeclaration: true
544 | ignoreCompanionObjectPropertyDeclaration: true
545 | ignoreAnnotation: false
546 | ignoreNamedArgument: true
547 | ignoreEnums: false
548 | ignoreRanges: false
549 | MandatoryBracesIfStatements:
550 | active: false
551 | MandatoryBracesLoops:
552 | active: false
553 | MaxLineLength:
554 | active: true
555 | maxLineLength: 120
556 | excludePackageStatements: true
557 | excludeImportStatements: true
558 | excludeCommentStatements: false
559 | MayBeConst:
560 | active: true
561 | ModifierOrder:
562 | active: true
563 | NestedClassesVisibility:
564 | active: false
565 | NewLineAtEndOfFile:
566 | active: false
567 | NoTabs:
568 | active: false
569 | OptionalAbstractKeyword:
570 | active: true
571 | OptionalUnit:
572 | active: false
573 | OptionalWhenBraces:
574 | active: false
575 | PreferToOverPairSyntax:
576 | active: false
577 | ProtectedMemberInFinalClass:
578 | active: true
579 | RedundantExplicitType:
580 | active: false
581 | RedundantVisibilityModifierRule:
582 | active: false
583 | ReturnCount:
584 | active: true
585 | max: 2
586 | excludedFunctions: 'equals'
587 | excludeLabeled: false
588 | excludeReturnFromLambda: true
589 | excludeGuardClauses: false
590 | SafeCast:
591 | active: true
592 | SerialVersionUIDInSerializableClass:
593 | active: false
594 | SpacingBetweenPackageAndImports:
595 | active: false
596 | ThrowsCount:
597 | active: true
598 | max: 2
599 | TrailingWhitespace:
600 | active: false
601 | UnderscoresInNumericLiterals:
602 | active: false
603 | acceptableDecimalLength: 5
604 | UnnecessaryAbstractClass:
605 | active: true
606 | excludeAnnotatedClasses: ['dagger.Module']
607 | UnnecessaryAnnotationUseSiteTarget:
608 | active: false
609 | UnnecessaryApply:
610 | active: false
611 | UnnecessaryInheritance:
612 | active: true
613 | UnnecessaryLet:
614 | active: false
615 | UnnecessaryParentheses:
616 | active: false
617 | UntilInsteadOfRangeTo:
618 | active: false
619 | UnusedImports:
620 | active: false
621 | UnusedPrivateClass:
622 | active: true
623 | UnusedPrivateMember:
624 | active: false
625 | allowedNames: '(_|ignored|expected|serialVersionUID)'
626 | UseArrayLiteralsInAnnotations:
627 | active: false
628 | UseCheckOrError:
629 | active: false
630 | UseDataClass:
631 | active: false
632 | excludeAnnotatedClasses: []
633 | allowVars: false
634 | UseIfInsteadOfWhen:
635 | active: false
636 | UseRequire:
637 | active: false
638 | UselessCallOnNotNull:
639 | active: true
640 | UtilityClassWithPublicConstructor:
641 | active: true
642 | VarCouldBeVal:
643 | active: false
644 | WildcardImport:
645 | active: true
646 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
647 | excludeImports: ['java.util.*', 'kotlinx.android.synthetic.*']
648 |
--------------------------------------------------------------------------------
/config/jacoco.gradle:
--------------------------------------------------------------------------------
1 | junitJacoco {
2 | jacocoVersion = '0.8.7' // type String
3 | ignoreProjects = [] // type String array
4 | excludes // type String List
5 | includeNoLocationClasses = false // type boolean
6 | includeInstrumentationCoverageInMergedReport = false // type boolean
7 | // xml.enabled = true
8 | // csv.enabled = true
9 | // html.enabled = true
10 | }
11 |
12 |
13 | //apply plugin: 'jacoco'
14 | //
15 | //jacoco {
16 | // // Custom reports directory can be specfied like this:
17 | // reportsDir = file("$project.rootDir/jacocoReport")
18 | //}
19 | //
20 | //tasks.withType(Test) {
21 | // jacoco.includeNoLocationClasses = true
22 | // jacoco.excludes = ['jdk.internal.*']
23 | // // see related issue https://github.com/gradle/gradle/issues/5184#issuecomment-457865951
24 | //}
25 | //
26 | //project.afterEvaluate {
27 | //
28 | // (android.hasProperty('applicationVariants')
29 | // ? android.'applicationVariants'
30 | // : android.'libraryVariants')
31 | // .all { variant ->
32 | // def variantName = variant.name
33 | // def unitTestTask = "test${variantName.capitalize()}UnitTest"
34 | // def androidTestCoverageTask = "create${variantName.capitalize()}CoverageReport"
35 | //
36 | // tasks.create(name: "${unitTestTask}Coverage", type: JacocoReport, dependsOn: [
37 | // "$unitTestTask",
38 | //// "$androidTestCoverageTask" // disable UI Test
39 | // ]) {
40 | // group = "Reporting"
41 | // description = "Generate Jacoco coverage reports for the ${variantName.capitalize()} build"
42 | //
43 | // reports {
44 | // html.enabled = true
45 | // xml.enabled = true
46 | // csv.enabled = true
47 | // }
48 | //
49 | // def excludes = [
50 | // // data binding
51 | // 'android/databinding/**/*.class',
52 | // '**/android/databinding/*Binding.class',
53 | // '**/android/databinding/*',
54 | // '**/androidx/databinding/*',
55 | // '**/BR.*',
56 | // // android
57 | // '**/R.class',
58 | // '**/R$*.class',
59 | // '**/BuildConfig.*',
60 | // '**/Manifest*.*',
61 | // '**/*Test*.*',
62 | // 'android/**/*.*',
63 | // // butterKnife
64 | // '**/*$ViewInjector*.*',
65 | // '**/*$ViewBinder*.*',
66 | // // dagger
67 | // '**/*_MembersInjector.class',
68 | // '**/Dagger*Component.class',
69 | // '**/Dagger*Component$Builder.class',
70 | // '**/*Module_*Factory.class',
71 | // '**/di/module/*',
72 | // '**/*_Factory*.*',
73 | // '**/*Module*.*',
74 | // '**/*Dagger*.*',
75 | // '**/*Hilt*.*',
76 | // // kotlin
77 | // '**/*MapperImpl*.*',
78 | // '**/*$ViewInjector*.*',
79 | // '**/*$ViewBinder*.*',
80 | // '**/BuildConfig.*',
81 | // '**/*Component*.*',
82 | // '**/*BR*.*',
83 | // '**/Manifest*.*',
84 | // '**/*$Lambda$*.*',
85 | // '**/*Companion*.*',
86 | // '**/*Module*.*',
87 | // '**/*Dagger*.*',
88 | // '**/*Hilt*.*',
89 | // '**/*MembersInjector*.*',
90 | // '**/*_MembersInjector.class',
91 | // '**/*_Factory*.*',
92 | // '**/*_Provide*Factory*.*',
93 | // '**/*Extensions*.*',
94 | // // sealed and data classes
95 | // '**/*$Result.*',
96 | // '**/*$Result$*.*'
97 | // ]
98 | //
99 | // def javaClasses = fileTree(dir: variant.javaCompileProvider.get().destinationDir,
100 | // excludes: excludes)
101 | //
102 | // def kotlinClasses = fileTree(dir: "${buildDir}/tmp/kotlin-classes/${variantName}",
103 | // excludes: excludes)
104 | //
105 | // classDirectories.setFrom(files([
106 | // javaClasses,
107 | // kotlinClasses
108 | // ]))
109 | //
110 | // def variantSourceSets = variant.sourceSets.java.srcDirs.collect { it.path }.flatten()
111 | // sourceDirectories.setFrom(project.files(variantSourceSets))
112 | //
113 | // def androidTestsData = fileTree(dir: "${buildDir}/outputs/code_coverage/${variantName}AndroidTest/connected/", includes: ["**/*.ec"])
114 | //
115 | // executionData(files([
116 | // "$project.buildDir/jacoco/${unitTestTask}.exec",
117 | // androidTestsData
118 | // ]))
119 | // }
120 | //
121 | // }
122 | //}
--------------------------------------------------------------------------------
/core-android/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core-android/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-kapt'
4 |
5 | android {
6 | compileSdkVersion AndroidConfig.compileSdkVersion
7 |
8 | defaultConfig {
9 | minSdkVersion AndroidConfig.minSdkVersion
10 | targetSdkVersion AndroidConfig.targetSdkVersion
11 | versionCode 1
12 | versionName "1.0"
13 |
14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
15 | consumerProguardFiles "consumer-rules.pro"
16 | }
17 |
18 | buildTypes {
19 | release {
20 | minifyEnabled false
21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
22 | }
23 | }
24 |
25 | buildFeatures {
26 | viewBinding true
27 | }
28 |
29 | compileOptions {
30 | sourceCompatibility JavaVersion.VERSION_1_8
31 | targetCompatibility JavaVersion.VERSION_1_8
32 | }
33 |
34 | kotlinOptions {
35 | jvmTarget = JavaVersion.VERSION_1_8.toString()
36 | }
37 | }
38 |
39 | dependencies {
40 | implementation fileTree(dir: "libs", include: ["*.jar"])
41 | implementation AndroidLib.kotlin_stdlib
42 | implementation AndroidLib.androidx_core
43 | implementation AndroidLib.androidx_appcompat
44 | implementation AndroidLib.androidx_constraintlayout
45 |
46 | implementation AndroidLib.coroutines_core
47 | implementation AndroidLib.coroutines_android
48 |
49 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-alpha05"
50 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha05"
51 | implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
52 |
53 | kapt "androidx.lifecycle:lifecycle-compiler:2.3.0-alpha05"
54 |
55 | implementation 'com.squareup.picasso:picasso:2.8'
56 |
57 | testImplementation AndroidTestLib.junit_lib
58 | androidTestImplementation AndroidTestLib.android_test_junit
59 | androidTestImplementation AndroidTestLib.android_test_espresso_core
60 | }
--------------------------------------------------------------------------------
/core-android/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pratamawijaya/BaseKotlinAndroid/ab535de8cd6aa9c468d69d3bebc54f6bd77fe250/core-android/consumer-rules.pro
--------------------------------------------------------------------------------
/core-android/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
--------------------------------------------------------------------------------
/core-android/src/androidTest/java/com/pratama/baseandroid/coreandroid/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.coreandroid
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.pratama.baseandroid.coreandroid.test", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/core-android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/core-android/src/main/java/com/pratama/baseandroid/coreandroid/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.coreandroid
2 |
3 | import androidx.appcompat.app.AppCompatActivity
4 |
5 | open class BaseActivity : AppCompatActivity() {
6 |
7 | }
--------------------------------------------------------------------------------
/core-android/src/main/java/com/pratama/baseandroid/coreandroid/BaseFragment.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.coreandroid
2 |
3 | import androidx.fragment.app.Fragment
4 |
5 | abstract class BaseFragment : Fragment() {
6 |
7 | }
--------------------------------------------------------------------------------
/core-android/src/main/java/com/pratama/baseandroid/coreandroid/BaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.coreandroid
2 |
3 | import android.util.Log
4 | import androidx.lifecycle.LiveData
5 | import androidx.lifecycle.MutableLiveData
6 | import androidx.lifecycle.ViewModel
7 | import kotlinx.coroutines.delay
8 | import kotlinx.coroutines.withTimeout
9 |
10 | abstract class BaseViewModel : ViewModel() {
11 |
12 | private val defaultNumberOfRetries = 3
13 | private val defaultTimeout = 10000L // 10sec
14 |
15 |
16 | fun uiState(): LiveData = uiState
17 | protected val uiState: MutableLiveData = MutableLiveData()
18 |
19 | protected suspend fun retryWithTimeout(
20 | numberOfRetries: Int = defaultNumberOfRetries,
21 | timeout: Long = defaultTimeout,
22 | block: suspend () -> T
23 | ) = retry(numberOfRetries) {
24 | withTimeout(timeout) {
25 | block()
26 | }
27 | }
28 |
29 | protected suspend fun retry(
30 | numberOfRetries: Int,
31 | delayBetweenRetries: Long = 100,
32 | block: suspend () -> T
33 | ): T {
34 | repeat(numberOfRetries) {
35 | try {
36 | return block()
37 | } catch (exception: Exception) {
38 | Log.e("error", exception.localizedMessage)
39 | }
40 | delay(delayBetweenRetries)
41 | }
42 | return block() // last attempt
43 | }
44 |
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/core-android/src/main/java/com/pratama/baseandroid/coreandroid/base/BaseActivityBinding.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.coreandroid.base
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import androidx.appcompat.app.AppCompatActivity
6 | import androidx.viewbinding.ViewBinding
7 |
8 | abstract class BaseActivityBinding : AppCompatActivity() {
9 |
10 | private var _binding: ViewBinding? = null
11 | abstract val bindingInflater: (LayoutInflater) -> T
12 |
13 | @Suppress("UNCHECKED_CAST")
14 | protected val binding: T
15 | get() = _binding as T
16 |
17 | override fun onCreate(savedInstanceState: Bundle?) {
18 | super.onCreate(savedInstanceState)
19 | _binding = bindingInflater.invoke(layoutInflater)
20 | setContentView(requireNotNull(_binding).root)
21 | setupView(binding)
22 | }
23 |
24 | abstract fun setupView(binding: T)
25 |
26 | override fun onDestroy() {
27 | super.onDestroy()
28 | _binding = null
29 | }
30 |
31 | }
--------------------------------------------------------------------------------
/core-android/src/main/java/com/pratama/baseandroid/coreandroid/base/BaseFragmentBinding.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.coreandroid.base
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.fragment.app.Fragment
8 | import androidx.viewbinding.ViewBinding
9 |
10 | /*
11 | https://chetangupta.net/viewbinding/
12 | */
13 |
14 | abstract class BaseFragmentBinding : Fragment() {
15 |
16 | private var _binding: T? = null
17 | private val binding get() = _binding!!
18 |
19 | abstract val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> T
20 |
21 | override fun onCreateView(
22 | inflater: LayoutInflater,
23 | container: ViewGroup?,
24 | savedInstanceState: Bundle?
25 | ): View? {
26 | _binding = bindingInflater.invoke(inflater, container, false)
27 | return requireNotNull(_binding).root
28 | }
29 |
30 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
31 | super.onViewCreated(view, savedInstanceState)
32 | setupView(binding)
33 | }
34 |
35 | abstract fun setupView(binding: T)
36 |
37 | override fun onDestroyView() {
38 | super.onDestroyView()
39 | _binding = null
40 | }
41 | }
--------------------------------------------------------------------------------
/core-android/src/main/java/com/pratama/baseandroid/coreandroid/base/network/NetworkboundResource.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.coreandroid.base.network
2 |
3 | import kotlinx.coroutines.flow.*
4 |
5 | inline fun networkBoundResource(
6 | crossinline query: suspend () -> Flow,
7 | crossinline fetch: suspend () -> RequestType,
8 | crossinline saveFetchResult: suspend (RequestType) -> Unit,
9 | crossinline onFetchFailed: (Throwable) -> Unit = { Unit },
10 | crossinline shouldFetch: (ResultType?) -> Boolean = { true }
11 | ) = flow> {
12 |
13 | emit(Resource.loading(null))
14 | val data = query().first()
15 |
16 | val flow = if (shouldFetch(data)) {
17 | emit(Resource.loading(data))
18 | try {
19 | saveFetchResult(fetch())
20 | query().map { Resource.success(it) }
21 | } catch (throwable: Throwable) {
22 | onFetchFailed(throwable)
23 | query().map {
24 | Resource.error(
25 | msg = throwable.localizedMessage,
26 | data = it,
27 | throwable = throwable
28 | )
29 | }
30 | }
31 | } else {
32 | query().map { Resource.success(it) }
33 | }
34 |
35 | emitAll(flow)
36 | }
37 |
--------------------------------------------------------------------------------
/core-android/src/main/java/com/pratama/baseandroid/coreandroid/base/network/Resource.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.coreandroid.base.network
2 |
3 | data class Resource(
4 | val status: Status,
5 | val data: T?,
6 | val message: String?,
7 | val throwable: Throwable? = null
8 | ) {
9 | enum class Status {
10 | SUCCESS,
11 | ERROR,
12 | LOADING
13 | }
14 |
15 | companion object {
16 | fun success(data: T?): Resource {
17 | return Resource(
18 | Status.SUCCESS,
19 | data,
20 | null
21 | )
22 | }
23 |
24 | fun error(msg: String, data: T? = null, throwable: Throwable?): Resource {
25 | return Resource(
26 | Status.ERROR,
27 | data,
28 | msg,
29 | throwable
30 | )
31 | }
32 |
33 | fun loading(data: T? = null): Resource {
34 | return Resource(
35 | Status.LOADING,
36 | data,
37 | null
38 | )
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/core-android/src/main/java/com/pratama/baseandroid/coreandroid/exception/Failure.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.coreandroid.exception
2 |
3 | sealed class Failure {
4 | object NetworkException : Failure()
5 | data class ServerError(val message: String?) : Failure()
6 | object LocalDataNotFound : Failure()
7 | }
--------------------------------------------------------------------------------
/core-android/src/main/java/com/pratama/baseandroid/coreandroid/extensions/Activity.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.coreandroid.extensions
2 |
3 | import android.app.Activity
4 | import android.widget.Toast
5 |
6 | fun Activity.toast(message: String) {
7 | Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
8 | }
--------------------------------------------------------------------------------
/core-android/src/main/java/com/pratama/baseandroid/coreandroid/extensions/Fragment.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.coreandroid.extensions
2 |
3 | import android.widget.Toast
4 | import androidx.fragment.app.Fragment
5 |
6 | fun Fragment.toast(message: String) {
7 | Toast.makeText(requireActivity(), message, Toast.LENGTH_SHORT).show()
8 | }
--------------------------------------------------------------------------------
/core-android/src/main/java/com/pratama/baseandroid/coreandroid/extensions/View.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.coreandroid.extensions
2 |
3 | import android.view.View
4 | import android.view.View.GONE
5 | import android.view.View.VISIBLE
6 | import android.widget.ImageView
7 | import com.squareup.picasso.Picasso
8 |
9 | fun View.toVisible() {
10 | this.visibility = VISIBLE
11 | }
12 |
13 | fun View.toGone() {
14 | this.visibility = GONE
15 | }
16 |
17 | fun ImageView.loadFromUrl(url: String) {
18 | if (url.isEmpty()) return
19 |
20 | Picasso.get().load(url).into(this)
21 | }
--------------------------------------------------------------------------------
/core-android/src/main/java/com/pratama/baseandroid/coreandroid/functional/Either.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2019 Fernando Cejas Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 |
18 | package com.pratama.baseandroid.coreandroid.functional
19 |
20 | /**
21 | * https://github.com/android10/Android-CleanArchitecture-Kotlin/blob/master/app/src/main/kotlin/com/fernandocejas/sample/core/functional/Either.kt
22 | *
23 | * Represents a value of one of two possible types (a disjoint union).
24 | * Instances of [Either] are either an instance of [Left] or [Right].
25 | * FP Convention dictates that [Left] is used for "failure"
26 | * and [Right] is used for "success".
27 | *
28 | * @see Left
29 | * @see Right
30 | */
31 | sealed class Either {
32 | /** * Represents the left side of [Either] class which by convention is a "Failure". */
33 | data class Left(val a: L) : Either()
34 |
35 | /** * Represents the right side of [Either] class which by convention is a "Success". */
36 | data class Right(val b: R) : Either()
37 |
38 | /**
39 | * Returns true if this is a Right, false otherwise.
40 | * @see Right
41 | */
42 | val isRight get() = this is Right
43 |
44 | /**
45 | * Returns true if this is a Left, false otherwise.
46 | * @see Left
47 | */
48 | val isLeft get() = this is Left
49 |
50 | /**
51 | * Creates a Left type.
52 | * @see Left
53 | */
54 | fun left(a: L) = Either.Left(a)
55 |
56 |
57 | /**
58 | * Creates a Left type.
59 | * @see Right
60 | */
61 | fun right(b: R) = Either.Right(b)
62 |
63 | /**
64 | * Applies fnL if this is a Left or fnR if this is a Right.
65 | * @see Left
66 | * @see Right
67 | */
68 | fun fold(fnL: (L) -> Any, fnR: (R) -> Any): Any =
69 | when (this) {
70 | is Left -> fnL(a)
71 | is Right -> fnR(b)
72 | }
73 | }
74 |
75 | /**
76 | * Composes 2 functions
77 | * See Credits to Alex Hart.
78 | */
79 | fun ((A) -> B).c(f: (B) -> C): (A) -> C = {
80 | f(this(it))
81 | }
82 |
83 | /**
84 | * Right-biased flatMap() FP convention which means that Right is assumed to be the default case
85 | * to operate on. If it is Left, operations like map, flatMap, ... return the Left value unchanged.
86 | */
87 | fun Either.flatMap(fn: (R) -> Either): Either =
88 | when (this) {
89 | is Either.Left -> Either.Left(a)
90 | is Either.Right -> fn(b)
91 | }
92 |
93 | /**
94 | * Right-biased map() FP convention which means that Right is assumed to be the default case
95 | * to operate on. If it is Left, operations like map, flatMap, ... return the Left value unchanged.
96 | */
97 | fun Either.map(fn: (R) -> (T)): Either = this.flatMap(fn.c(::right))
98 |
99 | /** Returns the value from this `Right` or the given argument if this is a `Left`.
100 | * Right(12).getOrElse(17) RETURNS 12 and Left(12).getOrElse(17) RETURNS 17
101 | */
102 | fun Either.getOrElse(value: R): R =
103 | when (this) {
104 | is Either.Left -> value
105 | is Either.Right -> b
106 | }
--------------------------------------------------------------------------------
/core-android/src/main/java/com/pratama/baseandroid/coreandroid/network/NetworkChecker.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.coreandroid.network
2 |
3 | interface NetworkChecker {
4 | fun isNetworkConnected(): Boolean
5 | }
--------------------------------------------------------------------------------
/core-android/src/main/java/com/pratama/baseandroid/coreandroid/network/NetworkCheckerImpl.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.coreandroid.network
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 | import android.net.NetworkCapabilities
6 | import android.os.Build
7 |
8 | class NetworkCheckerImpl(val ctx: Context) : NetworkChecker {
9 |
10 | companion object {
11 | const val CONNECTION_CELLULAR = 1
12 | const val CONNECTION_WIFI = 2
13 | const val CONNECTION_VPN = 3
14 | }
15 |
16 | override fun isNetworkConnected(): Boolean {
17 | val connectionType = getConnectionType(ctx)
18 | return connectionType != 0
19 | }
20 |
21 | private fun getConnectionType(context: Context): Int {
22 | var result = 0 // Returns connection type. 0: none; 1: mobile data; 2: wifi
23 | val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
24 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
25 | cm?.run {
26 | cm.getNetworkCapabilities(cm.activeNetwork)?.run {
27 | when {
28 | hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> {
29 | result = CONNECTION_WIFI
30 | }
31 | hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> {
32 | result = CONNECTION_CELLULAR
33 | }
34 | hasTransport(NetworkCapabilities.TRANSPORT_VPN) -> {
35 | result = CONNECTION_VPN
36 | }
37 | }
38 | }
39 | }
40 | } else {
41 | cm?.run {
42 | cm.activeNetworkInfo?.run {
43 | when (type) {
44 | ConnectivityManager.TYPE_WIFI -> {
45 | result = CONNECTION_WIFI
46 | }
47 | ConnectivityManager.TYPE_MOBILE -> {
48 | result = CONNECTION_CELLULAR
49 | }
50 | ConnectivityManager.TYPE_VPN -> {
51 | result = CONNECTION_VPN
52 | }
53 | }
54 | }
55 | }
56 | }
57 | return result
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/core-android/src/main/java/com/pratama/baseandroid/coreandroid/usecase/UseCase.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.coreandroid.usecase
2 |
3 | import com.pratama.baseandroid.coreandroid.exception.Failure
4 | import com.pratama.baseandroid.coreandroid.functional.Either
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.GlobalScope
7 | import kotlinx.coroutines.async
8 | import kotlinx.coroutines.launch
9 |
10 | abstract class UseCase where Type : Any {
11 | abstract suspend fun run(params: Params): Either
12 |
13 | operator fun invoke(params: Params, onResult: (Either) -> Unit = {}) {
14 | val job = GlobalScope.async(Dispatchers.IO) { run(params) }
15 | GlobalScope.launch(Dispatchers.Main) { onResult(job.await()) }
16 | }
17 |
18 | class None
19 | }
--------------------------------------------------------------------------------
/core-android/src/test/java/com/pratama/baseandroid/coreandroid/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.coreandroid
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 | }
--------------------------------------------------------------------------------
/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 -XX:+UseParallelGC
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=false
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 |
23 | org.gradle.caching=true
24 |
25 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pratamawijaya/BaseKotlinAndroid/ab535de8cd6aa9c468d69d3bebc54f6bd77fe250/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Mar 19 08:35:18 ICT 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/plugins/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | .gradle
--------------------------------------------------------------------------------
/plugins/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | mavenCentral()
4 | }
5 | dependencies {
6 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.21"
7 | }
8 | }
9 |
10 | apply plugin: 'kotlin'
11 | apply plugin: 'java-gradle-plugin'
12 | apply from: '../build-system/dependencies.gradle'
13 |
14 | repositories {
15 | mavenCentral()
16 | }
17 |
18 | dependencies {
19 |
20 | }
21 |
22 | compileKotlin {
23 | kotlinOptions {
24 | jvmTarget = "1.8"
25 | }
26 | }
27 | compileTestKotlin {
28 | kotlinOptions {
29 | jvmTarget = "1.8"
30 | }
31 | }
32 |
33 | gradlePlugin {
34 | plugins {
35 | helloPlugin {
36 | id = 'com.pratama.hello'
37 | implementationClass = 'com.pratama.hello.HelloPlugin'
38 | }
39 |
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/plugins/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
--------------------------------------------------------------------------------
/plugins/settings.gradle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pratamawijaya/BaseKotlinAndroid/ab535de8cd6aa9c468d69d3bebc54f6bd77fe250/plugins/settings.gradle
--------------------------------------------------------------------------------
/plugins/src/main/java/com/pratama/hello/HelloPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import org.gradle.api.provider.Property
4 |
5 | interface GreetingPluginExtension {
6 | val message: Property
7 | val greeter: Property
8 | }
9 |
10 | class HelloPlugin : Plugin {
11 |
12 | override fun apply(project: Project) {
13 | project.tasks.create("HelloPluginTask") {
14 | println("Hello There")
15 | }
16 | }
17 |
18 | }
--------------------------------------------------------------------------------
/routing/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/routing/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 |
4 | android {
5 | compileSdkVersion AndroidConfig.compileSdkVersion
6 |
7 | defaultConfig {
8 | minSdkVersion AndroidConfig.minSdkVersion
9 | targetSdkVersion AndroidConfig.targetSdkVersion
10 | versionCode 1
11 | versionName "1.0"
12 |
13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
14 | consumerProguardFiles "consumer-rules.pro"
15 | }
16 |
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 | }
24 |
25 | dependencies {
26 | implementation fileTree(dir: "libs", include: ["*.jar"])
27 | implementation AndroidLib.kotlin_stdlib
28 | implementation AndroidLib.androidx_core
29 | implementation AndroidLib.androidx_appcompat
30 | implementation AndroidLib.androidx_constraintlayout
31 |
32 | api AndroidLib.routing_navigator
33 |
34 | testImplementation AndroidTestLib.junit_lib
35 | androidTestImplementation AndroidTestLib.android_test_junit
36 | androidTestImplementation AndroidTestLib.android_test_espresso_core
37 | }
--------------------------------------------------------------------------------
/routing/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/routing/src/main/java/com/pratama/baseandroid/routing/Routes.kt:
--------------------------------------------------------------------------------
1 | package com.pratama.baseandroid.routing
2 |
3 | import com.github.florent37.navigator.Param
4 | import com.github.florent37.navigator.Route
5 | import com.github.florent37.navigator.RouteWithParams
6 |
7 | object Routes {
8 | object Splash : Route("/")
9 |
10 | object HomePage : Route("/home")
11 |
12 | object DetailPage : RouteWithParams("/detail/{id}") {
13 | data class Paramater(val id: Int) : Param()
14 | }
15 | }
--------------------------------------------------------------------------------
/secrets.properties:
--------------------------------------------------------------------------------
1 | API_KEY = "4b4df2ea3a154950852b6fda536cfb7f"
--------------------------------------------------------------------------------
/secrets.properties.example:
--------------------------------------------------------------------------------
1 | API_KEY = "ur api key"
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | includeBuild("plugins")
2 |
3 | include ':routing'
4 | include ':core-android'
5 | include ':app'
6 | rootProject.name = "BaseAndroidKotlin"
7 |
--------------------------------------------------------------------------------
/ss/ss1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pratamawijaya/BaseKotlinAndroid/ab535de8cd6aa9c468d69d3bebc54f6bd77fe250/ss/ss1.png
--------------------------------------------------------------------------------
/travis_bak.yml:
--------------------------------------------------------------------------------
1 | language: android
2 | sudo: required
3 | jdk: oraclejdk8
4 |
5 | env:
6 | global:
7 | - ANDROID_API_LEVEL=30
8 | - ANDROID_BUILD_TOOLS_VERSION=30.0.3
9 | - ANDROID_ABI=armeabi-v7a
10 |
11 | android:
12 | components:
13 | - tools
14 | - platform-tools
15 | - extra-google-m2repository
16 | - extra-android-m2repository
17 | licenses:
18 | - 'android-sdk-preview-license-52d11cd2'
19 | - 'android-sdk-license-.+'
20 | - 'google-gdk-license-.+'
21 |
22 | before_install:
23 | - touch $HOME/.android/repositories.cfg
24 | - yes | sdkmanager "platforms;android-30"
25 | - yes | sdkmanager "build-tools;30.0.3"
26 |
27 |
28 | before_cache:
29 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
30 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/
31 |
32 | cache:
33 | directories:
34 | - $HOME/.gradle/caches/
35 | - $HOME/.gradle/wrapper/
36 | - $HOME/.android/build-cache
37 |
38 | before_script:
39 | - chmod +x gradlew
40 |
41 | script:
42 | - ./gradlew clean check testDebugUnitTestCoverage assembleDebug
43 |
44 | after_success:
45 | - bash <(curl -s https://codecov.io/bash) -f jacocoReport/**/*.xml
46 |
47 |
48 |
--------------------------------------------------------------------------------