Feature/styling (#36)

* Working with material styles

* Replaced deprecated ViewModelProviders.of and applied base styles with material components

* Various code style fixes and styles.xml improvements
This commit is contained in:
Melih Aksoy
2019-09-13 14:09:28 +02:00
committed by GitHub
parent 5a10edce17
commit 99a2abf050
58 changed files with 523 additions and 464 deletions

View File

@@ -1,12 +1,12 @@
# Rocket Science # Android-Kotlin-Modulerized-CleanArchitecture
[![Actions](https://github.com/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture/workflows/Android%20CI/badge.svg)](https://github.com/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture/actions) [![CircleCI](https://circleci.com/gh/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture/tree/master.svg?style=svg)](https://circleci.com/gh/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture/tree/master) [![codebeat badge](https://codebeat.co/badges/542fb08a-b3cc-4ff8-b1bb-35a66932f12f)](https://codebeat.co/projects/github-com-melihaksoy-rocketscience-master) [![codecov](https://codecov.io/gh/melihaksoy/RocketScience/branch/master/graph/badge.svg?token=pXPKpV5dz6)](https://codecov.io/gh/melihaksoy/RocketScience) [![Awesome Kotlin Badge](https://kotlin.link/awesome-kotlin.svg)](https://github.com/KotlinBy/awesome-kotlin) [![Actions](https://github.com/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture/workflows/Android%20CI/badge.svg)](https://github.com/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture/actions) [![CircleCI](https://circleci.com/gh/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture/tree/master.svg?style=svg)](https://circleci.com/gh/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture/tree/master) [![codebeat badge](https://codebeat.co/badges/542fb08a-b3cc-4ff8-b1bb-35a66932f12f)](https://codebeat.co/projects/github-com-melihaksoy-rocketscience-master) [![codecov](https://codecov.io/gh/melihaksoy/RocketScience/branch/master/graph/badge.svg?token=pXPKpV5dz6)](https://codecov.io/gh/melihaksoy/RocketScience) [![Awesome Kotlin Badge](https://kotlin.link/awesome-kotlin.svg)](https://github.com/KotlinBy/awesome-kotlin)
RocketScience is a prototype application tries to serve an example for modularization & clean architecture in Android. This is a prototype application tries to serve an example for modularization & clean architecture in Android.
While there are many blogs about good practices, it's hard to come by a complete example that merges these different popular topics & approaches. While there are many blogs about good practices, it's hard to come by a complete example that merges these different popular topics & approaches.
RocketScience takes popular approaches and libraries to create an example on how to actually bind these components with each other. This project takes popular approaches and libraries to create an example on how to actually bind these components with each other.
I'll soon be writing small series about roadmap, challanges, alternatives and try - fails I've encountered during development in [Medium](https://medium.com/@aksoymelihcan). I'll soon be writing small series about roadmap, challanges, alternatives and try - fails I've encountered during development in [Medium](https://medium.com/@aksoymelihcan).

View File

@@ -4,7 +4,6 @@ apply plugin: 'kotlin-kapt'
apply from: "$rootProject.projectDir/scripts/default_android_config.gradle" apply from: "$rootProject.projectDir/scripts/default_android_config.gradle"
apply from: "$rootProject.projectDir/scripts/sources.gradle" apply from: "$rootProject.projectDir/scripts/sources.gradle"
apply from: "$rootProject.projectDir/scripts/flavors.gradle"
android { android {
defaultConfig { defaultConfig {
@@ -26,7 +25,6 @@ dependencies {
implementation project(':features:launches') implementation project(':features:launches')
implementation project(':features:detail') implementation project(':features:detail')
implementation libraries.coroutines
implementation libraries.navigation implementation libraries.navigation
debugImplementation libraries.leakCanary debugImplementation libraries.leakCanary
@@ -37,6 +35,7 @@ dependencies {
compileOnly libraries.retrofit compileOnly libraries.retrofit
compileOnly libraries.room compileOnly libraries.room
compileOnly libraries.paging compileOnly libraries.paging
compileOnly libraries.swipeRefreshLayout
// Need for proper renders in xml previews // Need for proper renders in xml previews
compileOnly libraries.constraintLayout compileOnly libraries.constraintLayout

View File

@@ -1,24 +1,22 @@
package com.melih.rocketscience package com.melih.rocketscience
import android.os.Bundle import android.os.Bundle
import androidx.databinding.DataBindingUtil import androidx.appcompat.widget.Toolbar
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.navigation.ui.NavigationUI import androidx.navigation.ui.NavigationUI
import com.melih.rocketscience.databinding.MainActivityBinding
import dagger.android.support.DaggerAppCompatActivity import dagger.android.support.DaggerAppCompatActivity
class MainActivity : DaggerAppCompatActivity() { class MainActivity : DaggerAppCompatActivity() {
private lateinit var binding: MainActivityBinding
private lateinit var navController: NavController private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main) setContentView(R.layout.activity_main)
navController = findNavController(R.id.nav_host_fragment) navController = findNavController(R.id.nav_host_fragment)
NavigationUI.setupWithNavController(binding.toolbar, navController) NavigationUI.setupWithNavController(findViewById<Toolbar>(R.id.toolbar), navController)
} }
override fun onSupportNavigateUp(): Boolean { override fun onSupportNavigateUp(): Boolean {

View File

@@ -8,8 +8,10 @@ import dagger.android.AndroidInjector
@AppScope @AppScope
@Component( @Component(
modules = [AndroidInjectionModule::class, modules = [
AppModule::class], AndroidInjectionModule::class,
AppModule::class
],
dependencies = [CoreComponent::class] dependencies = [CoreComponent::class]
) )

View File

@@ -1,27 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@null"
android:orientation="vertical">
<data class="MainActivityBinding" /> <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?android:actionBarSize" />
<LinearLayout <fragment
android:layout_width="match_parent" android:id="@+id/nav_host_fragment"
android:layout_height="match_parent" android:name="androidx.navigation.fragment.NavHostFragment"
android:orientation="vertical"> android:layout_width="match_parent"
android:layout_height="match_parent"
<androidx.appcompat.widget.Toolbar app:defaultNavHost="true"
android:id="@+id/toolbar" app:navGraph="@navigation/nav_main" />
android:layout_width="match_parent" </LinearLayout>
android:layout_height="?android:actionBarSize"
android:background="@color/colorPrimary" />
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_main" />
</LinearLayout>
</layout>

View File

@@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.3.41' ext.kotlin_version = '1.3.50'
ext.nav_version = '2.2.0-alpha01' ext.nav_version = '2.2.0-alpha01'
repositories { repositories {
@@ -12,7 +12,7 @@ buildscript {
classpath 'com.android.tools.build:gradle:3.5.0' classpath 'com.android.tools.build:gradle:3.5.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:0.9.18" classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:0.9.18"
classpath "de.mannodermaus.gradle.plugins:android-junit5:1.5.0.0" classpath "de.mannodermaus.gradle.plugins:android-junit5:1.5.1.0"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files

View File

@@ -4,7 +4,6 @@ apply plugin: 'kotlin-kapt'
apply from: "$rootProject.projectDir/scripts/default_android_config.gradle" apply from: "$rootProject.projectDir/scripts/default_android_config.gradle"
apply from: "$rootProject.projectDir/scripts/sources.gradle" apply from: "$rootProject.projectDir/scripts/sources.gradle"
apply from: "$rootProject.projectDir/scripts/flavors.gradle"
android { android {
dataBinding { dataBinding {
@@ -23,6 +22,7 @@ dependencies {
implementation libraries.liveDataKTX implementation libraries.liveDataKTX
implementation libraries.navigation implementation libraries.navigation
implementation libraries.picasso implementation libraries.picasso
implementation libraries.material
testImplementation testLibraries.jUnitApi testImplementation testLibraries.jUnitApi
testImplementation testLibraries.mockk testImplementation testLibraries.mockk

View File

@@ -14,7 +14,7 @@ import javax.inject.Inject
* Parent of fragments which has injections. Aim is to seperate [BaseFragment] functionality for fragments which * Parent of fragments which has injections. Aim is to seperate [BaseFragment] functionality for fragments which
* won't need any injection. * won't need any injection.
* *
* Note that fragments that extends from [BaseDaggerFragment] should contribute android injector. * Note that fragments that extends from [BaseDaggerFragment] should contribute their injector.
* *
* This class provides [viewModelFactory] which serves as factory for view models * This class provides [viewModelFactory] which serves as factory for view models
* in the project. It's injected by map of view models that this app is serving. Check [ViewModelFactory] * in the project. It's injected by map of view models that this app is serving. Check [ViewModelFactory]
@@ -22,16 +22,16 @@ import javax.inject.Inject
*/ */
abstract class BaseDaggerFragment<T : ViewDataBinding> : BaseFragment<T>(), HasAndroidInjector { abstract class BaseDaggerFragment<T : ViewDataBinding> : BaseFragment<T>(), HasAndroidInjector {
// region Properties //region Properties
@get:Inject @get:Inject
internal var androidInjector: DispatchingAndroidInjector<Any>? = null internal var androidInjector: DispatchingAndroidInjector<Any>? = null
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
// endregion //endregion
// region Functions //region Functions
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
AndroidSupportInjection.inject(this) AndroidSupportInjection.inject(this)
@@ -39,5 +39,5 @@ abstract class BaseDaggerFragment<T : ViewDataBinding> : BaseFragment<T>(), HasA
} }
override fun androidInjector(): AndroidInjector<Any>? = androidInjector override fun androidInjector(): AndroidInjector<Any>? = androidInjector
// endregion //endregion
} }

View File

@@ -20,12 +20,18 @@ import com.melih.repository.interactors.base.Reason
*/ */
abstract class BaseFragment<T : ViewDataBinding> : Fragment() { abstract class BaseFragment<T : ViewDataBinding> : Fragment() {
// region Properties //region Abstractions
@LayoutRes
abstract fun getLayoutId(): Int
//endregion
//region Properties
protected lateinit var binding: T protected lateinit var binding: T
// endregion //endregion
// region Functions //region Functions
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@@ -46,8 +52,5 @@ abstract class BaseFragment<T : ViewDataBinding> : Fragment() {
block() block()
}.show() }.show()
} }
//endregion
@LayoutRes
abstract fun getLayoutId(): Int
// endregion
} }

View File

@@ -37,12 +37,12 @@ const val INITIAL_PAGE = 0
@UseExperimental(ExperimentalCoroutinesApi::class) @UseExperimental(ExperimentalCoroutinesApi::class)
abstract class BasePagingDataSource<T> : PageKeyedDataSource<Int, T>() { abstract class BasePagingDataSource<T> : PageKeyedDataSource<Int, T>() {
// region Abstractions //region Abstractions
abstract fun loadDataForPage(page: Int): Flow<Result<List<T>>> // Load next page(s) abstract fun loadDataForPage(page: Int): Flow<Result<List<T>>> // Load next page(s)
// endregion //endregion
// region Properties //region Properties
private val _stateData = MutableLiveData<State>() private val _stateData = MutableLiveData<State>()
private val _reasonData = MutableLiveData<Reason>() private val _reasonData = MutableLiveData<Reason>()
@@ -59,9 +59,9 @@ abstract class BasePagingDataSource<T> : PageKeyedDataSource<Int, T>() {
*/ */
val reasonData: LiveData<Reason> val reasonData: LiveData<Reason>
get() = _reasonData get() = _reasonData
// endregion //endregion
// region Functions //region Functions
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, T>) { override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, T>) {
// Looping through channel as we'll receive any state, error or data here // Looping through channel as we'll receive any state, error or data here
@@ -136,5 +136,5 @@ abstract class BasePagingDataSource<T> : PageKeyedDataSource<Int, T>() {
coroutineScope.cancel() coroutineScope.cancel()
super.invalidate() super.invalidate()
} }
// endregion //endregion
} }

View File

@@ -16,20 +16,20 @@ import androidx.paging.DataSource
*/ */
abstract class BasePagingFactory<T> : DataSource.Factory<Int, T>() { abstract class BasePagingFactory<T> : DataSource.Factory<Int, T>() {
// region Abstractions //region Abstractions
abstract fun createSource(): BasePagingDataSource<T> abstract fun createSource(): BasePagingDataSource<T>
// endregion //endregion
// region Properties //region Properties
private val _currentSource = MutableLiveData<BasePagingDataSource<T>>() private val _currentSource = MutableLiveData<BasePagingDataSource<T>>()
val currentSource: LiveData<BasePagingDataSource<T>> val currentSource: LiveData<BasePagingDataSource<T>>
get() = _currentSource get() = _currentSource
// endregion //endregion
// region Functions //region Functions
override fun create(): DataSource<Int, T> = createSource().apply { _currentSource.postValue(this) } override fun create(): DataSource<Int, T> = createSource().apply { _currentSource.postValue(this) }
@@ -38,5 +38,5 @@ abstract class BasePagingFactory<T> : DataSource.Factory<Int, T>() {
* by calling [BasePagingDataSource.invalidate] * by calling [BasePagingDataSource.invalidate]
*/ */
fun invalidateDataSource() = currentSource.value?.invalidate() fun invalidateDataSource() = currentSource.value?.invalidate()
// endregion //endregion
} }

View File

@@ -15,6 +15,8 @@ abstract class BasePagingListAdapter<T>(
private val clickListener: (T) -> Unit private val clickListener: (T) -> Unit
) : PagedListAdapter<T, BaseViewHolder<T>>(callback) { ) : PagedListAdapter<T, BaseViewHolder<T>>(callback) {
//region Abstractions
/** /**
* This method will be called to create view holder to obfuscate layout inflation creation / process * This method will be called to create view holder to obfuscate layout inflation creation / process
* *
@@ -27,6 +29,9 @@ abstract class BasePagingListAdapter<T>(
parent: ViewGroup, parent: ViewGroup,
viewType: Int viewType: Int
): BaseViewHolder<T> ): BaseViewHolder<T>
//endregion
//region Functions
/** /**
* [createViewHolder] will provide holders, no need to override this * [createViewHolder] will provide holders, no need to override this
@@ -51,6 +56,7 @@ abstract class BasePagingListAdapter<T>(
holder.bind(item) holder.bind(item)
} }
} }
//endregion
} }
/** /**
@@ -58,6 +64,8 @@ abstract class BasePagingListAdapter<T>(
*/ */
abstract class BaseViewHolder<T>(binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) { abstract class BaseViewHolder<T>(binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) {
//region Functions
/** /**
* Items are delivered to [bind] via [BaseListAdapter.onBindViewHolder] * Items are delivered to [bind] via [BaseListAdapter.onBindViewHolder]
* *
@@ -65,4 +73,5 @@ abstract class BaseViewHolder<T>(binding: ViewDataBinding) : RecyclerView.ViewHo
* @param position position from adapter * @param position position from adapter
*/ */
abstract fun bind(item: T) abstract fun bind(item: T)
//endregion
} }

View File

@@ -1,7 +1,7 @@
package com.melih.core.base.viewmodel package com.melih.core.base.viewmodel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations.switchMap
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.paging.PagedList import androidx.paging.PagedList
import androidx.paging.toLiveData import androidx.paging.toLiveData
@@ -20,19 +20,19 @@ import com.melih.repository.interactors.base.State
*/ */
abstract class BasePagingViewModel<T> : ViewModel() { abstract class BasePagingViewModel<T> : ViewModel() {
// region Abstractions //region Abstractions
abstract val factory: BasePagingFactory<T> abstract val factory: BasePagingFactory<T>
abstract val config: PagedList.Config abstract val config: PagedList.Config
// endregion //endregion
// region Properties //region Properties
/** /**
* Observe [stateData] to get notified of state of data * Observe [stateData] to get notified of state of data
*/ */
val stateData: LiveData<State> by lazy { val stateData: LiveData<State> by lazy {
Transformations.switchMap(factory.currentSource) { switchMap(factory.currentSource) {
it.stateData it.stateData
} }
} }
@@ -41,7 +41,7 @@ abstract class BasePagingViewModel<T> : ViewModel() {
* Observe [errorData] to get notified if an error occurs * Observe [errorData] to get notified if an error occurs
*/ */
val errorData: LiveData<Reason> by lazy { val errorData: LiveData<Reason> by lazy {
Transformations.switchMap(factory.currentSource) { switchMap(factory.currentSource) {
it.reasonData it.reasonData
} }
} }
@@ -52,9 +52,9 @@ abstract class BasePagingViewModel<T> : ViewModel() {
val pagedList: LiveData<PagedList<T>> by lazy { val pagedList: LiveData<PagedList<T>> by lazy {
factory.toLiveData(config) factory.toLiveData(config)
} }
// endregion //endregion
// region Functions //region Functions
fun refresh() { fun refresh() {
factory.currentSource.value?.invalidate() factory.currentSource.value?.invalidate()
@@ -66,5 +66,5 @@ abstract class BasePagingViewModel<T> : ViewModel() {
fun retry() { fun retry() {
factory.currentSource.value?.invalidate() factory.currentSource.value?.invalidate()
} }
// endregion //endregion
} }

View File

@@ -15,10 +15,10 @@ import kotlinx.coroutines.launch
*/ */
abstract class BaseViewModel<T> : ViewModel() { abstract class BaseViewModel<T> : ViewModel() {
// region Abstractions //region Abstractions
abstract suspend fun loadData() abstract suspend fun loadData()
// endregion //endregion
init { init {
viewModelScope.launch { viewModelScope.launch {
@@ -26,7 +26,7 @@ abstract class BaseViewModel<T> : ViewModel() {
} }
} }
// region Properties //region Properties
private val _successData = MutableLiveData<T>() private val _successData = MutableLiveData<T>()
private val _stateData = MutableLiveData<State>() private val _stateData = MutableLiveData<State>()
@@ -49,9 +49,9 @@ abstract class BaseViewModel<T> : ViewModel() {
*/ */
val errorData: LiveData<Reason> val errorData: LiveData<Reason>
get() = _errorData get() = _errorData
// endregion //endregion
// region Functions //region Functions
/** /**
* Default success handler which assigns given [data] to [successData] * Default success handler which assigns given [data] to [successData]
@@ -97,5 +97,5 @@ abstract class BaseViewModel<T> : ViewModel() {
loadData() loadData()
} }
} }
// endregion //endregion
} }

View File

@@ -0,0 +1,27 @@
package com.melih.core.extensions
import androidx.recyclerview.widget.DiffUtil
/**
* Get [diff callback][DiffUtil.ItemCallback] for given type based on provided checker.
* It uses [itemCheck] for both [DiffUtil.ItemCallback.areItemsTheSame] and [DiffUtil.ItemCallback.areContentsTheSame].
*/
inline fun <T> createDiffCallback(crossinline itemCheck: (oldItem: T, newItem: T) -> Boolean) = createDiffCallback(itemCheck, itemCheck)
/**
* Get [diff callback][DiffUtil.ItemCallback] for given type based on provided checker
*/
inline fun <T> createDiffCallback(
crossinline itemCheck: (oldItem: T, newItem: T) -> Boolean,
crossinline contentCheck: (oldItem: T, newItem: T) -> Boolean
) = object : DiffUtil.ItemCallback<T>() {
//region Functions
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean =
itemCheck(oldItem, newItem)
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean =
contentCheck(oldItem, newItem)
//endregion
}

View File

@@ -3,9 +3,6 @@ package com.melih.core.extensions
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
/** /**
* Reduces required boilerplate code to observe a live data * Reduces required boilerplate code to observe a live data
@@ -16,13 +13,3 @@ import androidx.lifecycle.ViewModelProviders
fun <T> Fragment.observe(data: LiveData<T>, block: (T) -> Unit) { fun <T> Fragment.observe(data: LiveData<T>, block: (T) -> Unit) {
data.observe(this, Observer(block)) data.observe(this, Observer(block))
} }
/**
* Method for getting viewModel from factory and run a block over it if required for easy access
*
* crossinline for unwanted returns
*/
inline fun <reified T : ViewModel> ViewModelProvider.Factory.createFor(
fragment: Fragment,
crossinline block: T.() -> Unit = {}
): T = ViewModelProviders.of(fragment, this)[T::class.java].apply(block)

View File

@@ -1,14 +0,0 @@
package com.melih.core.extensions
import androidx.recyclerview.widget.DiffUtil
/**
* Get [diff callback][DiffUtil.ItemCallback] for given type based on provided checker
*/
inline fun <T> getDiffCallbackForType(crossinline itemCheck: (oldItem: T, newItem: T) -> Boolean) = object : DiffUtil.ItemCallback<T>() {
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean =
itemCheck(oldItem, newItem)
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean =
itemCheck(oldItem, newItem)
}

View File

@@ -1,7 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="colorPrimary">#008577</color> <color name="primaryColor">#f9a825</color>
<color name="colorPrimaryDark">#00574B</color> <color name="primaryLightColor">#ffd95a</color>
<color name="colorAccent">#D81B60</color> <color name="primaryDarkColor">#c17900</color>
<color name="lightGray">#8F8F8F</color> <color name="secondaryColor">#757575</color>
<color name="secondaryLightColor">#a4a4a4</color>
<color name="secondaryDarkColor">#494949</color>
<color name="primaryTextColor">#000000</color>
<color name="secondaryTextColor">#686868</color>
</resources> </resources>

View File

@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<dimen name="padding_standard">8dp</dimen> <dimen name="padding_standard">8dp</dimen>
<dimen name="padding_large">16dp</dimen>
<dimen name="corner_radius_light">4dp</dimen>
<dimen name="corner_radius_standard">11dp</dimen> <dimen name="corner_radius_standard">11dp</dimen>
</resources> </resources>

View File

@@ -1,35 +1,40 @@
<resources> <resources>
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<!-- Customize your theme here. --> <item name="colorPrimary">@color/primaryColor</item>
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/primaryDarkColor</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorSecondary">@color/secondaryColor</item>
<item name="colorAccent">@color/colorAccent</item> <item name="colorAccent">@color/secondaryDarkColor</item>
</style>
<!--Styles--> <item name="android:textColorPrimary">@color/primaryTextColor</item>
<item name="android:textColorSecondary">@color/secondaryTextColor</item>
<style name="TitleTextStyle"> <item name="toolbarStyle">@style/AppTheme.ToolbarStyle</item>
<item name="android:singleLine">true</item> <item name="materialCardViewStyle">@style/AppTheme.CardViewStyle</item>
</style> </style>
<style name="ShortDescriptionTextStyle"> <!-- Widgets -->
<item name="android:ellipsize">end</item>
<item name="android:maxLines">@integer/common_max_lines</item>
<item name="android:gravity">center|left</item>
</style>
<style name="DescriptionTextStyle"> <style name="AppTheme.ToolbarStyle" parent="Widget.MaterialComponents.Toolbar">
<item name="android:background">@color/primaryColor</item>
</style>
</style> <style name="AppTheme.CardViewStyle" parent="Widget.MaterialComponents.CardView">
<item name="android:background">@null</item>
</style>
<!--Text appearances--> <!--Styles-->
<style name="AppTheme.BaseTextViewStyle" parent="Widget.MaterialComponents.TextView" />
<style name="TitleTextAppearance" parent="TextAppearance.AppCompat.Title" /> <style name="AppTheme.TextViewStyle.Title" parent="AppTheme.BaseTextViewStyle">
<item name="android:textAppearance">@style/TextAppearance.MaterialComponents.Headline5</item>
<style name="DescriptionTextAppearance" parent="TextAppearance.AppCompat.Body1"> <item name="android:singleLine">true</item>
<item name="android:textColor">@color/lightGray</item> </style>
</style>
<style name="AppTheme.TextViewStyle.Description" parent="AppTheme.BaseTextViewStyle">
<item name="android:ellipsize">end</item>
<item name="android:maxLines">@integer/common_max_lines</item>
<item name="android:textAppearance">@style/TextAppearance.MaterialComponents.Caption</item>
</style>
</resources> </resources>

View File

@@ -12,12 +12,12 @@ import dagger.android.ContributesAndroidInjector
@Module @Module
abstract class DetailContributor { abstract class DetailContributor {
// region Contributes //region Contributes
@ContributesAndroidInjector( @ContributesAndroidInjector(
modules = [DetailFragmentModule::class] modules = [DetailFragmentModule::class]
) )
@DetailFragmentScope @DetailFragmentScope
abstract fun detailFragment(): DetailFragment abstract fun detailFragment(): DetailFragment
// endregion //endregion
} }

View File

@@ -15,13 +15,13 @@ import dagger.multibindings.IntoMap
@Module @Module
abstract class DetailFragmentModule { abstract class DetailFragmentModule {
// region ViewModels //region ViewModels
@Binds @Binds
@IntoMap @IntoMap
@ViewModelKey(DetailViewModel::class) @ViewModelKey(DetailViewModel::class)
abstract fun detailViewModel(detailViewModel: DetailViewModel): ViewModel abstract fun detailViewModel(detailViewModel: DetailViewModel): ViewModel
// endregion //endregion
@Module @Module
companion object { companion object {

View File

@@ -3,21 +3,20 @@ package com.melih.detail.ui
import android.os.Bundle import android.os.Bundle
import android.text.method.ScrollingMovementMethod import android.text.method.ScrollingMovementMethod
import android.view.View import android.view.View
import androidx.fragment.app.viewModels
import com.melih.core.base.lifecycle.BaseDaggerFragment import com.melih.core.base.lifecycle.BaseDaggerFragment
import com.melih.core.extensions.createFor
import com.melih.core.extensions.observe import com.melih.core.extensions.observe
import com.melih.detail.R import com.melih.detail.R
import com.melih.detail.databinding.DetailBinding import com.melih.detail.databinding.DetailBinding
class DetailFragment : BaseDaggerFragment<DetailBinding>() { class DetailFragment : BaseDaggerFragment<DetailBinding>() {
// region Properties //region Properties
private val viewModel: DetailViewModel private val viewModel by viewModels<DetailViewModel> { viewModelFactory }
get() = viewModelFactory.createFor(this) //endregion
// endregion
// region Functions //region Functions
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@@ -34,5 +33,5 @@ class DetailFragment : BaseDaggerFragment<DetailBinding>() {
} }
override fun getLayoutId(): Int = R.layout.fragment_detail override fun getLayoutId(): Int = R.layout.fragment_detail
// endregion //endregion
} }

View File

@@ -1,6 +1,6 @@
package com.melih.detail.ui package com.melih.detail.ui
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations.map
import com.melih.core.base.viewmodel.BaseViewModel import com.melih.core.base.viewmodel.BaseViewModel
import com.melih.repository.entities.LaunchEntity import com.melih.repository.entities.LaunchEntity
import com.melih.repository.interactors.GetLaunchDetails import com.melih.repository.interactors.GetLaunchDetails
@@ -13,13 +13,13 @@ class DetailViewModel @Inject constructor(
private val getLaunchDetailsParams: GetLaunchDetails.Params private val getLaunchDetailsParams: GetLaunchDetails.Params
) : BaseViewModel<LaunchEntity>() { ) : BaseViewModel<LaunchEntity>() {
// region Properties //region Properties
val rocketName = Transformations.map(successData) { val rocketName = map(successData) {
it.rocket.name it.rocket.name
} }
val description = Transformations.map(successData) { val description = map(successData) {
if (it.missions.isEmpty()) { if (it.missions.isEmpty()) {
"" ""
} else { } else {
@@ -27,12 +27,12 @@ class DetailViewModel @Inject constructor(
} }
} }
val imageUrl = Transformations.map(successData) { val imageUrl = map(successData) {
it.rocket.imageURL it.rocket.imageURL
} }
// endregion //endregion
// region Functions //region Functions
/** /**
* Triggering interactor in view model scope * Triggering interactor in view model scope
@@ -42,5 +42,5 @@ class DetailViewModel @Inject constructor(
it.handle(::handleState, ::handleFailure, ::handleSuccess) it.handle(::handleState, ::handleFailure, ::handleSuccess)
} }
} }
// endregion //endregion
} }

View File

@@ -1,75 +1,69 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout <layout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<data class="DetailBinding"> <data class="DetailBinding">
<variable <variable
name="viewModel" name="viewModel"
type="com.melih.detail.ui.DetailViewModel" /> type="com.melih.detail.ui.DetailViewModel" />
</data> </data>
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout <ImageView
android:layout_width="match_parent" android:id="@+id/imgRocket"
android:layout_height="match_parent"> imageUrl="@{viewModel.imageUrl}"
android:layout_width="0dp"
android:layout_height="220dp"
android:layout_marginStart="@dimen/padding_standard"
android:layout_marginLeft="@dimen/padding_standard"
android:layout_marginTop="@dimen/padding_standard"
android:layout_marginEnd="@dimen/padding_standard"
android:layout_marginRight="@dimen/padding_standard"
android:contentDescription="@string/cd_rocket_image"
android:scaleType="centerCrop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars[14]" />
<ImageView <com.google.android.material.textview.MaterialTextView
android:id="@+id/imgRocket" android:id="@+id/tvTitle"
imageUrl="@{viewModel.imageUrl}" style="@style/AppTheme.TextViewStyle.Title"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="220dp" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/padding_standard" android:layout_marginStart="@dimen/padding_standard"
android:layout_marginLeft="@dimen/padding_standard" android:layout_marginLeft="@dimen/padding_standard"
android:layout_marginTop="@dimen/padding_standard" android:layout_marginTop="@dimen/padding_standard"
android:layout_marginEnd="@dimen/padding_standard" android:layout_marginEnd="@dimen/padding_standard"
android:layout_marginRight="@dimen/padding_standard" android:layout_marginRight="@dimen/padding_standard"
android:contentDescription="@string/cd_rocket_image" android:text="@{viewModel.rocketName}"
android:scaleType="centerCrop" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/imgRocket"
app:layout_constraintTop_toTopOf="parent" tools:text="@sample/launches.json/launches/name" />
tools:srcCompat="@tools:sample/avatars[14]" />
<TextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/tvTitle" android:id="@+id/tvDescription"
style="@style/TitleTextStyle" android:layout_width="0dp"
android:layout_width="0dp" android:layout_height="0dp"
android:layout_height="wrap_content" android:layout_marginStart="@dimen/padding_standard"
android:layout_marginStart="@dimen/padding_standard" android:layout_marginLeft="@dimen/padding_standard"
android:layout_marginLeft="@dimen/padding_standard" android:layout_marginTop="@dimen/padding_standard"
android:layout_marginTop="@dimen/padding_standard" android:layout_marginEnd="@dimen/padding_standard"
android:layout_marginEnd="@dimen/padding_standard" android:layout_marginRight="@dimen/padding_standard"
android:layout_marginRight="@dimen/padding_standard" android:layout_marginBottom="@dimen/padding_standard"
android:text="@{viewModel.rocketName}" android:text="@{viewModel.description}"
android:textAppearance="@style/TitleTextAppearance" android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imgRocket" app:layout_constraintStart_toStartOf="parent"
tools:text="@sample/launches.json/launches/name" /> app:layout_constraintTop_toBottomOf="@+id/tvTitle"
tools:text="@sample/launches.json/launches/missions/description" />
<TextView </androidx.constraintlayout.widget.ConstraintLayout>
android:id="@+id/tvDescription"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="@dimen/padding_standard"
android:layout_marginLeft="@dimen/padding_standard"
android:layout_marginTop="@dimen/padding_standard"
android:layout_marginEnd="@dimen/padding_standard"
android:layout_marginRight="@dimen/padding_standard"
android:layout_marginBottom="@dimen/padding_standard"
android:text="@{viewModel.description}"
android:textAppearance="@style/DescriptionTextAppearance"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
tools:text="@sample/launches.json/launches/missions/description" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout> </layout>

View File

@@ -8,6 +8,7 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation libraries.paging implementation libraries.paging
implementation libraries.swipeRefreshLayout
testImplementation testLibraries.coroutinesTest testImplementation testLibraries.coroutinesTest
} }

View File

@@ -12,12 +12,12 @@ import dagger.android.ContributesAndroidInjector
@Module @Module
abstract class LaunchesContributor { abstract class LaunchesContributor {
// region Contributes //region Contributes
@ContributesAndroidInjector( @ContributesAndroidInjector(
modules = [LaunchesFragmentModule::class] modules = [LaunchesFragmentModule::class]
) )
@LaunchesFragmentScope @LaunchesFragmentScope
abstract fun launchesFragment(): LaunchesFragment abstract fun launchesFragment(): LaunchesFragment
// endregion //endregion
} }

View File

@@ -14,13 +14,13 @@ import dagger.multibindings.IntoMap
@Module @Module
abstract class LaunchesFragmentModule { abstract class LaunchesFragmentModule {
// region ViewModels //region ViewModels
@Binds @Binds
@IntoMap @IntoMap
@ViewModelKey(LaunchesViewModel::class) @ViewModelKey(LaunchesViewModel::class)
abstract fun listViewModel(listViewModel: LaunchesViewModel): ViewModel abstract fun listViewModel(listViewModel: LaunchesViewModel): ViewModel
// endregion //endregion
@Module @Module
companion object { companion object {

View File

@@ -2,29 +2,29 @@ package com.melih.list.ui
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.fragment.app.viewModels
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.melih.core.actions.openDetail import com.melih.core.actions.openDetail
import com.melih.core.base.lifecycle.BaseDaggerFragment import com.melih.core.base.lifecycle.BaseDaggerFragment
import com.melih.core.extensions.createFor
import com.melih.core.extensions.observe import com.melih.core.extensions.observe
import com.melih.list.R import com.melih.list.R
import com.melih.list.databinding.ListBinding import com.melih.list.databinding.ListBinding
import com.melih.list.ui.adapters.LaunchesAdapter import com.melih.list.ui.adapters.LaunchesAdapter
import com.melih.list.ui.vm.LaunchesViewModel import com.melih.list.ui.vm.LaunchesViewModel
import com.melih.repository.entities.LaunchEntity import com.melih.repository.entities.LaunchEntity
import com.melih.repository.interactors.base.PersistenceEmpty
import com.melih.repository.interactors.base.State import com.melih.repository.interactors.base.State
class LaunchesFragment : BaseDaggerFragment<ListBinding>(), SwipeRefreshLayout.OnRefreshListener { class LaunchesFragment : BaseDaggerFragment<ListBinding>(), SwipeRefreshLayout.OnRefreshListener {
// region Properties //region Properties
private val viewModel: LaunchesViewModel private val viewModel by viewModels<LaunchesViewModel> { viewModelFactory }
get() = viewModelFactory.createFor(this)
private val launchesAdapter = LaunchesAdapter(::onItemSelected) private val launchesAdapter = LaunchesAdapter(::onItemSelected)
// endregion //endregion
// region Functions //region Lifecyle
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@@ -36,6 +36,23 @@ class LaunchesFragment : BaseDaggerFragment<ListBinding>(), SwipeRefreshLayout.O
observeDataChanges() observeDataChanges()
} }
override fun onResume() {
super.onResume()
// Workaround for SwipeRefreshLayout leak -> https://issuetracker.google.com/issues/136153683
binding.swipeRefreshLayout.isEnabled = true
}
override fun onPause() {
super.onPause()
// Workaround for SwipeRefreshLayout leak -> https://issuetracker.google.com/issues/136153683
binding.swipeRefreshLayout.isEnabled = false
}
//endregion
//region Functions
private fun observeDataChanges() { private fun observeDataChanges() {
// Observing state to show loading // Observing state to show loading
@@ -45,8 +62,10 @@ class LaunchesFragment : BaseDaggerFragment<ListBinding>(), SwipeRefreshLayout.O
// Observing error to show toast with retry action // Observing error to show toast with retry action
observe(viewModel.errorData) { observe(viewModel.errorData) {
showSnackbarWithAction(it) { if (it !is PersistenceEmpty) {
viewModel.retry() showSnackbarWithAction(it) {
viewModel.retry()
}
} }
} }
@@ -64,5 +83,5 @@ class LaunchesFragment : BaseDaggerFragment<ListBinding>(), SwipeRefreshLayout.O
} }
override fun getLayoutId(): Int = R.layout.fragment_launches override fun getLayoutId(): Int = R.layout.fragment_launches
// endregion //endregion
} }

View File

@@ -4,25 +4,30 @@ import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import com.melih.core.base.recycler.BasePagingListAdapter import com.melih.core.base.recycler.BasePagingListAdapter
import com.melih.core.base.recycler.BaseViewHolder import com.melih.core.base.recycler.BaseViewHolder
import com.melih.core.extensions.getDiffCallbackForType import com.melih.core.extensions.createDiffCallback
import com.melih.list.databinding.LaunchRowBinding import com.melih.list.databinding.LaunchRowBinding
import com.melih.repository.entities.LaunchEntity import com.melih.repository.entities.LaunchEntity
class LaunchesAdapter(itemClickListener: (LaunchEntity) -> Unit) : BasePagingListAdapter<LaunchEntity>( class LaunchesAdapter(itemClickListener: (LaunchEntity) -> Unit) : BasePagingListAdapter<LaunchEntity>(
getDiffCallbackForType { oldItem, newItem -> oldItem.id == newItem.id }, createDiffCallback { oldItem, newItem -> oldItem.id == newItem.id },
itemClickListener itemClickListener
) { ) {
//region Functions
override fun createViewHolder( override fun createViewHolder(
inflater: LayoutInflater, inflater: LayoutInflater,
parent: ViewGroup, parent: ViewGroup,
viewType: Int viewType: Int
): BaseViewHolder<LaunchEntity> = ): BaseViewHolder<LaunchEntity> =
LaunchesViewHolder(LaunchRowBinding.inflate(inflater, parent, false)) LaunchesViewHolder(LaunchRowBinding.inflate(inflater, parent, false))
//endregion
} }
class LaunchesViewHolder(private val binding: LaunchRowBinding) : BaseViewHolder<LaunchEntity>(binding) { class LaunchesViewHolder(private val binding: LaunchRowBinding) : BaseViewHolder<LaunchEntity>(binding) {
//region Functions
override fun bind(item: LaunchEntity) { override fun bind(item: LaunchEntity) {
binding.entity = item binding.entity = item
@@ -31,4 +36,5 @@ class LaunchesViewHolder(private val binding: LaunchRowBinding) : BaseViewHolder
binding.executePendingBindings() binding.executePendingBindings()
} }
//endregion
} }

View File

@@ -8,6 +8,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import javax.inject.Inject import javax.inject.Inject
/**
* Uses [GetLaunches] to get data for pagination
*/
class LaunchesPagingSource @Inject constructor( class LaunchesPagingSource @Inject constructor(
private val getLaunches: GetLaunches, private val getLaunches: GetLaunches,
private val getLaunchesParams: GetLaunches.Params private val getLaunchesParams: GetLaunches.Params

View File

@@ -10,5 +10,8 @@ class LaunchesPagingSourceFactory @Inject constructor(
private val sourceProvider: Provider<LaunchesPagingSource> private val sourceProvider: Provider<LaunchesPagingSource>
) : BasePagingFactory<LaunchEntity>() { ) : BasePagingFactory<LaunchEntity>() {
//region Functions
override fun createSource(): BasePagingDataSource<LaunchEntity> = sourceProvider.get() override fun createSource(): BasePagingDataSource<LaunchEntity> = sourceProvider.get()
//endregion
} }

View File

@@ -12,34 +12,12 @@ class LaunchesViewModel @Inject constructor(
private val launchesPagingConfig: PagedList.Config private val launchesPagingConfig: PagedList.Config
) : BasePagingViewModel<LaunchEntity>() { ) : BasePagingViewModel<LaunchEntity>() {
// region Properties //region Properties
override val factory: BasePagingFactory<LaunchEntity> override val factory: BasePagingFactory<LaunchEntity>
get() = launchesPagingSourceFactory get() = launchesPagingSourceFactory
override val config: PagedList.Config override val config: PagedList.Config
get() = launchesPagingConfig get() = launchesPagingConfig
//endregion
//private val _filteredItems = MediatorLiveData<PagedList<LaunchEntity>>()
//val filteredItems: LiveData<PagedList<LaunchEntity>>
// get() = _filteredItems
// endregion
//init {
// _filteredItems.addSource(pagedList, _filteredItems::setValue)
//}
// region Functions
//fun filterItemListBy(query: String?) {
//
// _filteredItems.value = if (!query.isNullOrBlank()) {
// pagedList.value
// ?.snapshot() as PagedList<LaunchEntity>
// } else {
// pagedList.value
// }
//}
// endregion
} }

View File

@@ -1,37 +1,36 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout <layout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<data class="ListBinding"> <data class="ListBinding">
<variable <variable
name="viewModel" name="viewModel"
type="com.melih.list.ui.vm.LaunchesViewModel" /> type="com.melih.list.ui.vm.LaunchesViewModel" />
</data> </data>
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:background="@null">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout" android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="com.melih.core.utils.SnackbarBehaviour"> android:background="@null"
app:layout_behavior="com.melih.core.utils.SnackbarBehaviour">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/rocketList" android:id="@+id/rocketList"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layoutAnimation="@anim/layout_item_enter" android:background="@null"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" android:layoutAnimation="@anim/layout_item_enter"
app:layout_constraintBottom_toBottomOf="parent" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent" tools:listitem="@layout/row_launch" />
app:layout_constraintStart_toStartOf="parent" </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
app:layout_constraintTop_toTopOf="parent" </androidx.coordinatorlayout.widget.CoordinatorLayout>
tools:listitem="@layout/row_launch" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout> </layout>

View File

@@ -1,77 +1,69 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout <layout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<data class="LaunchRowBinding"> <data class="LaunchRowBinding">
<variable <variable
name="entity" name="entity"
type="com.melih.repository.entities.LaunchEntity" /> type="com.melih.repository.entities.LaunchEntity" />
</data> </data>
<androidx.cardview.widget.CardView <com.google.android.material.card.MaterialCardView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/padding_standard" android:layout_marginStart="@dimen/padding_standard"
android:layout_marginLeft="@dimen/padding_standard" android:layout_marginLeft="@dimen/padding_standard"
android:layout_marginTop="@dimen/padding_standard" android:layout_marginTop="@dimen/padding_standard"
android:layout_marginEnd="@dimen/padding_standard" android:layout_marginEnd="@dimen/padding_standard"
android:layout_marginRight="@dimen/padding_standard" android:layout_marginRight="@dimen/padding_standard"
app:cardCornerRadius="@dimen/corner_radius_standard" android:background="@null"
app:cardElevation="10dp"> app:cardElevation="10dp">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="160dp"> android:layout_height="160dp">
<ImageView <ImageView
android:id="@+id/imgRocket" android:id="@+id/imgRocket"
imageUrl="@{entity.rocket.imageURL}" imageUrl="@{entity.rocket.imageURL}"
android:layout_width="120dp" android:layout_width="120dp"
android:layout_height="120dp" android:layout_height="120dp"
android:layout_marginStart="@dimen/padding_standard" android:layout_marginStart="@dimen/padding_large"
android:layout_marginLeft="@dimen/padding_standard" android:contentDescription="@string/cd_rocket_image"
android:contentDescription="@string/cd_rocket_image" android:scaleType="centerCrop"
android:scaleType="centerCrop" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintTop_toTopOf="parent" tools:srcCompat="@tools:sample/avatars[14]" />
tools:srcCompat="@tools:sample/avatars[14]" />
<TextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/tvDescription" android:id="@+id/tvTitle"
style="@style/ShortDescriptionTextStyle" style="@style/AppTheme.TextViewStyle.Title"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/padding_standard" android:layout_marginStart="@dimen/padding_standard"
android:layout_marginLeft="@dimen/padding_standard" android:layout_marginEnd="@dimen/padding_large"
android:layout_marginEnd="@dimen/padding_standard" android:text="@{entity.name}"
android:layout_marginRight="@dimen/padding_standard" app:layout_constraintEnd_toEndOf="parent"
android:textAppearance="@style/DescriptionTextAppearance" app:layout_constraintStart_toEndOf="@+id/imgRocket"
app:layout_constraintBottom_toBottomOf="@+id/imgRocket" app:layout_constraintTop_toTopOf="@+id/imgRocket"
app:layout_constraintEnd_toEndOf="parent" tools:text="@sample/launches.json/launches/name" />
app:layout_constraintStart_toEndOf="@+id/imgRocket"
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
tools:text="@sample/launches.json/launches/missions/description" />
<TextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/tvTitle" android:id="@+id/tvDescription"
style="@style/TitleTextStyle" style="@style/AppTheme.TextViewStyle.Description"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="0dp"
android:layout_marginStart="@dimen/padding_standard" android:layout_marginStart="@dimen/padding_standard"
android:layout_marginLeft="@dimen/padding_standard" android:layout_marginEnd="@dimen/padding_large"
android:layout_marginEnd="@dimen/padding_standard" app:layout_constraintBottom_toBottomOf="@+id/imgRocket"
android:layout_marginRight="@dimen/padding_standard" app:layout_constraintEnd_toEndOf="parent"
android:text="@{entity.name}" app:layout_constraintStart_toEndOf="@+id/imgRocket"
android:textAppearance="@style/TitleTextAppearance" app:layout_constraintTop_toBottomOf="@+id/tvTitle"
app:layout_constraintEnd_toEndOf="parent" tools:text="@sample/launches.json/launches/missions/description" />
app:layout_constraintStart_toEndOf="@+id/imgRocket" </androidx.constraintlayout.widget.ConstraintLayout>
app:layout_constraintTop_toTopOf="@+id/imgRocket" </com.google.android.material.card.MaterialCardView>
tools:text="@sample/launches.json/launches/name" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</layout> </layout>

View File

@@ -1,6 +1,6 @@
#Thu Jun 13 10:50:25 CEST 2019 #Wed Sep 04 15:39:59 CEST 2019
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-all.zip

View File

@@ -1,5 +1,4 @@
package com.melih.repository package com.melih.repository
const val DEFAULT_NAME = "Default name" internal const val DEFAULT_NAME = "Default name"
const val EMPTY_STRING = "" internal const val EMPTY_STRING = ""
const val DEFAULT_IMAGE_SIZE = 480

View File

@@ -4,10 +4,13 @@ import com.melih.repository.entities.LaunchEntity
import com.melih.repository.interactors.base.Result import com.melih.repository.interactors.base.Result
/** /**
* Abstract class to create contract in sources to seperate low level business logic from build and return type * Contract for sources to seperate low level business logic from build and return type
*/ */
abstract class Repository { abstract class Repository {
//region Abstractions
internal abstract suspend fun getNextLaunches(count: Int, page: Int): Result<List<LaunchEntity>> internal abstract suspend fun getNextLaunches(count: Int, page: Int): Result<List<LaunchEntity>>
internal abstract suspend fun getLaunchById(id: Long): Result<LaunchEntity> internal abstract suspend fun getLaunchById(id: Long): Result<LaunchEntity>
//endregion
} }

View File

@@ -18,11 +18,16 @@ import javax.inject.Inject
@UseExperimental(ExperimentalCoroutinesApi::class) @UseExperimental(ExperimentalCoroutinesApi::class)
class GetLaunchDetails @Inject constructor() : BaseInteractor<LaunchEntity, GetLaunchDetails.Params>() { class GetLaunchDetails @Inject constructor() : BaseInteractor<LaunchEntity, GetLaunchDetails.Params>() {
//region Properties
@field:Inject @field:Inject
internal lateinit var networkSource: NetworkSource internal lateinit var networkSource: NetworkSource
@field:Inject @field:Inject
internal lateinit var persistenceSource: PersistenceSource internal lateinit var persistenceSource: PersistenceSource
//endregion
//region Functions
override suspend fun FlowCollector<Result<LaunchEntity>>.run(params: Params) { override suspend fun FlowCollector<Result<LaunchEntity>>.run(params: Params) {
val result = persistenceSource.getLaunchById(params.id) val result = persistenceSource.getLaunchById(params.id)
@@ -42,6 +47,7 @@ class GetLaunchDetails @Inject constructor() : BaseInteractor<LaunchEntity, GetL
emit(result) emit(result)
} }
} }
//endregion
data class Params( data class Params(
val id: Long val id: Long

View File

@@ -19,11 +19,16 @@ const val DEFAULT_LAUNCHES_AMOUNT = 15
@UseExperimental(ExperimentalCoroutinesApi::class) @UseExperimental(ExperimentalCoroutinesApi::class)
class GetLaunches @Inject constructor() : BaseInteractor<List<LaunchEntity>, GetLaunches.Params>() { class GetLaunches @Inject constructor() : BaseInteractor<List<LaunchEntity>, GetLaunches.Params>() {
//region Properties
@field:Inject @field:Inject
internal lateinit var networkSource: NetworkSource internal lateinit var networkSource: NetworkSource
@field:Inject @field:Inject
internal lateinit var persistenceSource: PersistenceSource internal lateinit var persistenceSource: PersistenceSource
//endregion
//region Functions
override suspend fun FlowCollector<Result<List<LaunchEntity>>>.run(params: Params) { override suspend fun FlowCollector<Result<List<LaunchEntity>>>.run(params: Params) {
@@ -40,6 +45,7 @@ class GetLaunches @Inject constructor() : BaseInteractor<List<LaunchEntity>, Get
} }
} }
} }
//endregion
data class Params( data class Params(
val count: Int = DEFAULT_LAUNCHES_AMOUNT, val count: Int = DEFAULT_LAUNCHES_AMOUNT,

View File

@@ -13,12 +13,12 @@ import kotlinx.coroutines.flow.flowOn
@UseExperimental(ExperimentalCoroutinesApi::class) @UseExperimental(ExperimentalCoroutinesApi::class)
abstract class BaseInteractor<T, in P : InteractorParameters> { abstract class BaseInteractor<T, in P : InteractorParameters> {
// region Abstractions //region Abstractions
protected abstract suspend fun FlowCollector<Result<T>>.run(params: P) protected abstract suspend fun FlowCollector<Result<T>>.run(params: P)
// endregion //endregion
// region Functions //region Functions
operator fun invoke(params: P) = operator fun invoke(params: P) =
flow<Result<T>> { flow<Result<T>> {
@@ -26,7 +26,7 @@ abstract class BaseInteractor<T, in P : InteractorParameters> {
run(params) run(params)
emit(State.Loaded()) emit(State.Loaded())
}.flowOn(Dispatchers.IO) }.flowOn(Dispatchers.IO)
// endregion //endregion
} }
/** /**

View File

@@ -9,6 +9,8 @@ import com.melih.repository.R
*/ */
sealed class Reason(@StringRes val messageRes: Int) sealed class Reason(@StringRes val messageRes: Int)
//region Subclasses
class NetworkError : Reason(R.string.reason_network) class NetworkError : Reason(R.string.reason_network)
class EmptyResultError : Reason(R.string.reason_empty_body) class EmptyResultError : Reason(R.string.reason_empty_body)
class GenericError : Reason(R.string.reason_generic) class GenericError : Reason(R.string.reason_generic)
@@ -16,3 +18,4 @@ class ResponseError : Reason(R.string.reason_response)
class TimeoutError : Reason(R.string.reason_timeout) class TimeoutError : Reason(R.string.reason_timeout)
class PersistenceEmpty : Reason(R.string.reason_persistance_empty) class PersistenceEmpty : Reason(R.string.reason_persistance_empty)
class NoNetworkPersistenceEmpty : Reason(R.string.reason_no_network_persistance_empty) class NoNetworkPersistenceEmpty : Reason(R.string.reason_no_network_persistance_empty)
//endregion

View File

@@ -9,7 +9,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
@UseExperimental(ExperimentalCoroutinesApi::class) @UseExperimental(ExperimentalCoroutinesApi::class)
sealed class Result<out T> sealed class Result<out T>
// region Subclasses //region Subclasses
class Success<out T>(val successData: T) : Result<T>() class Success<out T>(val successData: T) : Result<T>()
class Failure(val errorData: Reason) : Result<Nothing>() class Failure(val errorData: Reason) : Result<Nothing>()
@@ -18,9 +18,9 @@ sealed class State : Result<Nothing>() {
class Loading : State() class Loading : State()
class Loaded : State() class Loaded : State()
} }
// endregion //endregion
// region Extensions //region Extensions
inline fun <T> Result<T>.handle(stateBlock: (State) -> Unit, failureBlock: (Reason) -> Unit, successBlock: (T) -> Unit) { inline fun <T> Result<T>.handle(stateBlock: (State) -> Unit, failureBlock: (Reason) -> Unit, successBlock: (T) -> Unit) {
when (this) { when (this) {
@@ -50,4 +50,4 @@ inline fun <T> Result<T>.onState(stateBlock: (State) -> Unit): Result<T> {
return this return this
} }
// endregion //endregion

View File

@@ -12,6 +12,8 @@ import retrofit2.http.Query
*/ */
internal interface Api { internal interface Api {
//region Get
@GET("launch/next/{count}") @GET("launch/next/{count}")
suspend fun getNextLaunches( suspend fun getNextLaunches(
@Path("count") count: Int, @Path("count") count: Int,
@@ -22,4 +24,5 @@ internal interface Api {
suspend fun getLaunchById( suspend fun getLaunchById(
@Path("id") id: Long @Path("id") id: Long
): Response<LaunchEntity> ): Response<LaunchEntity>
//endregion
} }

View File

@@ -16,7 +16,7 @@ internal const val TIMEOUT_DURATION = 7L
internal class ApiImpl @Inject constructor() : Api { internal class ApiImpl @Inject constructor() : Api {
// region Properties //region Properties
private val service by lazy { private val service by lazy {
val moshi = Moshi.Builder() val moshi = Moshi.Builder()
@@ -39,7 +39,9 @@ internal class ApiImpl @Inject constructor() : Api {
.build() .build()
.create(Api::class.java) .create(Api::class.java)
} }
// endregion //endregion
//region Functions
override suspend fun getNextLaunches( override suspend fun getNextLaunches(
count: Int, count: Int,
@@ -51,4 +53,5 @@ internal class ApiImpl @Inject constructor() : Api {
id: Long id: Long
): Response<LaunchEntity> = ): Response<LaunchEntity> =
service.getLaunchById(id) service.getLaunchById(id)
//endregion
} }

View File

@@ -28,6 +28,8 @@ const val DB_NAME = "LaunchesDB"
) )
internal abstract class LaunchesDatabase : RoomDatabase() { internal abstract class LaunchesDatabase : RoomDatabase() {
//region Companion
companion object { companion object {
private lateinit var instance: LaunchesDatabase private lateinit var instance: LaunchesDatabase
@@ -42,6 +44,10 @@ internal abstract class LaunchesDatabase : RoomDatabase() {
} }
} }
//endregion
//region Abstractions
internal abstract val launchesDao: LaunchesDao internal abstract val launchesDao: LaunchesDao
//endregion
} }

View File

@@ -10,13 +10,19 @@ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
*/ */
abstract class BaseConverter<T> { abstract class BaseConverter<T> {
//region Abstractions
abstract fun getAdapter(moshi: Moshi): JsonAdapter<T>
//endregion
//region Properties
private val moshi = Moshi.Builder() private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory()) .add(KotlinJsonAdapterFactory())
.build() .build()
//endregion
abstract fun getAdapter(moshi: Moshi): JsonAdapter<T> //region Functions
// region Functions
@TypeConverter @TypeConverter
fun convertFrom(item: T) = fun convertFrom(item: T) =
@@ -25,5 +31,5 @@ abstract class BaseConverter<T> {
@TypeConverter @TypeConverter
fun convertTo(string: String) = fun convertTo(string: String) =
getAdapter(moshi).fromJson(string) getAdapter(moshi).fromJson(string)
// endregion //endregion
} }

View File

@@ -10,13 +10,19 @@ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
*/ */
abstract class BaseListConverter<T> { abstract class BaseListConverter<T> {
//region Abstractions
abstract fun getAdapter(moshi: Moshi): JsonAdapter<List<T>>
//endregion
//region Properties
private val moshi = Moshi.Builder() private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory()) .add(KotlinJsonAdapterFactory())
.build() .build()
//endregion
abstract fun getAdapter(moshi: Moshi): JsonAdapter<List<T>> //region Functions
// region Functions
@TypeConverter @TypeConverter
fun convertFrom(items: List<T>) = fun convertFrom(items: List<T>) =
@@ -25,5 +31,5 @@ abstract class BaseListConverter<T> {
@TypeConverter @TypeConverter
fun convertTo(string: String): List<T>? = fun convertTo(string: String): List<T>? =
getAdapter(moshi).fromJson(string) getAdapter(moshi).fromJson(string)
// endregion //endregion
} }

View File

@@ -9,6 +9,7 @@ import com.squareup.moshi.Moshi
* Converts [location][LocationEntity] * Converts [location][LocationEntity]
*/ */
class LocationConverter : BaseConverter<LocationEntity>() { class LocationConverter : BaseConverter<LocationEntity>() {
override fun getAdapter(moshi: Moshi): JsonAdapter<LocationEntity> = override fun getAdapter(moshi: Moshi): JsonAdapter<LocationEntity> =
LocationEntityJsonAdapter(moshi) LocationEntityJsonAdapter(moshi)
} }

View File

@@ -9,6 +9,7 @@ import com.squareup.moshi.Moshi
* Converts [rocket][RocketEntity] * Converts [rocket][RocketEntity]
*/ */
class RocketConverter : BaseConverter<RocketEntity>() { class RocketConverter : BaseConverter<RocketEntity>() {
override fun getAdapter(moshi: Moshi): JsonAdapter<RocketEntity> = override fun getAdapter(moshi: Moshi): JsonAdapter<RocketEntity> =
RocketEntityJsonAdapter(moshi) RocketEntityJsonAdapter(moshi)
} }

View File

@@ -12,7 +12,7 @@ import com.melih.repository.entities.LaunchEntity
@Dao @Dao
internal abstract class LaunchesDao { internal abstract class LaunchesDao {
// region Queries //region Queries
@Query("SELECT * FROM Launches ORDER BY launchStartTime DESC LIMIT :count OFFSET :page*:count") @Query("SELECT * FROM Launches ORDER BY launchStartTime DESC LIMIT :count OFFSET :page*:count")
abstract suspend fun getLaunches(count: Int, page: Int): List<LaunchEntity> abstract suspend fun getLaunches(count: Int, page: Int): List<LaunchEntity>
@@ -22,14 +22,14 @@ internal abstract class LaunchesDao {
@Query("DELETE FROM Launches") @Query("DELETE FROM Launches")
abstract suspend fun nukeLaunches() abstract suspend fun nukeLaunches()
// endregion //endregion
// region Insertion //region Insertion
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun saveLaunches(launches: List<LaunchEntity>) abstract suspend fun saveLaunches(launches: List<LaunchEntity>)
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun saveLaunch(launch: LaunchEntity) abstract suspend fun saveLaunch(launch: LaunchEntity)
// endregion //endregion
} }

View File

@@ -1,7 +1,6 @@
package com.melih.repository.sources package com.melih.repository.sources
import android.net.NetworkInfo import android.net.NetworkInfo
import com.melih.repository.DEFAULT_IMAGE_SIZE
import com.melih.repository.Repository import com.melih.repository.Repository
import com.melih.repository.entities.LaunchEntity import com.melih.repository.entities.LaunchEntity
import com.melih.repository.interactors.DEFAULT_LAUNCHES_AMOUNT import com.melih.repository.interactors.DEFAULT_LAUNCHES_AMOUNT
@@ -19,6 +18,8 @@ import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
private const val DEFAULT_IMAGE_SIZE = 480
/** /**
* NetworkSource for fetching results using api and wrapping them as contracted in [repository][Repository], * NetworkSource for fetching results using api and wrapping them as contracted in [repository][Repository],
* returning either [failure][Failure] with proper [reason][Reason] or [success][Success] with data * returning either [failure][Failure] with proper [reason][Reason] or [success][Success] with data
@@ -27,16 +28,17 @@ internal class NetworkSource @Inject constructor(
private val apiImpl: ApiImpl, private val apiImpl: ApiImpl,
private val networkInfoProvider: Provider<NetworkInfo> private val networkInfoProvider: Provider<NetworkInfo>
) : Repository() { ) : Repository() {
// region Properties
//region Properties
private val isNetworkConnected: Boolean private val isNetworkConnected: Boolean
get() { get() {
val networkInfo = networkInfoProvider.get() val networkInfo = networkInfoProvider.get()
return networkInfo != null && networkInfo.isConnected return networkInfo != null && networkInfo.isConnected
} }
// endregion //endregion
// region Functions //region Functions
override suspend fun getNextLaunches(count: Int, page: Int): Result<List<LaunchEntity>> = override suspend fun getNextLaunches(count: Int, page: Int): Result<List<LaunchEntity>> =
safeExecute({ safeExecute({
@@ -73,8 +75,8 @@ internal class NetworkSource @Inject constructor(
} }
} }
private suspend inline fun <T, R> safeExecute( private inline fun <T, R> safeExecute(
block: suspend () -> Response<T>, block: () -> Response<T>,
transform: (T) -> R transform: (T) -> R
) = ) =
if (isNetworkConnected) { if (isNetworkConnected) {
@@ -112,5 +114,5 @@ internal class NetworkSource @Inject constructor(
} catch (e: Exception) { } catch (e: Exception) {
imageUrl imageUrl
} }
// endregion //endregion
} }

View File

@@ -16,7 +16,8 @@ import javax.inject.Inject
internal class PersistenceSource @Inject constructor( internal class PersistenceSource @Inject constructor(
ctx: Context ctx: Context
) : Repository() { ) : Repository() {
// region Functions
//region Functions
private val launchesDatabase = LaunchesDatabase.getInstance(ctx) private val launchesDatabase = LaunchesDatabase.getInstance(ctx)
@@ -45,5 +46,5 @@ internal class PersistenceSource @Inject constructor(
internal suspend fun saveLaunch(launch: LaunchEntity) { internal suspend fun saveLaunch(launch: LaunchEntity) {
launchesDatabase.launchesDao.saveLaunch(launch) launchesDatabase.launchesDao.saveLaunch(launch)
} }
// endregion //endregion
} }

View File

@@ -2,6 +2,7 @@ apply from: "$rootProject.projectDir/scripts/default_dependencies.gradle"
android { android {
compileSdkVersion versions.compileSdkVersion compileSdkVersion versions.compileSdkVersion
buildToolsVersion versions.buildToolsVersion
defaultConfig { defaultConfig {
minSdkVersion versions.minSdkVersion minSdkVersion versions.minSdkVersion

View File

@@ -1,77 +1,80 @@
ext { ext {
versions = [ versions = [
minSdkVersion : 21, minSdkVersion : 21,
compileSdkVersion : 28, compileSdkVersion : 29,
targetSdkVersion : 28, targetSdkVersion : 29,
buildToolsVersion : "28.0.3", buildToolsVersion : "29.0.2",
supportLibraryVersion : "28.0.0", appCompatVersion : "1.1.0-rc01",
appCompatVersion : "1.1.0-rc01", lifecycleVersion : "2.2.0-alpha03",
lifecycleVersion : "2.2.0-alpha03", fragmentVersion : "1.2.0-alpha02",
fragmentVersion : "1.2.0-alpha02", workManagerVersion : "2.3.0-alpha01",
workManagerVersion : "2.2.0-rc01", constraintLayoutVesion : "2.0.0-beta2",
constraintLayoutVesion: "2.0.0-beta1", cardViewVersion : "1.0.0",
cardViewVersion : "1.0.0", recyclerViewVersion : "1.1.0-beta03",
recyclerViewVersion : "1.1.0-beta02", pagingVersion : "2.1.0",
pagingVersion : "2.1.0", viewPagerVersion : "1.0.0-beta03",
viewPagerVersion : "1.0.0-beta03", materialVersion : "1.1.0-alpha09",
collectionVersion : "1.1.0", swipeRefreshLayoutVersion: "1.1.0-alpha02",
roomVersion : "2.2.0-alpha02", collectionVersion : "1.1.0",
daggerVersion : "2.24", roomVersion : "2.2.0-rc01",
okHttpVersion : "4.0.1", daggerVersion : "2.24",
retrofitVersion : "2.6.1", okHttpVersion : "4.0.1",
picassoVersion : "2.71828", retrofitVersion : "2.6.1",
moshiVersion : "1.8.0", picassoVersion : "2.71828",
coroutinesVersion : "1.3.0-RC2", moshiVersion : "1.8.0",
leakCanaryVersion : "2.0-beta-2", coroutinesVersion : "1.3.0-RC2",
timberVersion : "4.7.1", leakCanaryVersion : "2.0-beta-2",
jUnitVersion : "5.5.1", timberVersion : "4.7.1",
espressoVersion : "3.2.0", jUnitVersion : "5.5.1",
mockkVersion : "1.9.3", espressoVersion : "3.2.0",
kluentVersion : "1.53", mockkVersion : "1.9.3",
kluentVersion : "1.53",
] ]
libraries = [ libraries = [
/** /**
* Android libraries * Android libraries
*/ */
appCompat : "androidx.appcompat:appcompat:${versions.appCompatVersion}", appCompat : "androidx.appcompat:appcompat:${versions.appCompatVersion}",
recyclerView : "androidx.recyclerview:recyclerview:${versions.recyclerViewVersion}", recyclerView : "androidx.recyclerview:recyclerview:${versions.recyclerViewVersion}",
cardView : "androidx.cardview:cardview:${versions.cardViewVersion}", cardView : "androidx.cardview:cardview:${versions.cardViewVersion}",
constraintLayout: "androidx.constraintlayout:constraintlayout:${versions.constraintLayoutVesion}", constraintLayout : "androidx.constraintlayout:constraintlayout:${versions.constraintLayoutVesion}",
multixDex : "androidx.multidex:multidex:2.0.1", multixDex : "androidx.multidex:multidex:2.0.1",
fragment : "androidx.fragment:fragment-ktx:${versions.fragmentVersion}", fragment : "androidx.fragment:fragment-ktx:${versions.fragmentVersion}",
material : "com.google.android.material:material:${versions.materialVersion}",
/** /**
* Jetpack * Jetpack
*/ */
navigation : [ navigation : [
"androidx.navigation:navigation-fragment-ktx:$nav_version", "androidx.navigation:navigation-fragment-ktx:$nav_version",
"androidx.navigation:navigation-ui-ktx:$nav_version" "androidx.navigation:navigation-ui-ktx:$nav_version"
], ],
room : [ room : [
"androidx.room:room-runtime:${versions.roomVersion}", "androidx.room:room-runtime:${versions.roomVersion}",
"androidx.room:room-ktx:${versions.roomVersion}" "androidx.room:room-ktx:${versions.roomVersion}"
], ],
lifecycle : "androidx.lifecycle:lifecycle-extensions:${versions.lifecycleVersion}", lifecycle : "androidx.lifecycle:lifecycle-extensions:${versions.lifecycleVersion}",
liveDataKTX : "androidx.lifecycle:lifecycle-livedata-ktx:${versions.lifecycleVersion}", liveDataKTX : "androidx.lifecycle:lifecycle-livedata-ktx:${versions.lifecycleVersion}",
workManager : "androidx.work:work-runtime-ktx:${versions.workManagerVersion}", workManager : "androidx.work:work-runtime-ktx:${versions.workManagerVersion}",
paging : "androidx.paging:paging-runtime-ktx:${versions.pagingVersion}", paging : "androidx.paging:paging-runtime-ktx:${versions.pagingVersion}",
viewPager : "androidx.viewpager2:viewpager2:${versions.viewPagerVersion}", viewPager : "androidx.viewpager2:viewpager2:${versions.viewPagerVersion}",
collection : "androidx.collection:collection-ktx:${versions.collectionVersion}", collection : "androidx.collection:collection-ktx:${versions.collectionVersion}",
swipeRefreshLayout: "androidx.swiperefreshlayout:swiperefreshlayout:${versions.swipeRefreshLayoutVersion}",
/** /**
* Kotlin * Kotlin
*/ */
kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version", kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version",
coroutines : "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.coroutinesVersion}", coroutines : "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.coroutinesVersion}",
/** /**
* Dagger * Dagger
*/ */
dagger : [ dagger : [
"com.google.dagger:dagger:${versions.daggerVersion}", "com.google.dagger:dagger:${versions.daggerVersion}",
"com.google.dagger:dagger-android:${versions.daggerVersion}", "com.google.dagger:dagger-android:${versions.daggerVersion}",
"com.google.dagger:dagger-android-support:${versions.daggerVersion}" "com.google.dagger:dagger-android-support:${versions.daggerVersion}"
@@ -80,17 +83,17 @@ ext {
/** /**
* OkHttp * OkHttp
*/ */
okHttp : [ okHttp : [
"com.squareup.okhttp3:okhttp:${versions.okHttpVersion}", "com.squareup.okhttp3:okhttp:${versions.okHttpVersion}",
"com.squareup.okhttp3:logging-interceptor:${versions.okHttpVersion}" "com.squareup.okhttp3:logging-interceptor:${versions.okHttpVersion}"
], ],
okHttpLogger : "com.squareup.okhttp3:logging-interceptor:${versions.okHttpVersion}", okHttpLogger : "com.squareup.okhttp3:logging-interceptor:${versions.okHttpVersion}",
/** /**
* Retrofit * Retrofit
*/ */
retrofit : [ retrofit : [
"com.squareup.retrofit2:retrofit:${versions.retrofitVersion}", "com.squareup.retrofit2:retrofit:${versions.retrofitVersion}",
"com.squareup.retrofit2:converter-moshi:${versions.retrofitVersion}" "com.squareup.retrofit2:converter-moshi:${versions.retrofitVersion}"
], ],
@@ -98,27 +101,27 @@ ext {
/** /**
* Moshi * Moshi
*/ */
moshi : [ moshi : [
"com.squareup.moshi:moshi:${versions.moshiVersion}", "com.squareup.moshi:moshi:${versions.moshiVersion}",
"com.squareup.moshi:moshi-kotlin:${versions.moshiVersion}" "com.squareup.moshi:moshi-kotlin:${versions.moshiVersion}"
], ],
moshiKotlin : "com.squareup.moshi:moshi-kotlin:${versions.moshiVersion}", moshiKotlin : "com.squareup.moshi:moshi-kotlin:${versions.moshiVersion}",
/** /**
* Picasso for image loading * Picasso for image loading
*/ */
picasso : "com.squareup.picasso:picasso:${versions.picassoVersion}", picasso : "com.squareup.picasso:picasso:${versions.picassoVersion}",
/** /**
* LeakCanary * LeakCanary
*/ */
leakCanary : "com.squareup.leakcanary:leakcanary-android:${versions.leakCanaryVersion}", leakCanary : "com.squareup.leakcanary:leakcanary-android:${versions.leakCanaryVersion}",
/** /**
* Timber * Timber
*/ */
timber : "com.jakewharton.timber:timber:${versions.timberVersion}" timber : "com.jakewharton.timber:timber:${versions.timberVersion}"
] ]
annotationProcessors = [ annotationProcessors = [

View File

@@ -1,18 +0,0 @@
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
testCoverageEnabled = true
}
dev {
initWith debug
}
}
}

View File

@@ -1,3 +1,2 @@
apply from: "$rootProject.projectDir/scripts/default_android_config.gradle" apply from: "$rootProject.projectDir/scripts/default_android_config.gradle"
apply from: "$rootProject.projectDir/scripts/sources.gradle" apply from: "$rootProject.projectDir/scripts/sources.gradle"
apply from: "$rootProject.projectDir/scripts/flavors.gradle"

View File

@@ -14,4 +14,21 @@ android {
jvmTarget = '1.8' jvmTarget = '1.8'
freeCompilerArgs += "-Xuse-experimental=kotlin.Experimental" freeCompilerArgs += "-Xuse-experimental=kotlin.Experimental"
} }
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
testCoverageEnabled = true
}
dev {
initWith debug
}
}
} }