mirror of
https://github.com/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture.git
synced 2026-05-02 21:54:22 +02:00
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:
@@ -1,12 +1,12 @@
|
||||
# Rocket Science
|
||||
# Android-Kotlin-Modulerized-CleanArchitecture
|
||||
|
||||
[](https://github.com/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture/actions) [](https://circleci.com/gh/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture/tree/master) [](https://codebeat.co/projects/github-com-melihaksoy-rocketscience-master) [](https://codecov.io/gh/melihaksoy/RocketScience) [](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.
|
||||
|
||||
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).
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ apply plugin: 'kotlin-kapt'
|
||||
|
||||
apply from: "$rootProject.projectDir/scripts/default_android_config.gradle"
|
||||
apply from: "$rootProject.projectDir/scripts/sources.gradle"
|
||||
apply from: "$rootProject.projectDir/scripts/flavors.gradle"
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
@@ -26,7 +25,6 @@ dependencies {
|
||||
implementation project(':features:launches')
|
||||
implementation project(':features:detail')
|
||||
|
||||
implementation libraries.coroutines
|
||||
implementation libraries.navigation
|
||||
|
||||
debugImplementation libraries.leakCanary
|
||||
@@ -37,6 +35,7 @@ dependencies {
|
||||
compileOnly libraries.retrofit
|
||||
compileOnly libraries.room
|
||||
compileOnly libraries.paging
|
||||
compileOnly libraries.swipeRefreshLayout
|
||||
|
||||
// Need for proper renders in xml previews
|
||||
compileOnly libraries.constraintLayout
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
package com.melih.rocketscience
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.ui.NavigationUI
|
||||
import com.melih.rocketscience.databinding.MainActivityBinding
|
||||
import dagger.android.support.DaggerAppCompatActivity
|
||||
|
||||
class MainActivity : DaggerAppCompatActivity() {
|
||||
|
||||
private lateinit var binding: MainActivityBinding
|
||||
private lateinit var navController: NavController
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
navController = findNavController(R.id.nav_host_fragment)
|
||||
NavigationUI.setupWithNavController(binding.toolbar, navController)
|
||||
NavigationUI.setupWithNavController(findViewById<Toolbar>(R.id.toolbar), navController)
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
|
||||
@@ -8,8 +8,10 @@ import dagger.android.AndroidInjector
|
||||
|
||||
@AppScope
|
||||
@Component(
|
||||
modules = [AndroidInjectionModule::class,
|
||||
AppModule::class],
|
||||
modules = [
|
||||
AndroidInjectionModule::class,
|
||||
AppModule::class
|
||||
],
|
||||
|
||||
dependencies = [CoreComponent::class]
|
||||
)
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
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>
|
||||
<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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.41'
|
||||
ext.kotlin_version = '1.3.50'
|
||||
ext.nav_version = '2.2.0-alpha01'
|
||||
|
||||
repositories {
|
||||
@@ -12,7 +12,7 @@ buildscript {
|
||||
classpath 'com.android.tools.build:gradle:3.5.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
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"
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
||||
@@ -4,7 +4,6 @@ apply plugin: 'kotlin-kapt'
|
||||
|
||||
apply from: "$rootProject.projectDir/scripts/default_android_config.gradle"
|
||||
apply from: "$rootProject.projectDir/scripts/sources.gradle"
|
||||
apply from: "$rootProject.projectDir/scripts/flavors.gradle"
|
||||
|
||||
android {
|
||||
dataBinding {
|
||||
@@ -23,6 +22,7 @@ dependencies {
|
||||
implementation libraries.liveDataKTX
|
||||
implementation libraries.navigation
|
||||
implementation libraries.picasso
|
||||
implementation libraries.material
|
||||
|
||||
testImplementation testLibraries.jUnitApi
|
||||
testImplementation testLibraries.mockk
|
||||
|
||||
@@ -14,7 +14,7 @@ import javax.inject.Inject
|
||||
* Parent of fragments which has injections. Aim is to seperate [BaseFragment] functionality for fragments which
|
||||
* 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
|
||||
* 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 {
|
||||
|
||||
// region Properties
|
||||
//region Properties
|
||||
|
||||
@get:Inject
|
||||
internal var androidInjector: DispatchingAndroidInjector<Any>? = null
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelFactory
|
||||
// endregion
|
||||
//endregion
|
||||
|
||||
// region Functions
|
||||
//region Functions
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
AndroidSupportInjection.inject(this)
|
||||
@@ -39,5 +39,5 @@ abstract class BaseDaggerFragment<T : ViewDataBinding> : BaseFragment<T>(), HasA
|
||||
}
|
||||
|
||||
override fun androidInjector(): AndroidInjector<Any>? = androidInjector
|
||||
// endregion
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -20,12 +20,18 @@ import com.melih.repository.interactors.base.Reason
|
||||
*/
|
||||
abstract class BaseFragment<T : ViewDataBinding> : Fragment() {
|
||||
|
||||
// region Properties
|
||||
//region Abstractions
|
||||
|
||||
@LayoutRes
|
||||
abstract fun getLayoutId(): Int
|
||||
//endregion
|
||||
|
||||
//region Properties
|
||||
|
||||
protected lateinit var binding: T
|
||||
// endregion
|
||||
//endregion
|
||||
|
||||
// region Functions
|
||||
//region Functions
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@@ -46,8 +52,5 @@ abstract class BaseFragment<T : ViewDataBinding> : Fragment() {
|
||||
block()
|
||||
}.show()
|
||||
}
|
||||
|
||||
@LayoutRes
|
||||
abstract fun getLayoutId(): Int
|
||||
// endregion
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -37,12 +37,12 @@ const val INITIAL_PAGE = 0
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
abstract class BasePagingDataSource<T> : PageKeyedDataSource<Int, T>() {
|
||||
|
||||
// region Abstractions
|
||||
//region Abstractions
|
||||
|
||||
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 _reasonData = MutableLiveData<Reason>()
|
||||
@@ -59,9 +59,9 @@ abstract class BasePagingDataSource<T> : PageKeyedDataSource<Int, T>() {
|
||||
*/
|
||||
val reasonData: LiveData<Reason>
|
||||
get() = _reasonData
|
||||
// endregion
|
||||
//endregion
|
||||
|
||||
// region Functions
|
||||
//region Functions
|
||||
|
||||
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, T>) {
|
||||
// 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()
|
||||
super.invalidate()
|
||||
}
|
||||
// endregion
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -16,20 +16,20 @@ import androidx.paging.DataSource
|
||||
*/
|
||||
abstract class BasePagingFactory<T> : DataSource.Factory<Int, T>() {
|
||||
|
||||
// region Abstractions
|
||||
//region Abstractions
|
||||
|
||||
abstract fun createSource(): BasePagingDataSource<T>
|
||||
// endregion
|
||||
//endregion
|
||||
|
||||
// region Properties
|
||||
//region Properties
|
||||
|
||||
private val _currentSource = MutableLiveData<BasePagingDataSource<T>>()
|
||||
|
||||
val currentSource: LiveData<BasePagingDataSource<T>>
|
||||
get() = _currentSource
|
||||
// endregion
|
||||
//endregion
|
||||
|
||||
// region Functions
|
||||
//region Functions
|
||||
|
||||
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]
|
||||
*/
|
||||
fun invalidateDataSource() = currentSource.value?.invalidate()
|
||||
// endregion
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ abstract class BasePagingListAdapter<T>(
|
||||
private val clickListener: (T) -> Unit
|
||||
) : PagedListAdapter<T, BaseViewHolder<T>>(callback) {
|
||||
|
||||
//region Abstractions
|
||||
|
||||
/**
|
||||
* 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,
|
||||
viewType: Int
|
||||
): BaseViewHolder<T>
|
||||
//endregion
|
||||
|
||||
//region Functions
|
||||
|
||||
/**
|
||||
* [createViewHolder] will provide holders, no need to override this
|
||||
@@ -51,6 +56,7 @@ abstract class BasePagingListAdapter<T>(
|
||||
holder.bind(item)
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,6 +64,8 @@ abstract class BasePagingListAdapter<T>(
|
||||
*/
|
||||
abstract class BaseViewHolder<T>(binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
//region Functions
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
abstract fun bind(item: T)
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.melih.core.base.viewmodel
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.Transformations.switchMap
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.paging.PagedList
|
||||
import androidx.paging.toLiveData
|
||||
@@ -20,19 +20,19 @@ import com.melih.repository.interactors.base.State
|
||||
*/
|
||||
abstract class BasePagingViewModel<T> : ViewModel() {
|
||||
|
||||
// region Abstractions
|
||||
//region Abstractions
|
||||
|
||||
abstract val factory: BasePagingFactory<T>
|
||||
abstract val config: PagedList.Config
|
||||
// endregion
|
||||
//endregion
|
||||
|
||||
// region Properties
|
||||
//region Properties
|
||||
|
||||
/**
|
||||
* Observe [stateData] to get notified of state of data
|
||||
*/
|
||||
val stateData: LiveData<State> by lazy {
|
||||
Transformations.switchMap(factory.currentSource) {
|
||||
switchMap(factory.currentSource) {
|
||||
it.stateData
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ abstract class BasePagingViewModel<T> : ViewModel() {
|
||||
* Observe [errorData] to get notified if an error occurs
|
||||
*/
|
||||
val errorData: LiveData<Reason> by lazy {
|
||||
Transformations.switchMap(factory.currentSource) {
|
||||
switchMap(factory.currentSource) {
|
||||
it.reasonData
|
||||
}
|
||||
}
|
||||
@@ -52,9 +52,9 @@ abstract class BasePagingViewModel<T> : ViewModel() {
|
||||
val pagedList: LiveData<PagedList<T>> by lazy {
|
||||
factory.toLiveData(config)
|
||||
}
|
||||
// endregion
|
||||
//endregion
|
||||
|
||||
// region Functions
|
||||
//region Functions
|
||||
|
||||
fun refresh() {
|
||||
factory.currentSource.value?.invalidate()
|
||||
@@ -66,5 +66,5 @@ abstract class BasePagingViewModel<T> : ViewModel() {
|
||||
fun retry() {
|
||||
factory.currentSource.value?.invalidate()
|
||||
}
|
||||
// endregion
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ import kotlinx.coroutines.launch
|
||||
*/
|
||||
abstract class BaseViewModel<T> : ViewModel() {
|
||||
|
||||
// region Abstractions
|
||||
//region Abstractions
|
||||
|
||||
abstract suspend fun loadData()
|
||||
// endregion
|
||||
//endregion
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
@@ -26,7 +26,7 @@ abstract class BaseViewModel<T> : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
// region Properties
|
||||
//region Properties
|
||||
|
||||
private val _successData = MutableLiveData<T>()
|
||||
private val _stateData = MutableLiveData<State>()
|
||||
@@ -49,9 +49,9 @@ abstract class BaseViewModel<T> : ViewModel() {
|
||||
*/
|
||||
val errorData: LiveData<Reason>
|
||||
get() = _errorData
|
||||
// endregion
|
||||
//endregion
|
||||
|
||||
// region Functions
|
||||
//region Functions
|
||||
|
||||
/**
|
||||
* Default success handler which assigns given [data] to [successData]
|
||||
@@ -97,5 +97,5 @@ abstract class BaseViewModel<T> : ViewModel() {
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -3,9 +3,6 @@ package com.melih.core.extensions
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.LiveData
|
||||
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
|
||||
@@ -16,13 +13,3 @@ import androidx.lifecycle.ViewModelProviders
|
||||
fun <T> Fragment.observe(data: LiveData<T>, block: (T) -> Unit) {
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#008577</color>
|
||||
<color name="colorPrimaryDark">#00574B</color>
|
||||
<color name="colorAccent">#D81B60</color>
|
||||
<color name="lightGray">#8F8F8F</color>
|
||||
<color name="primaryColor">#f9a825</color>
|
||||
<color name="primaryLightColor">#ffd95a</color>
|
||||
<color name="primaryDarkColor">#c17900</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>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<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>
|
||||
</resources>
|
||||
@@ -1,35 +1,40 @@
|
||||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
</style>
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||
<item name="colorPrimary">@color/primaryColor</item>
|
||||
<item name="colorPrimaryDark">@color/primaryDarkColor</item>
|
||||
<item name="colorSecondary">@color/secondaryColor</item>
|
||||
<item name="colorAccent">@color/secondaryDarkColor</item>
|
||||
|
||||
<!--Styles-->
|
||||
<item name="android:textColorPrimary">@color/primaryTextColor</item>
|
||||
<item name="android:textColorSecondary">@color/secondaryTextColor</item>
|
||||
|
||||
<style name="TitleTextStyle">
|
||||
<item name="android:singleLine">true</item>
|
||||
</style>
|
||||
<item name="toolbarStyle">@style/AppTheme.ToolbarStyle</item>
|
||||
<item name="materialCardViewStyle">@style/AppTheme.CardViewStyle</item>
|
||||
</style>
|
||||
|
||||
<style name="ShortDescriptionTextStyle">
|
||||
<item name="android:ellipsize">end</item>
|
||||
<item name="android:maxLines">@integer/common_max_lines</item>
|
||||
<item name="android:gravity">center|left</item>
|
||||
</style>
|
||||
<!-- Widgets -->
|
||||
|
||||
<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="DescriptionTextAppearance" parent="TextAppearance.AppCompat.Body1">
|
||||
<item name="android:textColor">@color/lightGray</item>
|
||||
</style>
|
||||
<style name="AppTheme.TextViewStyle.Title" parent="AppTheme.BaseTextViewStyle">
|
||||
<item name="android:textAppearance">@style/TextAppearance.MaterialComponents.Headline5</item>
|
||||
<item name="android:singleLine">true</item>
|
||||
</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>
|
||||
|
||||
@@ -12,12 +12,12 @@ import dagger.android.ContributesAndroidInjector
|
||||
@Module
|
||||
abstract class DetailContributor {
|
||||
|
||||
// region Contributes
|
||||
//region Contributes
|
||||
|
||||
@ContributesAndroidInjector(
|
||||
modules = [DetailFragmentModule::class]
|
||||
)
|
||||
@DetailFragmentScope
|
||||
abstract fun detailFragment(): DetailFragment
|
||||
// endregion
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -15,13 +15,13 @@ import dagger.multibindings.IntoMap
|
||||
@Module
|
||||
abstract class DetailFragmentModule {
|
||||
|
||||
// region ViewModels
|
||||
//region ViewModels
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(DetailViewModel::class)
|
||||
abstract fun detailViewModel(detailViewModel: DetailViewModel): ViewModel
|
||||
// endregion
|
||||
//endregion
|
||||
|
||||
@Module
|
||||
companion object {
|
||||
|
||||
@@ -3,21 +3,20 @@ package com.melih.detail.ui
|
||||
import android.os.Bundle
|
||||
import android.text.method.ScrollingMovementMethod
|
||||
import android.view.View
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.melih.core.base.lifecycle.BaseDaggerFragment
|
||||
import com.melih.core.extensions.createFor
|
||||
import com.melih.core.extensions.observe
|
||||
import com.melih.detail.R
|
||||
import com.melih.detail.databinding.DetailBinding
|
||||
|
||||
class DetailFragment : BaseDaggerFragment<DetailBinding>() {
|
||||
|
||||
// region Properties
|
||||
//region Properties
|
||||
|
||||
private val viewModel: DetailViewModel
|
||||
get() = viewModelFactory.createFor(this)
|
||||
// endregion
|
||||
private val viewModel by viewModels<DetailViewModel> { viewModelFactory }
|
||||
//endregion
|
||||
|
||||
// region Functions
|
||||
//region Functions
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
@@ -34,5 +33,5 @@ class DetailFragment : BaseDaggerFragment<DetailBinding>() {
|
||||
}
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.fragment_detail
|
||||
// endregion
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.melih.detail.ui
|
||||
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.Transformations.map
|
||||
import com.melih.core.base.viewmodel.BaseViewModel
|
||||
import com.melih.repository.entities.LaunchEntity
|
||||
import com.melih.repository.interactors.GetLaunchDetails
|
||||
@@ -13,13 +13,13 @@ class DetailViewModel @Inject constructor(
|
||||
private val getLaunchDetailsParams: GetLaunchDetails.Params
|
||||
) : BaseViewModel<LaunchEntity>() {
|
||||
|
||||
// region Properties
|
||||
//region Properties
|
||||
|
||||
val rocketName = Transformations.map(successData) {
|
||||
val rocketName = map(successData) {
|
||||
it.rocket.name
|
||||
}
|
||||
|
||||
val description = Transformations.map(successData) {
|
||||
val description = map(successData) {
|
||||
if (it.missions.isEmpty()) {
|
||||
""
|
||||
} else {
|
||||
@@ -27,12 +27,12 @@ class DetailViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
val imageUrl = Transformations.map(successData) {
|
||||
val imageUrl = map(successData) {
|
||||
it.rocket.imageURL
|
||||
}
|
||||
// endregion
|
||||
//endregion
|
||||
|
||||
// region Functions
|
||||
//region Functions
|
||||
|
||||
/**
|
||||
* Triggering interactor in view model scope
|
||||
@@ -42,5 +42,5 @@ class DetailViewModel @Inject constructor(
|
||||
it.handle(::handleState, ::handleFailure, ::handleSuccess)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -1,75 +1,69 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data class="DetailBinding">
|
||||
<data class="DetailBinding">
|
||||
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="com.melih.detail.ui.DetailViewModel" />
|
||||
</data>
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="com.melih.detail.ui.DetailViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<ImageView
|
||||
android:id="@+id/imgRocket"
|
||||
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
|
||||
android:id="@+id/imgRocket"
|
||||
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]" />
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/tvTitle"
|
||||
style="@style/AppTheme.TextViewStyle.Title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
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:text="@{viewModel.rocketName}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/imgRocket"
|
||||
tools:text="@sample/launches.json/launches/name" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTitle"
|
||||
style="@style/TitleTextStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
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:text="@{viewModel.rocketName}"
|
||||
android:textAppearance="@style/TitleTextAppearance"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/imgRocket"
|
||||
tools:text="@sample/launches.json/launches/name" />
|
||||
|
||||
<TextView
|
||||
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>
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
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/TextAppearance.MaterialComponents.Body1"
|
||||
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>
|
||||
</layout>
|
||||
|
||||
@@ -8,6 +8,7 @@ dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
implementation libraries.paging
|
||||
implementation libraries.swipeRefreshLayout
|
||||
|
||||
testImplementation testLibraries.coroutinesTest
|
||||
}
|
||||
|
||||
@@ -12,12 +12,12 @@ import dagger.android.ContributesAndroidInjector
|
||||
@Module
|
||||
abstract class LaunchesContributor {
|
||||
|
||||
// region Contributes
|
||||
//region Contributes
|
||||
|
||||
@ContributesAndroidInjector(
|
||||
modules = [LaunchesFragmentModule::class]
|
||||
)
|
||||
@LaunchesFragmentScope
|
||||
abstract fun launchesFragment(): LaunchesFragment
|
||||
// endregion
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -14,13 +14,13 @@ import dagger.multibindings.IntoMap
|
||||
@Module
|
||||
abstract class LaunchesFragmentModule {
|
||||
|
||||
// region ViewModels
|
||||
//region ViewModels
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(LaunchesViewModel::class)
|
||||
abstract fun listViewModel(listViewModel: LaunchesViewModel): ViewModel
|
||||
// endregion
|
||||
//endregion
|
||||
|
||||
@Module
|
||||
companion object {
|
||||
|
||||
@@ -2,29 +2,29 @@ package com.melih.list.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.melih.core.actions.openDetail
|
||||
import com.melih.core.base.lifecycle.BaseDaggerFragment
|
||||
import com.melih.core.extensions.createFor
|
||||
import com.melih.core.extensions.observe
|
||||
import com.melih.list.R
|
||||
import com.melih.list.databinding.ListBinding
|
||||
import com.melih.list.ui.adapters.LaunchesAdapter
|
||||
import com.melih.list.ui.vm.LaunchesViewModel
|
||||
import com.melih.repository.entities.LaunchEntity
|
||||
import com.melih.repository.interactors.base.PersistenceEmpty
|
||||
import com.melih.repository.interactors.base.State
|
||||
|
||||
class LaunchesFragment : BaseDaggerFragment<ListBinding>(), SwipeRefreshLayout.OnRefreshListener {
|
||||
|
||||
// region Properties
|
||||
//region Properties
|
||||
|
||||
private val viewModel: LaunchesViewModel
|
||||
get() = viewModelFactory.createFor(this)
|
||||
private val viewModel by viewModels<LaunchesViewModel> { viewModelFactory }
|
||||
|
||||
private val launchesAdapter = LaunchesAdapter(::onItemSelected)
|
||||
// endregion
|
||||
//endregion
|
||||
|
||||
// region Functions
|
||||
//region Lifecyle
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
@@ -36,6 +36,23 @@ class LaunchesFragment : BaseDaggerFragment<ListBinding>(), SwipeRefreshLayout.O
|
||||
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() {
|
||||
|
||||
// Observing state to show loading
|
||||
@@ -45,8 +62,10 @@ class LaunchesFragment : BaseDaggerFragment<ListBinding>(), SwipeRefreshLayout.O
|
||||
|
||||
// Observing error to show toast with retry action
|
||||
observe(viewModel.errorData) {
|
||||
showSnackbarWithAction(it) {
|
||||
viewModel.retry()
|
||||
if (it !is PersistenceEmpty) {
|
||||
showSnackbarWithAction(it) {
|
||||
viewModel.retry()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,5 +83,5 @@ class LaunchesFragment : BaseDaggerFragment<ListBinding>(), SwipeRefreshLayout.O
|
||||
}
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.fragment_launches
|
||||
// endregion
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -4,25 +4,30 @@ import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import com.melih.core.base.recycler.BasePagingListAdapter
|
||||
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.repository.entities.LaunchEntity
|
||||
|
||||
class LaunchesAdapter(itemClickListener: (LaunchEntity) -> Unit) : BasePagingListAdapter<LaunchEntity>(
|
||||
getDiffCallbackForType { oldItem, newItem -> oldItem.id == newItem.id },
|
||||
createDiffCallback { oldItem, newItem -> oldItem.id == newItem.id },
|
||||
itemClickListener
|
||||
) {
|
||||
|
||||
//region Functions
|
||||
|
||||
override fun createViewHolder(
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): BaseViewHolder<LaunchEntity> =
|
||||
LaunchesViewHolder(LaunchRowBinding.inflate(inflater, parent, false))
|
||||
|
||||
//endregion
|
||||
}
|
||||
|
||||
class LaunchesViewHolder(private val binding: LaunchRowBinding) : BaseViewHolder<LaunchEntity>(binding) {
|
||||
|
||||
//region Functions
|
||||
|
||||
override fun bind(item: LaunchEntity) {
|
||||
binding.entity = item
|
||||
|
||||
@@ -31,4 +36,5 @@ class LaunchesViewHolder(private val binding: LaunchRowBinding) : BaseViewHolder
|
||||
|
||||
binding.executePendingBindings()
|
||||
}
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Uses [GetLaunches] to get data for pagination
|
||||
*/
|
||||
class LaunchesPagingSource @Inject constructor(
|
||||
private val getLaunches: GetLaunches,
|
||||
private val getLaunchesParams: GetLaunches.Params
|
||||
|
||||
@@ -10,5 +10,8 @@ class LaunchesPagingSourceFactory @Inject constructor(
|
||||
private val sourceProvider: Provider<LaunchesPagingSource>
|
||||
) : BasePagingFactory<LaunchEntity>() {
|
||||
|
||||
//region Functions
|
||||
|
||||
override fun createSource(): BasePagingDataSource<LaunchEntity> = sourceProvider.get()
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -12,34 +12,12 @@ class LaunchesViewModel @Inject constructor(
|
||||
private val launchesPagingConfig: PagedList.Config
|
||||
) : BasePagingViewModel<LaunchEntity>() {
|
||||
|
||||
// region Properties
|
||||
//region Properties
|
||||
|
||||
override val factory: BasePagingFactory<LaunchEntity>
|
||||
get() = launchesPagingSourceFactory
|
||||
|
||||
override val config: PagedList.Config
|
||||
get() = launchesPagingConfig
|
||||
|
||||
//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
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -1,37 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data class="ListBinding">
|
||||
<data class="ListBinding">
|
||||
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="com.melih.list.ui.vm.LaunchesViewModel" />
|
||||
</data>
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="com.melih.list.ui.vm.LaunchesViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@null">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="com.melih.core.utils.SnackbarBehaviour">
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@null"
|
||||
app:layout_behavior="com.melih.core.utils.SnackbarBehaviour">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rocketList"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layoutAnimation="@anim/layout_item_enter"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:listitem="@layout/row_launch" />
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rocketList"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@null"
|
||||
android:layoutAnimation="@anim/layout_item_enter"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/row_launch" />
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</layout>
|
||||
|
||||
@@ -1,77 +1,69 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data class="LaunchRowBinding">
|
||||
<data class="LaunchRowBinding">
|
||||
|
||||
<variable
|
||||
name="entity"
|
||||
type="com.melih.repository.entities.LaunchEntity" />
|
||||
</data>
|
||||
<variable
|
||||
name="entity"
|
||||
type="com.melih.repository.entities.LaunchEntity" />
|
||||
</data>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
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"
|
||||
app:cardCornerRadius="@dimen/corner_radius_standard"
|
||||
app:cardElevation="10dp">
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
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:background="@null"
|
||||
app:cardElevation="10dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="160dp">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="160dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imgRocket"
|
||||
imageUrl="@{entity.rocket.imageURL}"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="120dp"
|
||||
android:layout_marginStart="@dimen/padding_standard"
|
||||
android:layout_marginLeft="@dimen/padding_standard"
|
||||
android:contentDescription="@string/cd_rocket_image"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:srcCompat="@tools:sample/avatars[14]" />
|
||||
<ImageView
|
||||
android:id="@+id/imgRocket"
|
||||
imageUrl="@{entity.rocket.imageURL}"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="120dp"
|
||||
android:layout_marginStart="@dimen/padding_large"
|
||||
android:contentDescription="@string/cd_rocket_image"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:srcCompat="@tools:sample/avatars[14]" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvDescription"
|
||||
style="@style/ShortDescriptionTextStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="@dimen/padding_standard"
|
||||
android:layout_marginLeft="@dimen/padding_standard"
|
||||
android:layout_marginEnd="@dimen/padding_standard"
|
||||
android:layout_marginRight="@dimen/padding_standard"
|
||||
android:textAppearance="@style/DescriptionTextAppearance"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/imgRocket"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/imgRocket"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
|
||||
tools:text="@sample/launches.json/launches/missions/description" />
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/tvTitle"
|
||||
style="@style/AppTheme.TextViewStyle.Title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/padding_standard"
|
||||
android:layout_marginEnd="@dimen/padding_large"
|
||||
android:text="@{entity.name}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/imgRocket"
|
||||
app:layout_constraintTop_toTopOf="@+id/imgRocket"
|
||||
tools:text="@sample/launches.json/launches/name" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTitle"
|
||||
style="@style/TitleTextStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/padding_standard"
|
||||
android:layout_marginLeft="@dimen/padding_standard"
|
||||
android:layout_marginEnd="@dimen/padding_standard"
|
||||
android:layout_marginRight="@dimen/padding_standard"
|
||||
android:text="@{entity.name}"
|
||||
android:textAppearance="@style/TitleTextAppearance"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/imgRocket"
|
||||
app:layout_constraintTop_toTopOf="@+id/imgRocket"
|
||||
tools:text="@sample/launches.json/launches/name" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/tvDescription"
|
||||
style="@style/AppTheme.TextViewStyle.Description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="@dimen/padding_standard"
|
||||
android:layout_marginEnd="@dimen/padding_large"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/imgRocket"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/imgRocket"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
|
||||
tools:text="@sample/launches.json/launches/missions/description" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</layout>
|
||||
|
||||
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
#Thu Jun 13 10:50:25 CEST 2019
|
||||
#Wed Sep 04 15:39:59 CEST 2019
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
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
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
package com.melih.repository
|
||||
|
||||
const val DEFAULT_NAME = "Default name"
|
||||
const val EMPTY_STRING = ""
|
||||
const val DEFAULT_IMAGE_SIZE = 480
|
||||
internal const val DEFAULT_NAME = "Default name"
|
||||
internal const val EMPTY_STRING = ""
|
||||
|
||||
@@ -4,10 +4,13 @@ import com.melih.repository.entities.LaunchEntity
|
||||
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 {
|
||||
|
||||
//region Abstractions
|
||||
|
||||
internal abstract suspend fun getNextLaunches(count: Int, page: Int): Result<List<LaunchEntity>>
|
||||
internal abstract suspend fun getLaunchById(id: Long): Result<LaunchEntity>
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -18,11 +18,16 @@ import javax.inject.Inject
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
class GetLaunchDetails @Inject constructor() : BaseInteractor<LaunchEntity, GetLaunchDetails.Params>() {
|
||||
|
||||
//region Properties
|
||||
|
||||
@field:Inject
|
||||
internal lateinit var networkSource: NetworkSource
|
||||
|
||||
@field:Inject
|
||||
internal lateinit var persistenceSource: PersistenceSource
|
||||
//endregion
|
||||
|
||||
//region Functions
|
||||
|
||||
override suspend fun FlowCollector<Result<LaunchEntity>>.run(params: Params) {
|
||||
val result = persistenceSource.getLaunchById(params.id)
|
||||
@@ -42,6 +47,7 @@ class GetLaunchDetails @Inject constructor() : BaseInteractor<LaunchEntity, GetL
|
||||
emit(result)
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
data class Params(
|
||||
val id: Long
|
||||
|
||||
@@ -19,11 +19,16 @@ const val DEFAULT_LAUNCHES_AMOUNT = 15
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
class GetLaunches @Inject constructor() : BaseInteractor<List<LaunchEntity>, GetLaunches.Params>() {
|
||||
|
||||
//region Properties
|
||||
|
||||
@field:Inject
|
||||
internal lateinit var networkSource: NetworkSource
|
||||
|
||||
@field:Inject
|
||||
internal lateinit var persistenceSource: PersistenceSource
|
||||
//endregion
|
||||
|
||||
//region Functions
|
||||
|
||||
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(
|
||||
val count: Int = DEFAULT_LAUNCHES_AMOUNT,
|
||||
|
||||
@@ -13,12 +13,12 @@ import kotlinx.coroutines.flow.flowOn
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
abstract class BaseInteractor<T, in P : InteractorParameters> {
|
||||
|
||||
// region Abstractions
|
||||
//region Abstractions
|
||||
|
||||
protected abstract suspend fun FlowCollector<Result<T>>.run(params: P)
|
||||
// endregion
|
||||
//endregion
|
||||
|
||||
// region Functions
|
||||
//region Functions
|
||||
|
||||
operator fun invoke(params: P) =
|
||||
flow<Result<T>> {
|
||||
@@ -26,7 +26,7 @@ abstract class BaseInteractor<T, in P : InteractorParameters> {
|
||||
run(params)
|
||||
emit(State.Loaded())
|
||||
}.flowOn(Dispatchers.IO)
|
||||
// endregion
|
||||
//endregion
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,8 @@ import com.melih.repository.R
|
||||
*/
|
||||
sealed class Reason(@StringRes val messageRes: Int)
|
||||
|
||||
//region Subclasses
|
||||
|
||||
class NetworkError : Reason(R.string.reason_network)
|
||||
class EmptyResultError : Reason(R.string.reason_empty_body)
|
||||
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 PersistenceEmpty : Reason(R.string.reason_persistance_empty)
|
||||
class NoNetworkPersistenceEmpty : Reason(R.string.reason_no_network_persistance_empty)
|
||||
//endregion
|
||||
|
||||
@@ -9,7 +9,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
sealed class Result<out T>
|
||||
|
||||
// region Subclasses
|
||||
//region Subclasses
|
||||
|
||||
class Success<out T>(val successData: T) : Result<T>()
|
||||
class Failure(val errorData: Reason) : Result<Nothing>()
|
||||
@@ -18,9 +18,9 @@ sealed class State : Result<Nothing>() {
|
||||
class Loading : 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) {
|
||||
when (this) {
|
||||
@@ -50,4 +50,4 @@ inline fun <T> Result<T>.onState(stateBlock: (State) -> Unit): Result<T> {
|
||||
|
||||
return this
|
||||
}
|
||||
// endregion
|
||||
//endregion
|
||||
|
||||
@@ -12,6 +12,8 @@ import retrofit2.http.Query
|
||||
*/
|
||||
internal interface Api {
|
||||
|
||||
//region Get
|
||||
|
||||
@GET("launch/next/{count}")
|
||||
suspend fun getNextLaunches(
|
||||
@Path("count") count: Int,
|
||||
@@ -22,4 +24,5 @@ internal interface Api {
|
||||
suspend fun getLaunchById(
|
||||
@Path("id") id: Long
|
||||
): Response<LaunchEntity>
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ internal const val TIMEOUT_DURATION = 7L
|
||||
|
||||
internal class ApiImpl @Inject constructor() : Api {
|
||||
|
||||
// region Properties
|
||||
//region Properties
|
||||
|
||||
private val service by lazy {
|
||||
val moshi = Moshi.Builder()
|
||||
@@ -39,7 +39,9 @@ internal class ApiImpl @Inject constructor() : Api {
|
||||
.build()
|
||||
.create(Api::class.java)
|
||||
}
|
||||
// endregion
|
||||
//endregion
|
||||
|
||||
//region Functions
|
||||
|
||||
override suspend fun getNextLaunches(
|
||||
count: Int,
|
||||
@@ -51,4 +53,5 @@ internal class ApiImpl @Inject constructor() : Api {
|
||||
id: Long
|
||||
): Response<LaunchEntity> =
|
||||
service.getLaunchById(id)
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ const val DB_NAME = "LaunchesDB"
|
||||
)
|
||||
internal abstract class LaunchesDatabase : RoomDatabase() {
|
||||
|
||||
//region Companion
|
||||
|
||||
companion object {
|
||||
|
||||
private lateinit var instance: LaunchesDatabase
|
||||
@@ -42,6 +44,10 @@ internal abstract class LaunchesDatabase : RoomDatabase() {
|
||||
}
|
||||
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Abstractions
|
||||
|
||||
internal abstract val launchesDao: LaunchesDao
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -10,13 +10,19 @@ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
*/
|
||||
abstract class BaseConverter<T> {
|
||||
|
||||
//region Abstractions
|
||||
|
||||
abstract fun getAdapter(moshi: Moshi): JsonAdapter<T>
|
||||
//endregion
|
||||
|
||||
//region Properties
|
||||
|
||||
private val moshi = Moshi.Builder()
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
//endregion
|
||||
|
||||
abstract fun getAdapter(moshi: Moshi): JsonAdapter<T>
|
||||
|
||||
// region Functions
|
||||
//region Functions
|
||||
|
||||
@TypeConverter
|
||||
fun convertFrom(item: T) =
|
||||
@@ -25,5 +31,5 @@ abstract class BaseConverter<T> {
|
||||
@TypeConverter
|
||||
fun convertTo(string: String) =
|
||||
getAdapter(moshi).fromJson(string)
|
||||
// endregion
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -10,13 +10,19 @@ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
*/
|
||||
abstract class BaseListConverter<T> {
|
||||
|
||||
//region Abstractions
|
||||
|
||||
abstract fun getAdapter(moshi: Moshi): JsonAdapter<List<T>>
|
||||
//endregion
|
||||
|
||||
//region Properties
|
||||
|
||||
private val moshi = Moshi.Builder()
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
//endregion
|
||||
|
||||
abstract fun getAdapter(moshi: Moshi): JsonAdapter<List<T>>
|
||||
|
||||
// region Functions
|
||||
//region Functions
|
||||
|
||||
@TypeConverter
|
||||
fun convertFrom(items: List<T>) =
|
||||
@@ -25,5 +31,5 @@ abstract class BaseListConverter<T> {
|
||||
@TypeConverter
|
||||
fun convertTo(string: String): List<T>? =
|
||||
getAdapter(moshi).fromJson(string)
|
||||
// endregion
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.squareup.moshi.Moshi
|
||||
* Converts [location][LocationEntity]
|
||||
*/
|
||||
class LocationConverter : BaseConverter<LocationEntity>() {
|
||||
|
||||
override fun getAdapter(moshi: Moshi): JsonAdapter<LocationEntity> =
|
||||
LocationEntityJsonAdapter(moshi)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.squareup.moshi.Moshi
|
||||
* Converts [rocket][RocketEntity]
|
||||
*/
|
||||
class RocketConverter : BaseConverter<RocketEntity>() {
|
||||
|
||||
override fun getAdapter(moshi: Moshi): JsonAdapter<RocketEntity> =
|
||||
RocketEntityJsonAdapter(moshi)
|
||||
RocketEntityJsonAdapter(moshi)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import com.melih.repository.entities.LaunchEntity
|
||||
@Dao
|
||||
internal abstract class LaunchesDao {
|
||||
|
||||
// region Queries
|
||||
//region Queries
|
||||
|
||||
@Query("SELECT * FROM Launches ORDER BY launchStartTime DESC LIMIT :count OFFSET :page*:count")
|
||||
abstract suspend fun getLaunches(count: Int, page: Int): List<LaunchEntity>
|
||||
@@ -22,14 +22,14 @@ internal abstract class LaunchesDao {
|
||||
|
||||
@Query("DELETE FROM Launches")
|
||||
abstract suspend fun nukeLaunches()
|
||||
// endregion
|
||||
//endregion
|
||||
|
||||
// region Insertion
|
||||
//region Insertion
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract suspend fun saveLaunches(launches: List<LaunchEntity>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract suspend fun saveLaunch(launch: LaunchEntity)
|
||||
// endregion
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.melih.repository.sources
|
||||
|
||||
import android.net.NetworkInfo
|
||||
import com.melih.repository.DEFAULT_IMAGE_SIZE
|
||||
import com.melih.repository.Repository
|
||||
import com.melih.repository.entities.LaunchEntity
|
||||
import com.melih.repository.interactors.DEFAULT_LAUNCHES_AMOUNT
|
||||
@@ -19,6 +18,8 @@ import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
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],
|
||||
* 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 networkInfoProvider: Provider<NetworkInfo>
|
||||
) : Repository() {
|
||||
// region Properties
|
||||
|
||||
//region Properties
|
||||
|
||||
private val isNetworkConnected: Boolean
|
||||
get() {
|
||||
val networkInfo = networkInfoProvider.get()
|
||||
return networkInfo != null && networkInfo.isConnected
|
||||
}
|
||||
// endregion
|
||||
//endregion
|
||||
|
||||
// region Functions
|
||||
//region Functions
|
||||
|
||||
override suspend fun getNextLaunches(count: Int, page: Int): Result<List<LaunchEntity>> =
|
||||
safeExecute({
|
||||
@@ -73,8 +75,8 @@ internal class NetworkSource @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun <T, R> safeExecute(
|
||||
block: suspend () -> Response<T>,
|
||||
private inline fun <T, R> safeExecute(
|
||||
block: () -> Response<T>,
|
||||
transform: (T) -> R
|
||||
) =
|
||||
if (isNetworkConnected) {
|
||||
@@ -112,5 +114,5 @@ internal class NetworkSource @Inject constructor(
|
||||
} catch (e: Exception) {
|
||||
imageUrl
|
||||
}
|
||||
// endregion
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ import javax.inject.Inject
|
||||
internal class PersistenceSource @Inject constructor(
|
||||
ctx: Context
|
||||
) : Repository() {
|
||||
// region Functions
|
||||
|
||||
//region Functions
|
||||
|
||||
private val launchesDatabase = LaunchesDatabase.getInstance(ctx)
|
||||
|
||||
@@ -45,5 +46,5 @@ internal class PersistenceSource @Inject constructor(
|
||||
internal suspend fun saveLaunch(launch: LaunchEntity) {
|
||||
launchesDatabase.launchesDao.saveLaunch(launch)
|
||||
}
|
||||
// endregion
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ apply from: "$rootProject.projectDir/scripts/default_dependencies.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion versions.compileSdkVersion
|
||||
buildToolsVersion versions.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion versions.minSdkVersion
|
||||
|
||||
@@ -1,77 +1,80 @@
|
||||
ext {
|
||||
|
||||
versions = [
|
||||
minSdkVersion : 21,
|
||||
compileSdkVersion : 28,
|
||||
targetSdkVersion : 28,
|
||||
buildToolsVersion : "28.0.3",
|
||||
supportLibraryVersion : "28.0.0",
|
||||
appCompatVersion : "1.1.0-rc01",
|
||||
lifecycleVersion : "2.2.0-alpha03",
|
||||
fragmentVersion : "1.2.0-alpha02",
|
||||
workManagerVersion : "2.2.0-rc01",
|
||||
constraintLayoutVesion: "2.0.0-beta1",
|
||||
cardViewVersion : "1.0.0",
|
||||
recyclerViewVersion : "1.1.0-beta02",
|
||||
pagingVersion : "2.1.0",
|
||||
viewPagerVersion : "1.0.0-beta03",
|
||||
collectionVersion : "1.1.0",
|
||||
roomVersion : "2.2.0-alpha02",
|
||||
daggerVersion : "2.24",
|
||||
okHttpVersion : "4.0.1",
|
||||
retrofitVersion : "2.6.1",
|
||||
picassoVersion : "2.71828",
|
||||
moshiVersion : "1.8.0",
|
||||
coroutinesVersion : "1.3.0-RC2",
|
||||
leakCanaryVersion : "2.0-beta-2",
|
||||
timberVersion : "4.7.1",
|
||||
jUnitVersion : "5.5.1",
|
||||
espressoVersion : "3.2.0",
|
||||
mockkVersion : "1.9.3",
|
||||
kluentVersion : "1.53",
|
||||
minSdkVersion : 21,
|
||||
compileSdkVersion : 29,
|
||||
targetSdkVersion : 29,
|
||||
buildToolsVersion : "29.0.2",
|
||||
appCompatVersion : "1.1.0-rc01",
|
||||
lifecycleVersion : "2.2.0-alpha03",
|
||||
fragmentVersion : "1.2.0-alpha02",
|
||||
workManagerVersion : "2.3.0-alpha01",
|
||||
constraintLayoutVesion : "2.0.0-beta2",
|
||||
cardViewVersion : "1.0.0",
|
||||
recyclerViewVersion : "1.1.0-beta03",
|
||||
pagingVersion : "2.1.0",
|
||||
viewPagerVersion : "1.0.0-beta03",
|
||||
materialVersion : "1.1.0-alpha09",
|
||||
swipeRefreshLayoutVersion: "1.1.0-alpha02",
|
||||
collectionVersion : "1.1.0",
|
||||
roomVersion : "2.2.0-rc01",
|
||||
daggerVersion : "2.24",
|
||||
okHttpVersion : "4.0.1",
|
||||
retrofitVersion : "2.6.1",
|
||||
picassoVersion : "2.71828",
|
||||
moshiVersion : "1.8.0",
|
||||
coroutinesVersion : "1.3.0-RC2",
|
||||
leakCanaryVersion : "2.0-beta-2",
|
||||
timberVersion : "4.7.1",
|
||||
jUnitVersion : "5.5.1",
|
||||
espressoVersion : "3.2.0",
|
||||
mockkVersion : "1.9.3",
|
||||
kluentVersion : "1.53",
|
||||
]
|
||||
|
||||
libraries = [
|
||||
/**
|
||||
* Android libraries
|
||||
*/
|
||||
appCompat : "androidx.appcompat:appcompat:${versions.appCompatVersion}",
|
||||
recyclerView : "androidx.recyclerview:recyclerview:${versions.recyclerViewVersion}",
|
||||
cardView : "androidx.cardview:cardview:${versions.cardViewVersion}",
|
||||
constraintLayout: "androidx.constraintlayout:constraintlayout:${versions.constraintLayoutVesion}",
|
||||
multixDex : "androidx.multidex:multidex:2.0.1",
|
||||
fragment : "androidx.fragment:fragment-ktx:${versions.fragmentVersion}",
|
||||
appCompat : "androidx.appcompat:appcompat:${versions.appCompatVersion}",
|
||||
recyclerView : "androidx.recyclerview:recyclerview:${versions.recyclerViewVersion}",
|
||||
cardView : "androidx.cardview:cardview:${versions.cardViewVersion}",
|
||||
constraintLayout : "androidx.constraintlayout:constraintlayout:${versions.constraintLayoutVesion}",
|
||||
multixDex : "androidx.multidex:multidex:2.0.1",
|
||||
fragment : "androidx.fragment:fragment-ktx:${versions.fragmentVersion}",
|
||||
material : "com.google.android.material:material:${versions.materialVersion}",
|
||||
|
||||
/**
|
||||
* Jetpack
|
||||
*/
|
||||
navigation : [
|
||||
navigation : [
|
||||
"androidx.navigation:navigation-fragment-ktx:$nav_version",
|
||||
"androidx.navigation:navigation-ui-ktx:$nav_version"
|
||||
],
|
||||
|
||||
room : [
|
||||
room : [
|
||||
"androidx.room:room-runtime:${versions.roomVersion}",
|
||||
"androidx.room:room-ktx:${versions.roomVersion}"
|
||||
],
|
||||
|
||||
lifecycle : "androidx.lifecycle:lifecycle-extensions:${versions.lifecycleVersion}",
|
||||
liveDataKTX : "androidx.lifecycle:lifecycle-livedata-ktx:${versions.lifecycleVersion}",
|
||||
workManager : "androidx.work:work-runtime-ktx:${versions.workManagerVersion}",
|
||||
paging : "androidx.paging:paging-runtime-ktx:${versions.pagingVersion}",
|
||||
viewPager : "androidx.viewpager2:viewpager2:${versions.viewPagerVersion}",
|
||||
collection : "androidx.collection:collection-ktx:${versions.collectionVersion}",
|
||||
lifecycle : "androidx.lifecycle:lifecycle-extensions:${versions.lifecycleVersion}",
|
||||
liveDataKTX : "androidx.lifecycle:lifecycle-livedata-ktx:${versions.lifecycleVersion}",
|
||||
workManager : "androidx.work:work-runtime-ktx:${versions.workManagerVersion}",
|
||||
paging : "androidx.paging:paging-runtime-ktx:${versions.pagingVersion}",
|
||||
viewPager : "androidx.viewpager2:viewpager2:${versions.viewPagerVersion}",
|
||||
collection : "androidx.collection:collection-ktx:${versions.collectionVersion}",
|
||||
swipeRefreshLayout: "androidx.swiperefreshlayout:swiperefreshlayout:${versions.swipeRefreshLayoutVersion}",
|
||||
|
||||
/**
|
||||
* Kotlin
|
||||
*/
|
||||
kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version",
|
||||
coroutines : "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.coroutinesVersion}",
|
||||
kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version",
|
||||
coroutines : "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.coroutinesVersion}",
|
||||
|
||||
/**
|
||||
* Dagger
|
||||
*/
|
||||
dagger : [
|
||||
dagger : [
|
||||
"com.google.dagger:dagger:${versions.daggerVersion}",
|
||||
"com.google.dagger:dagger-android:${versions.daggerVersion}",
|
||||
"com.google.dagger:dagger-android-support:${versions.daggerVersion}"
|
||||
@@ -80,17 +83,17 @@ ext {
|
||||
/**
|
||||
* OkHttp
|
||||
*/
|
||||
okHttp : [
|
||||
okHttp : [
|
||||
"com.squareup.okhttp3:okhttp:${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 : [
|
||||
"com.squareup.retrofit2:retrofit:${versions.retrofitVersion}",
|
||||
"com.squareup.retrofit2:converter-moshi:${versions.retrofitVersion}"
|
||||
],
|
||||
@@ -98,27 +101,27 @@ ext {
|
||||
/**
|
||||
* Moshi
|
||||
*/
|
||||
moshi : [
|
||||
moshi : [
|
||||
"com.squareup.moshi:moshi:${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 : "com.squareup.picasso:picasso:${versions.picassoVersion}",
|
||||
picasso : "com.squareup.picasso:picasso:${versions.picassoVersion}",
|
||||
|
||||
/**
|
||||
* LeakCanary
|
||||
*/
|
||||
leakCanary : "com.squareup.leakcanary:leakcanary-android:${versions.leakCanaryVersion}",
|
||||
leakCanary : "com.squareup.leakcanary:leakcanary-android:${versions.leakCanaryVersion}",
|
||||
|
||||
/**
|
||||
* Timber
|
||||
*/
|
||||
timber : "com.jakewharton.timber:timber:${versions.timberVersion}"
|
||||
timber : "com.jakewharton.timber:timber:${versions.timberVersion}"
|
||||
]
|
||||
|
||||
annotationProcessors = [
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
apply from: "$rootProject.projectDir/scripts/default_android_config.gradle"
|
||||
apply from: "$rootProject.projectDir/scripts/sources.gradle"
|
||||
apply from: "$rootProject.projectDir/scripts/flavors.gradle"
|
||||
|
||||
@@ -14,4 +14,21 @@ android {
|
||||
jvmTarget = '1.8'
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user