mirror of
https://github.com/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture.git
synced 2026-04-22 08:48:31 +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)
|
[](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.
|
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).
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.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)
|
|
||||||
|
|||||||
@@ -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"?>
|
<?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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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
|
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
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
@@ -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/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"
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user