├── .gitignore ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── nuhkoca │ │ └── offlineapp │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ │ └── com │ │ │ └── nuhkoca │ │ │ └── offlineapp │ │ │ ├── OfflineApp.kt │ │ │ ├── api │ │ │ ├── AuthInterceptor.kt │ │ │ └── INewsAPI.kt │ │ │ ├── base │ │ │ └── BaseAdapter.kt │ │ │ ├── binding │ │ │ ├── DateParsingBindingAdapter.kt │ │ │ ├── ImageBindingAdapter.kt │ │ │ └── ListBindingAdapter.kt │ │ │ ├── data │ │ │ ├── local │ │ │ │ ├── dao │ │ │ │ │ └── NewsDao.kt │ │ │ │ └── entity │ │ │ │ │ └── News.kt │ │ │ └── remote │ │ │ │ └── NewsResponse.kt │ │ │ ├── db │ │ │ └── NewsDB.kt │ │ │ ├── di │ │ │ ├── component │ │ │ │ ├── AppComponent.kt │ │ │ │ └── BindingComponent.kt │ │ │ ├── module │ │ │ │ ├── ActivityBuilder.kt │ │ │ │ ├── AppModule.kt │ │ │ │ ├── BindingModule.kt │ │ │ │ ├── ContextModule.kt │ │ │ │ ├── NetModule.kt │ │ │ │ ├── NewsGlideModule.kt │ │ │ │ ├── RoomModule.kt │ │ │ │ └── ViewModelModule.kt │ │ │ ├── qualifier │ │ │ │ └── ViewModelKey.kt │ │ │ └── scope │ │ │ │ ├── DataBinding.kt │ │ │ │ └── PerActivity.kt │ │ │ ├── helper │ │ │ ├── AppExecutors.kt │ │ │ ├── Constants.kt │ │ │ ├── DateTypeConverter.kt │ │ │ └── RecyclerViewItemDecoration.kt │ │ │ ├── repository │ │ │ ├── NetworkBoundResource.kt │ │ │ ├── NewsRepository.kt │ │ │ ├── Resource.kt │ │ │ ├── Status.kt │ │ │ └── UseCaseLiveData.kt │ │ │ ├── ui │ │ │ ├── news │ │ │ │ ├── NewsActivity.kt │ │ │ │ ├── NewsAdapter.kt │ │ │ │ ├── NewsUseCase.kt │ │ │ │ └── NewsViewModel.kt │ │ │ └── splash │ │ │ │ └── SplashActivity.kt │ │ │ ├── util │ │ │ ├── RateLimiter.kt │ │ │ └── ext │ │ │ │ └── ViewModelFactory.kt │ │ │ └── vm │ │ │ └── NewsViewModelFactory.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ ├── splash_bg.xml │ │ └── splash_icon.xml │ │ ├── layout │ │ ├── activity_news.xml │ │ └── news_item_layout.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── nuhkoca │ └── offlineapp │ └── ExampleUnitTest.java ├── build.gradle ├── buildSrc ├── .gitignore ├── build.gradle.kts └── src │ └── main │ └── java │ └── Depedencies.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── renovate.json └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | /.idea -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android-extensions' 3 | apply plugin: 'kotlin-android' 4 | apply plugin: 'kotlin-kapt' 5 | 6 | android { 7 | compileSdkVersion Versions.compile_sdk 8 | defaultConfig { 9 | applicationId "com.nuhkoca.offlineapp" 10 | minSdkVersion Versions.min_sdk 11 | targetSdkVersion Versions.target_sdk 12 | versionCode Versions.version_code 13 | versionName Versions.version_name 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | } 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | 23 | buildTypes.each { 24 | it.buildConfigField('String', 'BASE_URL', BASE_URL) 25 | it.buildConfigField('String', 'API_KEY', API_KEY) 26 | } 27 | 28 | dataBinding { 29 | enabled true 30 | } 31 | 32 | compileOptions { 33 | incremental true 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | sourceCompatibility JavaVersion.VERSION_1_8 36 | } 37 | 38 | allprojects { 39 | tasks.withType(JavaCompile) { 40 | options.compilerArgs << "-Xlint:-unchecked" << "-Xlint:deprecation" << "-Xdiags:verbose" 41 | } 42 | } 43 | } 44 | 45 | dependencies { 46 | implementation fileTree(dir: 'libs', include: ['*.jar']) 47 | implementation Libs.x 48 | implementation Libs.material 49 | implementation Libs.recyclerview 50 | implementation Libs.cardview 51 | implementation Libs.constrain_layout 52 | implementation Libs.room 53 | implementation Libs.room_runtime 54 | kapt Libs.room_compiler 55 | implementation Libs.paging 56 | implementation Libs.paging_runtime 57 | implementation Libs.lifecycle 58 | implementation Libs.lifecycle_common 59 | implementation Libs.dagger 60 | implementation Libs.dagger_android 61 | implementation Libs.dagger_support 62 | kapt Libs.dagger_processor 63 | kapt Libs.dagger_compiler 64 | implementation Libs.rxjava_android 65 | implementation Libs.rxjava 66 | implementation Libs.jetbrains 67 | implementation Libs.retrofit_adapter 68 | implementation Libs.retrofit 69 | implementation Libs.gson 70 | implementation Libs.logging 71 | implementation Libs.glide 72 | kapt Libs.glide_compiler 73 | implementation(Libs.okhttp) { 74 | exclude group: 'glide-parent' 75 | } 76 | implementation Libs.kotlin 77 | kapt Libs.databinding 78 | } 79 | repositories { 80 | mavenCentral() 81 | } 82 | -------------------------------------------------------------------------------- /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/androidTest/java/com/nuhkoca/offlineapp/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.InstrumentationRegistry; 6 | import androidx.test.runner.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getTargetContext(); 24 | 25 | assertEquals("com.nuhkoca.offlineapp", appContext.getPackageName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuhkoca/AndroidRecommendedArchitecture/9e40377f86c9cf3b10e659fedb82e94b0de0a724/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/OfflineApp.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp 2 | 3 | import androidx.databinding.DataBindingUtil 4 | import com.nuhkoca.offlineapp.di.component.DaggerAppComponent 5 | import com.nuhkoca.offlineapp.di.component.DaggerBindingComponent 6 | import dagger.android.AndroidInjector 7 | import dagger.android.support.DaggerApplication 8 | 9 | /** 10 | * An [DaggerApplication] that handles Dagger setup. 11 | * 12 | * @author nuhkoca 13 | */ 14 | class OfflineApp : DaggerApplication() { 15 | 16 | /** 17 | * Returns the injector 18 | * 19 | * @return the injector 20 | */ 21 | override fun applicationInjector(): AndroidInjector { 22 | val appComponent = DaggerAppComponent.builder().application(this).build() 23 | appComponent.inject(this) 24 | 25 | val bindingComponent = DaggerBindingComponent.builder().appComponent(appComponent).build() 26 | DataBindingUtil.setDefaultComponent(bindingComponent) 27 | 28 | return appComponent 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/api/AuthInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.api 2 | 3 | import com.nuhkoca.offlineapp.BuildConfig 4 | import com.nuhkoca.offlineapp.helper.Constants 5 | import okhttp3.Interceptor 6 | import okhttp3.Response 7 | import java.io.IOException 8 | import javax.inject.Inject 9 | import javax.inject.Singleton 10 | 11 | /** 12 | * A class that includes sensitive data to each network call 13 | * 14 | * @author nuhkoca 15 | */ 16 | @Singleton 17 | class AuthInterceptor 18 | /** 19 | * A default constructor that gets dependencies 20 | */ 21 | @Inject 22 | constructor() : Interceptor { 23 | 24 | /** 25 | * Adds specific headers to requests 26 | * 27 | * @param chain represents [Chain] 28 | * @return response with headers and others 29 | * @throws IOException throws 30 | */ 31 | @Throws(IOException::class) 32 | override fun intercept(chain: Interceptor.Chain): Response { 33 | val original = chain.request() 34 | val originalUrl = original.url() 35 | val url = originalUrl.newBuilder() 36 | .addQueryParameter(Constants.API_KEY, BuildConfig.API_KEY) 37 | .build() 38 | val requestBuilder = original.newBuilder().url(url) 39 | val newRequest = requestBuilder.build() 40 | 41 | return chain.proceed(newRequest) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/api/INewsAPI.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.api 2 | 3 | import com.nuhkoca.offlineapp.data.remote.NewsResponse 4 | 5 | import io.reactivex.Single 6 | import retrofit2.http.GET 7 | 8 | /** 9 | * The main services that handles all endpoint processes 10 | * 11 | * @author nuhkoca 12 | */ 13 | interface INewsAPI { 14 | 15 | @get:GET("everything?q=android&sortBy=popularity&sources=hacker-news,techcrunch&page=1") 16 | val news: Single 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/base/BaseAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.base 2 | 3 | import androidx.recyclerview.widget.RecyclerView 4 | 5 | abstract class BaseAdapter : RecyclerView.Adapter() { 6 | 7 | abstract fun setData(data: List) 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/binding/DateParsingBindingAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.binding 2 | 3 | import android.widget.TextView 4 | import androidx.databinding.BindingAdapter 5 | import java.text.SimpleDateFormat 6 | import java.util.* 7 | 8 | class DateParsingBindingAdapter { 9 | 10 | @BindingAdapter(value = ["android:date"]) 11 | fun bindDate(target: TextView, date: Date) { 12 | val formatter = SimpleDateFormat("MMM dd", Locale("tr", "TR", "tr")) 13 | target.text = formatter.format(date) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/binding/ImageBindingAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.binding 2 | 3 | import android.text.TextUtils 4 | import android.widget.ImageView 5 | import androidx.databinding.BindingAdapter 6 | import com.nuhkoca.offlineapp.di.module.NewsGlide 7 | 8 | /** 9 | * A [BindingAdapter] to bind images into ImageViews. 10 | * 11 | * @author nuhkoca 12 | */ 13 | /** 14 | * A default constructor that gets several dependents 15 | */ 16 | class ImageBindingAdapter { 17 | 18 | /** 19 | * A [BindingAdapter] method that binds image into view 20 | * 21 | * @param view represents an [ImageView] 22 | * @param url represents image url 23 | */ 24 | @BindingAdapter(value = ["android:src"]) 25 | fun bindImage(view: ImageView, url: String?) { 26 | if (!TextUtils.isEmpty(url) || url == null) { 27 | NewsGlide.with(view.context) 28 | .asBitmap() 29 | .load(url) 30 | .into(view) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/binding/ListBindingAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.binding 2 | 3 | import androidx.databinding.BindingAdapter 4 | import androidx.recyclerview.widget.RecyclerView 5 | import com.nuhkoca.offlineapp.base.BaseAdapter 6 | import com.nuhkoca.offlineapp.repository.Resource 7 | 8 | class ListBindingAdapter { 9 | 10 | @Suppress("UNCHECKED_CAST") 11 | @BindingAdapter(value = ["android:resource"]) 12 | fun setResource(recyclerView: RecyclerView, resource: Resource<*>) { 13 | val adapter = recyclerView.adapter ?: return 14 | 15 | resource.data?.let { 16 | if (adapter is BaseAdapter<*, *>) { 17 | adapter.setData(it as List) 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/data/local/dao/NewsDao.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.data.local.dao 2 | 3 | import com.nuhkoca.offlineapp.data.local.entity.News 4 | 5 | import androidx.lifecycle.LiveData 6 | import androidx.room.Dao 7 | import androidx.room.Insert 8 | import androidx.room.OnConflictStrategy 9 | import androidx.room.Query 10 | 11 | @Dao 12 | interface NewsDao { 13 | 14 | @Query("SELECT * FROM news") 15 | fun loadNews(): LiveData> 16 | 17 | @Insert(onConflict = OnConflictStrategy.REPLACE) 18 | fun saveNews(news: List) 19 | 20 | @Query("SELECT * FROM news WHERE id=:id") 21 | fun getNews(id: Int): LiveData 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/data/local/entity/News.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.data.local.entity 2 | 3 | import com.google.gson.annotations.SerializedName 4 | import com.nuhkoca.offlineapp.BR 5 | 6 | import java.util.Date 7 | 8 | import androidx.databinding.BaseObservable 9 | import androidx.databinding.Bindable 10 | import androidx.room.Entity 11 | import androidx.room.Index 12 | import androidx.room.PrimaryKey 13 | 14 | @Entity(tableName = "news", indices = [Index(value = ["title"], unique = true)]) 15 | class News : BaseObservable() { 16 | 17 | @PrimaryKey(autoGenerate = true) 18 | @SerializedName("id") 19 | @get:Bindable 20 | var id: Int = 0 21 | set(id) { 22 | field = id 23 | notifyPropertyChanged(BR.id) 24 | } 25 | 26 | @SerializedName("title") 27 | @get:Bindable 28 | var title: String? = null 29 | set(title) { 30 | field = title 31 | notifyPropertyChanged(BR.title) 32 | } 33 | 34 | @SerializedName("author") 35 | @get:Bindable 36 | var author: String? = null 37 | set(author) { 38 | field = author 39 | notifyPropertyChanged(BR.author) 40 | } 41 | 42 | @SerializedName("description") 43 | @get:Bindable 44 | var description: String? = null 45 | set(description) { 46 | field = description 47 | notifyPropertyChanged(BR.description) 48 | } 49 | 50 | @SerializedName("url") 51 | @get:Bindable 52 | var url: String? = null 53 | set(url) { 54 | field = url 55 | notifyPropertyChanged(BR.url) 56 | } 57 | 58 | @SerializedName("urlToImage") 59 | @get:Bindable 60 | var urlToImage: String? = null 61 | set(urlToImage) { 62 | field = urlToImage 63 | notifyPropertyChanged(BR.urlToImage) 64 | } 65 | 66 | @SerializedName("publishedAt") 67 | @get:Bindable 68 | var publishedAt: Date? = null 69 | set(publishedAt) { 70 | field = publishedAt 71 | notifyPropertyChanged(BR.publishedAt) 72 | } 73 | 74 | @SerializedName("content") 75 | @get:Bindable 76 | var content: String? = null 77 | set(content) { 78 | field = content 79 | notifyPropertyChanged(BR.content) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/data/remote/NewsResponse.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.data.remote 2 | 3 | import com.google.gson.annotations.SerializedName 4 | import com.nuhkoca.offlineapp.data.local.entity.News 5 | 6 | import androidx.databinding.BaseObservable 7 | import androidx.databinding.Bindable 8 | import com.nuhkoca.offlineapp.BR 9 | 10 | class NewsResponse : BaseObservable() { 11 | 12 | @SerializedName("articles") 13 | @get:Bindable 14 | var articles: List? = null 15 | set(articles) { 16 | field = articles 17 | notifyPropertyChanged(BR.articles) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/db/NewsDB.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.db 2 | 3 | import com.nuhkoca.offlineapp.data.local.dao.NewsDao 4 | import com.nuhkoca.offlineapp.data.local.entity.News 5 | import com.nuhkoca.offlineapp.helper.DateTypeConverter 6 | 7 | import androidx.room.Database 8 | import androidx.room.RoomDatabase 9 | import androidx.room.TypeConverters 10 | 11 | @Database(entities = [News::class], version = 1, exportSchema = false) 12 | @TypeConverters(DateTypeConverter::class) 13 | abstract class NewsDB : RoomDatabase() { 14 | 15 | abstract fun newsDao(): NewsDao 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/di/component/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.di.component 2 | 3 | import android.app.Application 4 | 5 | 6 | import com.nuhkoca.offlineapp.OfflineApp 7 | import com.nuhkoca.offlineapp.di.module.AppModule 8 | 9 | import javax.inject.Singleton 10 | 11 | import dagger.BindsInstance 12 | import dagger.Component 13 | import dagger.android.AndroidInjector 14 | import dagger.android.support.AndroidSupportInjectionModule 15 | 16 | /** 17 | * A main [Component] that conducts everything for injection 18 | * 19 | * @author nuhkoca 20 | */ 21 | @Singleton 22 | @Component(modules = [AndroidSupportInjectionModule::class, AppModule::class]) 23 | interface AppComponent : AndroidInjector { 24 | 25 | /** 26 | * Injects the main app 27 | * 28 | * @param instance represents an instance of of [OfflineApp] 29 | */ 30 | override fun inject(instance: OfflineApp) 31 | 32 | /** 33 | * Builder of [AppComponent] 34 | */ 35 | @Component.Builder 36 | interface Builder { 37 | @BindsInstance 38 | fun application(application: Application): AppComponent.Builder 39 | 40 | fun build(): AppComponent 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/di/component/BindingComponent.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.di.component 2 | 3 | import androidx.databinding.DataBindingComponent 4 | import com.nuhkoca.offlineapp.binding.DateParsingBindingAdapter 5 | import com.nuhkoca.offlineapp.binding.ImageBindingAdapter 6 | import com.nuhkoca.offlineapp.binding.ListBindingAdapter 7 | import com.nuhkoca.offlineapp.di.module.BindingModule 8 | import com.nuhkoca.offlineapp.di.scope.DataBinding 9 | import dagger.Component 10 | 11 | @DataBinding 12 | @Component(dependencies = [AppComponent::class], modules = [BindingModule::class]) 13 | interface BindingComponent: DataBindingComponent { 14 | 15 | override fun getImageBindingAdapter(): ImageBindingAdapter? 16 | 17 | override fun getListBindingAdapter(): ListBindingAdapter? 18 | 19 | override fun getDateParsingBindingAdapter(): DateParsingBindingAdapter? 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/di/module/ActivityBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.di.module 2 | 3 | import com.nuhkoca.offlineapp.di.scope.PerActivity 4 | import com.nuhkoca.offlineapp.ui.news.NewsActivity 5 | import com.nuhkoca.offlineapp.ui.splash.SplashActivity 6 | 7 | import dagger.Module 8 | import dagger.android.ContributesAndroidInjector 9 | 10 | /** 11 | * A [Module] that injects activities 12 | * 13 | * @author nuhkoca 14 | */ 15 | @Module 16 | abstract class ActivityBuilder { 17 | 18 | /** 19 | * Injects [SplashActivity] 20 | * 21 | * @return an instance of [SplashActivity] 22 | */ 23 | @PerActivity 24 | @ContributesAndroidInjector 25 | internal abstract fun contributesSplashActivityInjector(): SplashActivity 26 | 27 | /** 28 | * Injects [NewsActivity] 29 | * 30 | * @return an instance of [NewsActivity] 31 | */ 32 | @PerActivity 33 | @ContributesAndroidInjector 34 | internal abstract fun contributesNewsActivityInjector(): NewsActivity 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/di/module/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.di.module 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import io.reactivex.disposables.CompositeDisposable 6 | 7 | /** 8 | * A [Module] that injects general stuffs and includes other modules 9 | * 10 | * @author nuhkoca 11 | */ 12 | @Module(includes = [ActivityBuilder::class, ContextModule::class, ViewModelModule::class, RoomModule::class, NetModule::class]) 13 | class AppModule { 14 | 15 | /** 16 | * Returns an instance of [CompositeDisposable] 17 | * 18 | * @return an instance of [CompositeDisposable] 19 | */ 20 | @Provides 21 | internal fun provideCompositeDisposable(): CompositeDisposable { 22 | return CompositeDisposable() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/di/module/BindingModule.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.di.module 2 | 3 | import com.nuhkoca.offlineapp.binding.DateParsingBindingAdapter 4 | import com.nuhkoca.offlineapp.binding.ImageBindingAdapter 5 | import com.nuhkoca.offlineapp.binding.ListBindingAdapter 6 | import com.nuhkoca.offlineapp.di.scope.DataBinding 7 | 8 | import androidx.databinding.BindingAdapter 9 | import dagger.Module 10 | import dagger.Provides 11 | 12 | /** 13 | * A module that handles [BindingAdapter] classes as generic. 14 | * 15 | * @author nuhkoca 16 | */ 17 | @Module 18 | class BindingModule { 19 | 20 | /** 21 | * Returns an instance of [ImageBindingAdapter] 22 | * 23 | * @return an instance of [ImageBindingAdapter] 24 | */ 25 | @DataBinding 26 | @Provides 27 | internal fun bindsImageBindingAdapter(): ImageBindingAdapter { 28 | return ImageBindingAdapter() 29 | } 30 | 31 | /** 32 | * Returns an instance of [ListBindingAdapter] 33 | * 34 | * @return an instance of [ListBindingAdapter] 35 | */ 36 | @DataBinding 37 | @Provides 38 | internal fun bindsListBindingAdapter(): ListBindingAdapter { 39 | return ListBindingAdapter() 40 | } 41 | 42 | /** 43 | * Returns an instance of [DateParsingBindingAdapter] 44 | * 45 | * @return an instance of [DateParsingBindingAdapter] 46 | */ 47 | @DataBinding 48 | @Provides 49 | internal fun bindsDateParsingBindingAdapter(): DateParsingBindingAdapter { 50 | return DateParsingBindingAdapter() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/di/module/ContextModule.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.di.module 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | 6 | import dagger.Binds 7 | import dagger.Module 8 | 9 | /** 10 | * A [Module] that is resposible for injecting only [Context] 11 | */ 12 | @Module 13 | abstract class ContextModule { 14 | 15 | /** 16 | * Binds and returns an instance of [Context] 17 | * 18 | * @param application represents an instance of [Application] 19 | * @return an instance of [Context] 20 | */ 21 | @Binds 22 | internal abstract fun bindsContext(application: Application): Context 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/di/module/NetModule.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.di.module 2 | 3 | import com.google.gson.FieldNamingPolicy 4 | import com.google.gson.Gson 5 | import com.google.gson.GsonBuilder 6 | import com.nuhkoca.offlineapp.BuildConfig 7 | import com.nuhkoca.offlineapp.api.AuthInterceptor 8 | import com.nuhkoca.offlineapp.api.INewsAPI 9 | import com.nuhkoca.offlineapp.helper.Constants 10 | 11 | import java.util.concurrent.TimeUnit 12 | 13 | import javax.inject.Singleton 14 | import dagger.Module 15 | import dagger.Provides 16 | import okhttp3.OkHttpClient 17 | import okhttp3.logging.HttpLoggingInterceptor 18 | import retrofit2.Retrofit 19 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory 20 | import retrofit2.converter.gson.GsonConverterFactory 21 | 22 | /** 23 | * A [Module] that handles network injections 24 | * 25 | * @author nuhkoca 26 | */ 27 | @Module 28 | class NetModule { 29 | 30 | /** 31 | * Returns an instance of Gson 32 | * 33 | * @return an instance of [Gson] 34 | */ 35 | @Provides 36 | @Singleton 37 | internal fun provideGson(): Gson { 38 | return GsonBuilder() 39 | .setFieldNamingPolicy(FieldNamingPolicy.IDENTITY) 40 | .serializeNulls() 41 | .setLenient() 42 | .setDateFormat("dd-MM-yyyy") 43 | .create() 44 | } 45 | 46 | /** 47 | * Returns an instance of [HttpLoggingInterceptor] 48 | * 49 | * @return an instance of [HttpLoggingInterceptor] 50 | */ 51 | @Provides 52 | @Singleton 53 | internal fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor { 54 | return HttpLoggingInterceptor() 55 | .setLevel(HttpLoggingInterceptor.Level.BODY) 56 | } 57 | 58 | /** 59 | * Returns an instance of [OkHttpClient] 60 | * 61 | * @param httpLoggingInterceptor represents an instance of [HttpLoggingInterceptor] 62 | * @param authInterceptor represents an instance of [AuthInterceptor] 63 | * @return an instance of [OkHttpClient] 64 | */ 65 | @Provides 66 | @Singleton 67 | internal fun provideOkHttpClient(httpLoggingInterceptor: HttpLoggingInterceptor, authInterceptor: AuthInterceptor): OkHttpClient { 68 | val httpClient = OkHttpClient.Builder() 69 | httpClient.connectTimeout(Constants.DEFAULT_TIMEOUT.toLong(), TimeUnit.SECONDS) 70 | httpClient.readTimeout(Constants.DEFAULT_TIMEOUT.toLong(), TimeUnit.SECONDS) 71 | httpClient.writeTimeout(Constants.DEFAULT_TIMEOUT.toLong(), TimeUnit.SECONDS) 72 | 73 | httpClient.addInterceptor(authInterceptor) 74 | httpClient.interceptors().add(httpLoggingInterceptor) 75 | 76 | return httpClient.build() 77 | } 78 | 79 | /** 80 | * Returns an instance of AuthInterceptor 81 | * 82 | * @return an instance of [AuthInterceptor] 83 | */ 84 | @Provides 85 | @Singleton 86 | internal fun provideAuthInterceptor(): AuthInterceptor { 87 | return AuthInterceptor() 88 | } 89 | 90 | /** 91 | * Returns an instance of [Retrofit] 92 | * 93 | * @param okHttpClient represents an instance of [OkHttpClient] 94 | * @param gson represents an instance of [Gson] 95 | * @return an instance of [Retrofit] 96 | */ 97 | @Provides 98 | @Singleton 99 | internal fun provideRetrofitVideo(okHttpClient: OkHttpClient, gson: Gson): Retrofit { 100 | return Retrofit.Builder() 101 | .baseUrl(BuildConfig.BASE_URL) 102 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 103 | .addConverterFactory(GsonConverterFactory.create(gson)) 104 | .client(okHttpClient) 105 | .build() 106 | } 107 | 108 | /** 109 | * Returns an instance of INewsAPI 110 | * 111 | * @param retrofit represents an instance of [INewsAPI] 112 | * @return an instance of [INewsAPI] 113 | */ 114 | @Provides 115 | @Singleton 116 | internal fun provideIPetteServiceVideo(retrofit: Retrofit): INewsAPI { 117 | return retrofit.create(INewsAPI::class.java) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/di/module/NewsGlideModule.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.di.module 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | 6 | import com.bumptech.glide.Glide 7 | import com.bumptech.glide.GlideBuilder 8 | import com.bumptech.glide.Registry 9 | import com.bumptech.glide.annotation.Excludes 10 | import com.bumptech.glide.annotation.GlideModule 11 | import com.bumptech.glide.integration.okhttp3.OkHttpLibraryGlideModule 12 | import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader 13 | import com.bumptech.glide.load.engine.DiskCacheStrategy 14 | import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory 15 | import com.bumptech.glide.load.engine.cache.LruResourceCache 16 | import com.bumptech.glide.load.model.GlideUrl 17 | import com.bumptech.glide.module.AppGlideModule 18 | import com.bumptech.glide.request.RequestOptions 19 | import com.bumptech.glide.request.target.Target 20 | import com.bumptech.glide.signature.ObjectKey 21 | import com.nuhkoca.offlineapp.helper.Constants 22 | 23 | import java.io.InputStream 24 | import java.util.concurrent.TimeUnit 25 | import okhttp3.OkHttpClient 26 | 27 | import com.bumptech.glide.load.DecodeFormat.PREFER_ARGB_8888 28 | 29 | /** 30 | * A GlideModule for this app to manage all Glide structures in a single place 31 | * This will give the same impact on all Glide. 32 | * 33 | * @author nuhkoca 34 | */ 35 | @GlideModule(glideName = "NewsGlide") 36 | @Excludes(value = [OkHttpLibraryGlideModule::class]) 37 | class NewsGlideModule : AppGlideModule() { 38 | 39 | /** 40 | * Registers necessary components for Glide 41 | * 42 | * @param context represents an instance of [Context] 43 | * @param glide represents an instance of [Glide] 44 | * @param registry represents an instance of [Registry] 45 | */ 46 | override fun registerComponents(context: Context, glide: Glide, registry: Registry) { 47 | val client = OkHttpClient.Builder() 48 | .readTimeout(Constants.DEFAULT_TIMEOUT.toLong(), TimeUnit.SECONDS) 49 | .connectTimeout(Constants.DEFAULT_TIMEOUT.toLong(), TimeUnit.SECONDS) 50 | .build() 51 | 52 | val factory = OkHttpUrlLoader.Factory(client) 53 | 54 | glide.registry.replace(GlideUrl::class.java, InputStream::class.java, factory) 55 | } 56 | 57 | /** 58 | * Applies specific options to Glide 59 | * 60 | * @param context represents an instance of [Context] 61 | * @param builder represents an instance of [GlideBuilder] 62 | */ 63 | override fun applyOptions(context: Context, builder: GlideBuilder) { 64 | val memoryCacheSizeBytes = 1024 * 1024 * 300 // 300mb cache 65 | builder.setMemoryCache(LruResourceCache(memoryCacheSizeBytes.toLong())) 66 | builder.setDiskCache(InternalCacheDiskCacheFactory(context, memoryCacheSizeBytes.toLong())) 67 | builder.setDefaultRequestOptions(requestOptions()) 68 | } 69 | 70 | /** 71 | * A default [RequestOptions] to customize Glide 72 | * 73 | * @return an instance of [RequestOptions] 74 | */ 75 | private fun requestOptions(): RequestOptions { 76 | return RequestOptions() 77 | .signature(ObjectKey( 78 | System.currentTimeMillis() / (168 * 60 * 60 * 1000))) // 1 week cache 79 | .centerCrop() 80 | .dontAnimate() 81 | .override(Target.SIZE_ORIGINAL) 82 | .encodeFormat(Bitmap.CompressFormat.PNG) 83 | .encodeQuality(100) 84 | .diskCacheStrategy(DiskCacheStrategy.RESOURCE) 85 | .format(PREFER_ARGB_8888) 86 | .skipMemoryCache(true) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/di/module/RoomModule.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.di.module 2 | 3 | import android.content.Context 4 | 5 | import com.nuhkoca.offlineapp.data.local.dao.NewsDao 6 | import com.nuhkoca.offlineapp.db.NewsDB 7 | import com.nuhkoca.offlineapp.helper.Constants 8 | 9 | import javax.inject.Singleton 10 | import androidx.room.Room 11 | import androidx.room.migration.Migration 12 | import androidx.sqlite.db.SupportSQLiteDatabase 13 | import dagger.Module 14 | import dagger.Provides 15 | 16 | @Module 17 | class RoomModule { 18 | 19 | /** 20 | * Returns an instance of Migration 21 | * 22 | * @return an instance of [Migration] 23 | */ 24 | @Singleton 25 | @Provides 26 | internal fun provideMigration(): Migration { 27 | return object : Migration(Constants.DB_MIGRATION_START_VERSION, Constants.DB_MIGRATION_TARGET_VERSION) { 28 | override fun migrate(database: SupportSQLiteDatabase) { 29 | // Do nothing 30 | } 31 | } 32 | } 33 | 34 | /** 35 | * Returns an instance of NewsDB 36 | * 37 | * @param context represents an instance of [Context] 38 | * @param migration represents a custom [Migration] 39 | * @return an instance of [NewsDB] 40 | */ 41 | @Singleton 42 | @Provides 43 | internal fun provideNewsDB(context: Context, migration: Migration): NewsDB { 44 | return Room.databaseBuilder(context, NewsDB::class.java, Constants.NEWS_DB_NAME) 45 | .addMigrations(migration) 46 | .build() 47 | } 48 | 49 | /** 50 | * Returns an instance of NewsDao 51 | * 52 | * @param newsDB an instance of [NewsDB] 53 | * @return an instance of [NewsDao] 54 | */ 55 | @Singleton 56 | @Provides 57 | internal fun provideNewsDao(newsDB: NewsDB): NewsDao { 58 | return newsDB.newsDao() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/di/module/ViewModelModule.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.di.module 2 | 3 | import com.nuhkoca.offlineapp.di.qualifier.ViewModelKey 4 | import com.nuhkoca.offlineapp.ui.news.NewsViewModel 5 | import com.nuhkoca.offlineapp.vm.NewsViewModelFactory 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.ViewModelProvider 8 | import dagger.Binds 9 | import dagger.Module 10 | import dagger.multibindings.IntoMap 11 | 12 | /** 13 | * A [Module] that injects ViewModels 14 | * 15 | * @author nuhkoca 16 | */ 17 | @Module 18 | abstract class ViewModelModule { 19 | 20 | @Binds 21 | @IntoMap 22 | @ViewModelKey(NewsViewModel::class) 23 | internal abstract fun bindNewsViewModel(newsViewModel: NewsViewModel): ViewModel 24 | 25 | /** 26 | * Returns an instance of [NewsViewModelFactory] 27 | * 28 | * @param newsViewModelFactory an instance of [NewsViewModelFactory] 29 | * @return an instance of [NewsViewModelFactory] 30 | */ 31 | @Binds 32 | internal abstract fun bindsNewsViewModelFactory(newsViewModelFactory: NewsViewModelFactory): ViewModelProvider.Factory 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/di/qualifier/ViewModelKey.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.di.qualifier 2 | 3 | import androidx.lifecycle.ViewModel 4 | import dagger.MapKey 5 | import kotlin.reflect.KClass 6 | 7 | /** 8 | * An annotation that work for [ViewModel] classes. 9 | * 10 | * @author nuhkoca 11 | */ 12 | @MustBeDocumented 13 | @MapKey 14 | @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) 15 | @Retention(AnnotationRetention.RUNTIME) 16 | annotation class ViewModelKey(val value: KClass) 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/di/scope/DataBinding.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.di.scope 2 | 3 | import javax.inject.Scope 4 | 5 | /** 6 | * A scope for DataBinding classes. 7 | * 8 | * @author nuhkoca 9 | */ 10 | @Scope 11 | @Retention(AnnotationRetention.RUNTIME) 12 | @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.CLASS, AnnotationTarget.FILE) 13 | annotation class DataBinding 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/di/scope/PerActivity.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.di.scope 2 | 3 | import javax.inject.Scope 4 | 5 | /** 6 | * A [Scope] for activities. 7 | * 8 | * @author nuhkoca 9 | */ 10 | @Scope 11 | @Retention(AnnotationRetention.RUNTIME) 12 | annotation class PerActivity 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/helper/AppExecutors.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.helper 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | 6 | import java.util.concurrent.Executor 7 | import java.util.concurrent.Executors 8 | 9 | import javax.inject.Inject 10 | 11 | /** 12 | * A class that makes I/O, background or UI based processes. 13 | * 14 | * @author nuhkoca 15 | */ 16 | class AppExecutors 17 | /** 18 | * Default constructor 19 | * 20 | * @param diskIO represents background threads 21 | * @param networkIO represents network threads 22 | */ 23 | private constructor(private val diskIO: Executor, private val networkIO: Executor) { 24 | private val mainIO: Executor 25 | 26 | @Inject 27 | internal constructor() : this(Executors.newSingleThreadExecutor(), Executors.newFixedThreadPool(Constants.EXECUTOR_THREAD_POOL_OFFSET)) 28 | 29 | init { 30 | this.mainIO = MainThreadExecutor() 31 | } 32 | 33 | /** 34 | * A class for [.mainIO] 35 | */ 36 | internal class MainThreadExecutor : Executor { 37 | private val mainThreadHandler = Handler(Looper.getMainLooper()) 38 | 39 | override fun execute(command: Runnable) { 40 | mainThreadHandler.post(command) 41 | } 42 | } 43 | 44 | /** 45 | * Returns network executor 46 | * 47 | * @return [Executor] 48 | */ 49 | fun networkIO(): Executor { 50 | return networkIO 51 | } 52 | 53 | /** 54 | * Returns I/O executor 55 | * 56 | * @return [Executor] 57 | */ 58 | fun diskIO(): Executor { 59 | return diskIO 60 | } 61 | 62 | /** 63 | * Return UI executor 64 | * 65 | * @return [Executor] 66 | */ 67 | fun mainIO(): Executor { 68 | return mainIO 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/helper/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.helper 2 | 3 | 4 | /** 5 | * A helper class that holds appliation constants 6 | * 7 | * @author nuhkoca 8 | */ 9 | class Constants { 10 | init { 11 | throw AssertionError() 12 | } 13 | 14 | companion object { 15 | 16 | val DEFAULT_TIMEOUT = 60 17 | val EXECUTOR_THREAD_POOL_OFFSET = 5 18 | val DB_MIGRATION_START_VERSION = 1 19 | val DB_MIGRATION_TARGET_VERSION = 2 20 | val NEWS_DB_NAME = "news.db" 21 | val API_KEY = "apiKey" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/helper/DateTypeConverter.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.helper 2 | 3 | import java.util.Date 4 | 5 | import androidx.room.TypeConverter 6 | 7 | class DateTypeConverter { 8 | 9 | @TypeConverter 10 | fun toDate(value: Long?): Date? { 11 | return if (value == null) null else Date(value) 12 | } 13 | 14 | @TypeConverter 15 | fun toLong(value: Date?): Long? { 16 | return value?.time 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/helper/RecyclerViewItemDecoration.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.helper 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Rect 6 | import android.graphics.drawable.Drawable 7 | import android.util.TypedValue 8 | import android.view.View 9 | import androidx.recyclerview.widget.LinearLayoutManager 10 | import androidx.recyclerview.widget.RecyclerView 11 | 12 | /** 13 | * A helper class for [RecyclerView] and its [RecyclerView.ItemDecoration] 14 | * which adds either vertical or horizontal line under each item except the first one. 15 | * 16 | * @author nuhkoca 17 | */ 18 | class RecyclerViewItemDecoration(private val context: Context, orientation: Int, private val margin: Int) : RecyclerView.ItemDecoration() { 19 | 20 | private val mDivider: Drawable? 21 | private var mOrientation: Int = 0 22 | 23 | init { 24 | val a = context.obtainStyledAttributes(ATTRS) 25 | mDivider = a.getDrawable(0) 26 | a.recycle() 27 | setOrientation(orientation) 28 | } 29 | 30 | /** 31 | * Handles orientation 32 | * It can be either [RecyclerView.LayoutManager.VERTICAL] or [RecyclerView.LayoutManager.HORIZONTAL] 33 | * @param orientation 0 equals to [RecyclerView.LayoutManager.VERTICAL] and 1 equals to [RecyclerView.LayoutManager.HORIZONTAL] 34 | */ 35 | fun setOrientation(orientation: Int) { 36 | if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) { 37 | throw IllegalArgumentException("invalid orientation") 38 | } 39 | mOrientation = orientation 40 | } 41 | 42 | /** 43 | * Draw any appropriate decorations into the Canvas supplied to the RecyclerView. 44 | * Any content drawn by this method will be drawn after the item views are drawn 45 | * and will thus appear over the views. 46 | * 47 | * @param c Canvas to draw into 48 | * @param parent RecyclerView this ItemDecoration is drawing into 49 | * @param state The current state of RecyclerView. 50 | */ 51 | override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { 52 | if (mOrientation == VERTICAL_LIST) { 53 | drawVertical(c, parent) 54 | } 55 | } 56 | 57 | /** 58 | * Draws vertical line 59 | * @param c represents [Canvas] 60 | * @param parent represents [RecyclerView] 61 | */ 62 | private fun drawVertical(c: Canvas, parent: RecyclerView) { 63 | val left = parent.paddingLeft 64 | val right = parent.width - parent.paddingRight 65 | 66 | val childCount = parent.childCount - 1 67 | for (i in 0 until childCount) { 68 | val child = parent.getChildAt(i) 69 | val params = child 70 | .layoutParams as RecyclerView.LayoutParams 71 | val top = child.bottom + params.bottomMargin 72 | val bottom = top + mDivider!!.intrinsicHeight 73 | mDivider.setBounds(left + dpToPx(margin), top, right - dpToPx(margin), bottom) 74 | mDivider.draw(c) 75 | } 76 | } 77 | 78 | /** 79 | * Retrieve any offsets for the given item. Each field of `outRect` specifies 80 | * the number of pixels that the item view should be inset by, similar to padding or margin. 81 | * The default implementation sets the bounds of outRect to 0 and returns. 82 | * 83 | * 84 | * 85 | * If this ItemDecoration does not affect the positioning of item views, it should set 86 | * all four fields of `outRect` (left, top, right, bottom) to zero 87 | * before returning. 88 | * 89 | * 90 | * 91 | * If you need to access Adapter for additional data, you can call 92 | * [RecyclerView.getChildAdapterPosition] to get the adapter position of the 93 | * View. 94 | * 95 | * @param outRect Rect to receive the output. 96 | * @param view The child view to decorate 97 | * @param parent RecyclerView this ItemDecoration is decorating 98 | * @param state The current state of RecyclerView. 99 | */ 100 | override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { 101 | if (mOrientation == VERTICAL_LIST) { 102 | outRect.set(0, 0, 0, mDivider!!.intrinsicHeight) 103 | } else { 104 | outRect.set(0, 0, mDivider!!.intrinsicWidth, 0) 105 | } 106 | } 107 | 108 | /** 109 | * Converts dp to px and returns the value accordingly 110 | * @param dp value in dp 111 | * @return returns the value accordingly 112 | */ 113 | private fun dpToPx(dp: Int): Int { 114 | val r = context.resources 115 | return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), r.displayMetrics)) 116 | } 117 | 118 | companion object { 119 | 120 | private val ATTRS = intArrayOf(android.R.attr.listDivider) 121 | 122 | private val HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL 123 | private val VERTICAL_LIST = LinearLayoutManager.VERTICAL 124 | } 125 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/repository/NetworkBoundResource.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.repository 2 | 3 | import android.util.Log 4 | import androidx.annotation.MainThread 5 | import androidx.annotation.WorkerThread 6 | import androidx.lifecycle.LiveData 7 | import androidx.lifecycle.MediatorLiveData 8 | import io.reactivex.Completable 9 | import io.reactivex.CompletableObserver 10 | import io.reactivex.Single 11 | import io.reactivex.SingleObserver 12 | import io.reactivex.android.schedulers.AndroidSchedulers 13 | import io.reactivex.disposables.Disposable 14 | import io.reactivex.schedulers.Schedulers 15 | 16 | abstract class NetworkBoundResource 17 | @MainThread internal constructor() { 18 | private val result = MediatorLiveData>() 19 | private var mDisposable: Disposable? = null 20 | private var dbSource: LiveData 21 | 22 | internal val asLiveData: LiveData> 23 | get() = result 24 | 25 | init { 26 | result.value = Resource.loading(null) 27 | @Suppress("LeakingThis") 28 | dbSource = loadFromDb() 29 | result.addSource(dbSource) { data -> 30 | result.removeSource(dbSource) 31 | if (shouldFetch(data)) { 32 | fetchFromNetwork(dbSource) 33 | Log.d("TAG", "Network called") 34 | } else { 35 | result.addSource(dbSource) { newData -> result.setValue(Resource.success(newData)) } 36 | Log.d("TAG", "DB called") 37 | } 38 | } 39 | } 40 | 41 | private fun fetchFromNetwork(dbSource: LiveData) { 42 | result.addSource(dbSource) { newData -> result.setValue(Resource.loading(newData)) } 43 | createCall() 44 | .subscribeOn(Schedulers.io()) 45 | .observeOn(AndroidSchedulers.mainThread()) 46 | .subscribe(object : SingleObserver { 47 | override fun onSubscribe(d: Disposable) { 48 | if (!d.isDisposed) { 49 | mDisposable = d 50 | } 51 | } 52 | 53 | override fun onSuccess(requestType: RequestType) { 54 | result.removeSource(dbSource) 55 | saveResultAndReInit(requestType) 56 | Log.d("TAG", "Network called success") 57 | } 58 | 59 | override fun onError(e: Throwable) { 60 | onFetchFailed() 61 | result.removeSource(dbSource) 62 | result.addSource(dbSource) { newData -> 63 | result.setValue(Resource.error(e.message.toString(), newData)) } 64 | mDisposable!!.dispose() 65 | Log.d("TAG", "Network called error") 66 | } 67 | }) 68 | } 69 | 70 | @MainThread 71 | private fun saveResultAndReInit(response: RequestType) { 72 | Completable 73 | .fromCallable { saveCallResult(response) } 74 | .subscribeOn(Schedulers.io()) 75 | .observeOn(AndroidSchedulers.mainThread()) 76 | .subscribe(object : CompletableObserver { 77 | override fun onSubscribe(d: Disposable) { 78 | if (!d.isDisposed) { 79 | mDisposable = d 80 | } 81 | } 82 | 83 | override fun onComplete() { 84 | result.addSource(loadFromDb()) { newData -> result.setValue(Resource.success(newData)) } 85 | mDisposable!!.dispose() 86 | Log.d("TAG", "Network called reinit") 87 | } 88 | 89 | override fun onError(e: Throwable) { 90 | mDisposable!!.dispose() 91 | Log.d("TAG", "Network called error reinit") 92 | } 93 | }) 94 | } 95 | 96 | @WorkerThread 97 | protected abstract fun saveCallResult(item: RequestType) 98 | 99 | @MainThread 100 | protected abstract fun shouldFetch(data: ResultType?): Boolean 101 | 102 | @MainThread 103 | protected abstract fun loadFromDb(): LiveData 104 | 105 | @MainThread 106 | protected abstract fun createCall(): Single 107 | 108 | @MainThread 109 | protected abstract fun onFetchFailed() 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/repository/NewsRepository.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.repository 2 | 3 | import com.nuhkoca.offlineapp.api.INewsAPI 4 | import com.nuhkoca.offlineapp.data.local.dao.NewsDao 5 | import com.nuhkoca.offlineapp.data.local.entity.News 6 | import com.nuhkoca.offlineapp.data.remote.NewsResponse 7 | import com.nuhkoca.offlineapp.util.RateLimiter 8 | import java.util.concurrent.TimeUnit 9 | 10 | import javax.inject.Inject 11 | import androidx.lifecycle.LiveData 12 | import io.reactivex.Single 13 | 14 | class NewsRepository @Inject 15 | internal constructor(private val newsDao: NewsDao, private val iNewsAPI: INewsAPI) { 16 | 17 | private val rateLimiter = RateLimiter(10, TimeUnit.SECONDS) 18 | 19 | fun loadNews(): LiveData>> { 20 | return object : NetworkBoundResource, NewsResponse>() { 21 | override fun saveCallResult(item: NewsResponse) { 22 | newsDao.saveNews(item.articles!!) 23 | } 24 | 25 | override fun shouldFetch(data: List?): Boolean { 26 | return data == null || data.isEmpty() || rateLimiter.shouldFetch(RATE_LIMITER_TYPE) 27 | } 28 | 29 | override fun loadFromDb(): LiveData> { 30 | return newsDao.loadNews() 31 | } 32 | 33 | override fun createCall(): Single { 34 | return iNewsAPI.news 35 | } 36 | 37 | override fun onFetchFailed() { 38 | rateLimiter.reset(RATE_LIMITER_TYPE) 39 | } 40 | }.asLiveData 41 | } 42 | 43 | companion object { 44 | 45 | private const val RATE_LIMITER_TYPE = "data" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/repository/Resource.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.repository 2 | 3 | import com.nuhkoca.offlineapp.repository.Status.ERROR 4 | import com.nuhkoca.offlineapp.repository.Status.LOADING 5 | import com.nuhkoca.offlineapp.repository.Status.SUCCESS 6 | 7 | /** 8 | * A generic class that holds a value with its loading status. 9 | * @param 10 | */ 11 | data class Resource(val status: Status, val data: T?, val message: String?) { 12 | companion object { 13 | fun success(data: T?): Resource { 14 | return Resource(SUCCESS, data, null) 15 | } 16 | 17 | fun error(msg: String, data: T?): Resource { 18 | return Resource(ERROR, data, msg) 19 | } 20 | 21 | fun loading(data: T?): Resource { 22 | return Resource(LOADING, data, null) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/repository/Status.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.repository 2 | 3 | enum class Status { 4 | SUCCESS, 5 | ERROR, 6 | LOADING 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/repository/UseCaseLiveData.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.repository 2 | 3 | import androidx.lifecycle.LiveData 4 | import io.reactivex.disposables.Disposable 5 | 6 | /** 7 | * The [UseCaseLiveData] helper class that runs repository methods 8 | * @param represents model 9 | * @param

represents params 10 | * 11 | * @author nuhkoca 12 | */ 13 | abstract class UseCaseLiveData(newsRepository: NewsRepository) { 14 | 15 | internal val repository: NewsRepository = newsRepository 16 | 17 | abstract fun buildUseCaseObservable(params: P?): LiveData 18 | 19 | /** 20 | * Executes the target call 21 | * 22 | * @param params represents params to be passed 23 | * @return [Disposable] result 24 | */ 25 | fun execute(params: P?): LiveData { 26 | return buildUseCaseObservable(params) 27 | } 28 | 29 | fun getNewsRepository(): NewsRepository { 30 | return repository 31 | } 32 | 33 | class None 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/ui/news/NewsActivity.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.ui.news 2 | 3 | import android.os.Bundle 4 | import androidx.databinding.DataBindingUtil 5 | import androidx.lifecycle.ViewModelProvider 6 | import com.nuhkoca.offlineapp.R 7 | import com.nuhkoca.offlineapp.databinding.ActivityNewsBinding 8 | import com.nuhkoca.offlineapp.helper.RecyclerViewItemDecoration 9 | import com.nuhkoca.offlineapp.util.ext.get 10 | import dagger.android.support.DaggerAppCompatActivity 11 | import javax.inject.Inject 12 | 13 | class NewsActivity : DaggerAppCompatActivity() { 14 | 15 | @Inject 16 | lateinit var viewModelFactory: ViewModelProvider.Factory 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | val activityNewsBinding = DataBindingUtil.setContentView(this, R.layout.activity_news) 21 | val newsViewModel: NewsViewModel = viewModelFactory.get(this) 22 | activityNewsBinding.run { 23 | rvNews.adapter = NewsAdapter() 24 | rvNews.addItemDecoration(RecyclerViewItemDecoration(applicationContext, 1, 0)) 25 | viewmodel = newsViewModel 26 | lifecycleOwner = this@NewsActivity 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/ui/news/NewsAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.ui.news 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.databinding.DataBindingUtil 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.nuhkoca.offlineapp.BR 9 | import com.nuhkoca.offlineapp.base.BaseAdapter 10 | import com.nuhkoca.offlineapp.data.local.entity.News 11 | import com.nuhkoca.offlineapp.databinding.NewsItemLayoutBinding 12 | 13 | class NewsAdapter : BaseAdapter() { 14 | 15 | private var mNews: List? = null 16 | 17 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsViewHolder { 18 | val context = parent.context 19 | val inflater = LayoutInflater.from(context) 20 | 21 | val newsItemLayoutBinding = NewsItemLayoutBinding.inflate(inflater, parent, false) 22 | return NewsViewHolder(newsItemLayoutBinding.root) 23 | } 24 | 25 | override fun onBindViewHolder(holder: NewsViewHolder, position: Int) { 26 | val news = mNews?.get(position) 27 | 28 | news?.let { 29 | holder.bindTo(it) 30 | } 31 | } 32 | 33 | @Suppress("UNCHECKED_CAST") 34 | override fun setData(data: List) { 35 | mNews = data as List 36 | notifyDataSetChanged() 37 | } 38 | 39 | override fun getItemCount(): Int { 40 | return if (mNews == null) 0 else mNews!!.size 41 | } 42 | 43 | inner class NewsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 44 | 45 | private val mNewsItemLayoutBinding: NewsItemLayoutBinding? = DataBindingUtil.getBinding(itemView) 46 | 47 | fun bindTo(news: News) { 48 | mNewsItemLayoutBinding!!.setVariable(BR.news, news) 49 | mNewsItemLayoutBinding.executePendingBindings() 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/ui/news/NewsUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.ui.news 2 | 3 | import com.nuhkoca.offlineapp.data.local.entity.News 4 | import com.nuhkoca.offlineapp.repository.NewsRepository 5 | import com.nuhkoca.offlineapp.repository.Resource 6 | import com.nuhkoca.offlineapp.repository.UseCaseLiveData 7 | 8 | import javax.inject.Inject 9 | import androidx.lifecycle.LiveData 10 | import io.reactivex.Single 11 | 12 | /** 13 | * A [UseCaseLiveData] pattern for playable content 14 | * 15 | * @author nuhkoca 16 | */ 17 | class NewsUseCase @Inject 18 | internal constructor(newsRepository: NewsRepository) : UseCaseLiveData>, UseCaseLiveData.None>(newsRepository) { 19 | 20 | /** 21 | * Build use case with the power of RxJava 22 | * 23 | * @param params represents params to be passed 24 | * @return a [Single] response 25 | */ 26 | override fun buildUseCaseObservable(params: None?): LiveData>> { 27 | return getNewsRepository().loadNews() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/ui/news/NewsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.ui.news 2 | 3 | import com.nuhkoca.offlineapp.data.local.entity.News 4 | import com.nuhkoca.offlineapp.repository.Resource 5 | 6 | import javax.inject.Inject 7 | import androidx.lifecycle.LiveData 8 | import androidx.lifecycle.ViewModel 9 | import com.nuhkoca.offlineapp.repository.UseCaseLiveData 10 | 11 | class NewsViewModel @Inject 12 | internal constructor(newsUseCase: NewsUseCase) : ViewModel() { 13 | val news: LiveData>> = newsUseCase.execute(UseCaseLiveData.None()) 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/ui/splash/SplashActivity.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.ui.splash 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.os.Handler 6 | 7 | import com.nuhkoca.offlineapp.ui.news.NewsActivity 8 | 9 | import dagger.android.support.DaggerAppCompatActivity 10 | 11 | class SplashActivity : DaggerAppCompatActivity() { 12 | 13 | override fun onCreate(savedInstanceState: Bundle?) { 14 | super.onCreate(savedInstanceState) 15 | Handler().postDelayed({ startActivity(Intent(this@SplashActivity, NewsActivity::class.java)) }, 1500) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/util/RateLimiter.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.util 2 | 3 | import android.os.SystemClock 4 | import android.util.ArrayMap 5 | 6 | import java.util.concurrent.TimeUnit 7 | 8 | /** 9 | * Utility class that decides whether we should fetch some data or not. 10 | */ 11 | class RateLimiter(timeout: Int, timeUnit: TimeUnit) { 12 | private val timestamps = ArrayMap() 13 | private val timeout = timeUnit.toMillis(timeout.toLong()) 14 | 15 | @Synchronized 16 | fun shouldFetch(key: KEY): Boolean { 17 | val lastFetched = timestamps[key] 18 | val now = now() 19 | if (lastFetched == null) { 20 | timestamps[key] = now 21 | return true 22 | } 23 | if (now - lastFetched > timeout) { 24 | timestamps[key] = now 25 | return true 26 | } 27 | return false 28 | } 29 | 30 | private fun now() = SystemClock.uptimeMillis() 31 | 32 | @Synchronized 33 | fun reset(key: KEY) { 34 | timestamps.remove(key) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/util/ext/ViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.util.ext 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.fragment.app.FragmentActivity 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.ViewModelProvider 7 | import androidx.lifecycle.ViewModelProviders 8 | 9 | inline fun ViewModelProvider.Factory.get(fragment: Fragment): T = 10 | ViewModelProviders.of(fragment, this)[T::class.java] 11 | 12 | inline fun ViewModelProvider.Factory.get(fragmentActivity: FragmentActivity): T = 13 | ViewModelProviders.of(fragmentActivity, this)[T::class.java] 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/nuhkoca/offlineapp/vm/NewsViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp.vm 2 | 3 | 4 | import javax.inject.Inject 5 | import javax.inject.Provider 6 | import javax.inject.Singleton 7 | import androidx.lifecycle.ViewModel 8 | import androidx.lifecycle.ViewModelProvider 9 | 10 | /** 11 | * A [ViewModelProvider.Factory] that is injected onto each [ViewModel] by [NewsViewModelFactory]. 12 | */ 13 | @Suppress("UNCHECKED_CAST") 14 | @Singleton 15 | class NewsViewModelFactory @Inject constructor(private val viewModels: MutableMap, Provider>) : ViewModelProvider.Factory { 16 | 17 | override fun create(modelClass: Class): T = viewModels[modelClass]?.get() as T 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /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/splash_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/splash_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_news.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 32 | 33 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/res/layout/news_item_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 10 | 11 | 12 | 15 | 16 | 20 | 21 | 34 | 35 | 49 | 50 | 63 | 64 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /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/nuhkoca/AndroidRecommendedArchitecture/9e40377f86c9cf3b10e659fedb82e94b0de0a724/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuhkoca/AndroidRecommendedArchitecture/9e40377f86c9cf3b10e659fedb82e94b0de0a724/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuhkoca/AndroidRecommendedArchitecture/9e40377f86c9cf3b10e659fedb82e94b0de0a724/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuhkoca/AndroidRecommendedArchitecture/9e40377f86c9cf3b10e659fedb82e94b0de0a724/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuhkoca/AndroidRecommendedArchitecture/9e40377f86c9cf3b10e659fedb82e94b0de0a724/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuhkoca/AndroidRecommendedArchitecture/9e40377f86c9cf3b10e659fedb82e94b0de0a724/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuhkoca/AndroidRecommendedArchitecture/9e40377f86c9cf3b10e659fedb82e94b0de0a724/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuhkoca/AndroidRecommendedArchitecture/9e40377f86c9cf3b10e659fedb82e94b0de0a724/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuhkoca/AndroidRecommendedArchitecture/9e40377f86c9cf3b10e659fedb82e94b0de0a724/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuhkoca/AndroidRecommendedArchitecture/9e40377f86c9cf3b10e659fedb82e94b0de0a724/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuhkoca/AndroidRecommendedArchitecture/9e40377f86c9cf3b10e659fedb82e94b0de0a724/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuhkoca/AndroidRecommendedArchitecture/9e40377f86c9cf3b10e659fedb82e94b0de0a724/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuhkoca/AndroidRecommendedArchitecture/9e40377f86c9cf3b10e659fedb82e94b0de0a724/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuhkoca/AndroidRecommendedArchitecture/9e40377f86c9cf3b10e659fedb82e94b0de0a724/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuhkoca/AndroidRecommendedArchitecture/9e40377f86c9cf3b10e659fedb82e94b0de0a724/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #e93f33 4 | #D32F2F 5 | #40C4FF 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | OfflineApp 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/test/java/com/nuhkoca/offlineapp/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.nuhkoca.offlineapp; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '2.1.21' 5 | ext.gradle_version = '8.10.1' 6 | repositories { 7 | google() 8 | jcenter() 9 | } 10 | dependencies { 11 | classpath "com.android.tools.build:gradle:$gradle_version" 12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 13 | 14 | // NOTE: Do not place your application dependencies here; they belong 15 | // in the individual module build.gradle files 16 | } 17 | } 18 | 19 | allprojects { 20 | repositories { 21 | google() 22 | jcenter() 23 | } 24 | } 25 | 26 | task clean(type: Delete) { 27 | delete rootProject.buildDir 28 | } 29 | -------------------------------------------------------------------------------- /buildSrc/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /.gradle -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | jcenter() 7 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/Depedencies.kt: -------------------------------------------------------------------------------- 1 | object Versions { 2 | const val compile_sdk = 28 3 | const val min_sdk = 21 4 | const val target_sdk = 28 5 | 6 | const val version_code = 1 7 | const val version_name = "1.0" 8 | 9 | const val x = "1.7.1" 10 | const val material = "1.12.0" 11 | const val constraint_layout = "2.2.1" 12 | const val room = "2.7.1" 13 | const val dagger = "2.56.2" 14 | const val paging = "3.3.6" 15 | const val livedata = "2.9.1" 16 | const val rxjava = "2.2.21" 17 | const val rxandroid = "2.1.1" 18 | const val jetbrains = "26.0.2" 19 | const val retrofit = "2.12.0" 20 | const val okhttp = "4.12.0" 21 | const val glide = "4.16.0" 22 | const val kotlin_version = "2.1.21" 23 | const val databinding = "8.10.1" 24 | } 25 | 26 | object Libs { 27 | val x = "androidx.appcompat:appcompat:${Versions.x}" 28 | val material = "com.google.android.material:material:${Versions.material}" 29 | val recyclerview = "androidx.recyclerview:recyclerview:${Versions.material}" 30 | val cardview = "androidx.cardview:cardview:${Versions.material}" 31 | val constrain_layout = "androidx.constraintlayout:constraintlayout:${Versions.constraint_layout}" 32 | val room = "androidx.room:room-common:${Versions.room}" 33 | val room_runtime = "androidx.room:room-runtime:${Versions.room}" 34 | val room_compiler = "androidx.room:room-compiler:${Versions.room}" 35 | val paging = "androidx.paging:paging-common:${Versions.paging}" 36 | val paging_runtime = "androidx.paging:paging-runtime:${Versions.paging}" 37 | val lifecycle = "androidx.lifecycle:lifecycle-extensions:${Versions.livedata}" 38 | val lifecycle_common = "androidx.lifecycle:lifecycle-common-java8:${Versions.livedata}" 39 | val dagger = "com.google.dagger:dagger:${Versions.dagger}" 40 | val dagger_android = "com.google.dagger:dagger-android:${Versions.dagger}" 41 | val dagger_support = "com.google.dagger:dagger-android-support:${Versions.dagger}" 42 | val dagger_processor = "com.google.dagger:dagger-android-processor:${Versions.dagger}" 43 | val dagger_compiler = "com.google.dagger:dagger-compiler:${Versions.dagger}" 44 | val rxjava_android = "io.reactivex.rxjava2:rxandroid:${Versions.rxandroid}" 45 | val rxjava = "io.reactivex.rxjava2:rxjava:${Versions.rxjava}" 46 | val jetbrains = "org.jetbrains:annotations:${Versions.jetbrains}" 47 | val retrofit_adapter = "com.squareup.retrofit2:adapter-rxjava2:${Versions.retrofit}" 48 | val retrofit = "com.squareup.retrofit2:retrofit:${Versions.retrofit}" 49 | val gson = "com.squareup.retrofit2:converter-gson:${Versions.retrofit}" 50 | val logging = "com.squareup.okhttp3:logging-interceptor:${Versions.okhttp}" 51 | val glide = "com.github.bumptech.glide:glide:${Versions.glide}" 52 | val glide_compiler = "com.github.bumptech.glide:compiler:${Versions.glide}" 53 | val okhttp = "com.github.bumptech.glide:okhttp3-integration:${Versions.glide}" 54 | val kotlin = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.kotlin_version}" 55 | val databinding = "androidx.databinding:databinding-compiler:${Versions.databinding}" 56 | } 57 | -------------------------------------------------------------------------------- /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=-Xmx1536m 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=true 20 | 21 | android.databinding.enableV2=true 22 | -Pandroid.databinding.enableV2=true 23 | 24 | BASE_URL = "https://newsapi.org/v2/" 25 | API_KEY = "api-key" 26 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuhkoca/AndroidRecommendedArchitecture/9e40377f86c9cf3b10e659fedb82e94b0de0a724/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH="\\\"\\\"" 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "requiredStatusChecks": null, 6 | "rebaseWhen": "conflicted", 7 | "minor": { 8 | "automerge": true 9 | }, 10 | "patch": { 11 | "automerge": true 12 | }, 13 | "pin": { 14 | "automerge": true 15 | }, 16 | "digest": { 17 | "automerge": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------