├── .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 | [![build](https://github.com/pratamawijaya/BaseKotlinAndroid/actions/workflows/build.yaml/badge.svg)](https://github.com/pratamawijaya/BaseKotlinAndroid/actions/workflows/build.yaml) 6 | 7 | [![codecov](https://codecov.io/gh/pratamawijaya/BaseKotlinAndroid/branch/master/graph/badge.svg)](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 | --------------------------------------------------------------------------------