Feature/styling (#36)

* Working with material styles

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

* Various code style fixes and styles.xml improvements
This commit is contained in:
Melih Aksoy
2019-09-13 14:09:28 +02:00
committed by GitHub
parent 5a10edce17
commit 99a2abf050
58 changed files with 523 additions and 464 deletions
+3 -3
View File
@@ -1,12 +1,12 @@
# Rocket Science # Android-Kotlin-Modulerized-CleanArchitecture
[![Actions](https://github.com/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture/workflows/Android%20CI/badge.svg)](https://github.com/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture/actions) [![CircleCI](https://circleci.com/gh/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture/tree/master.svg?style=svg)](https://circleci.com/gh/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture/tree/master) [![codebeat badge](https://codebeat.co/badges/542fb08a-b3cc-4ff8-b1bb-35a66932f12f)](https://codebeat.co/projects/github-com-melihaksoy-rocketscience-master) [![codecov](https://codecov.io/gh/melihaksoy/RocketScience/branch/master/graph/badge.svg?token=pXPKpV5dz6)](https://codecov.io/gh/melihaksoy/RocketScience) [![Awesome Kotlin Badge](https://kotlin.link/awesome-kotlin.svg)](https://github.com/KotlinBy/awesome-kotlin) [![Actions](https://github.com/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture/workflows/Android%20CI/badge.svg)](https://github.com/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture/actions) [![CircleCI](https://circleci.com/gh/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture/tree/master.svg?style=svg)](https://circleci.com/gh/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture/tree/master) [![codebeat badge](https://codebeat.co/badges/542fb08a-b3cc-4ff8-b1bb-35a66932f12f)](https://codebeat.co/projects/github-com-melihaksoy-rocketscience-master) [![codecov](https://codecov.io/gh/melihaksoy/RocketScience/branch/master/graph/badge.svg?token=pXPKpV5dz6)](https://codecov.io/gh/melihaksoy/RocketScience) [![Awesome Kotlin Badge](https://kotlin.link/awesome-kotlin.svg)](https://github.com/KotlinBy/awesome-kotlin)
RocketScience is a prototype application tries to serve an example for modularization & clean architecture in Android. This is a prototype application tries to serve an example for modularization & clean architecture in Android.
While there are many blogs about good practices, it's hard to come by a complete example that merges these different popular topics & approaches. While there are many blogs about good practices, it's hard to come by a complete example that merges these different popular topics & approaches.
RocketScience takes popular approaches and libraries to create an example on how to actually bind these components with each other. This project takes popular approaches and libraries to create an example on how to actually bind these components with each other.
I'll soon be writing small series about roadmap, challanges, alternatives and try - fails I've encountered during development in [Medium](https://medium.com/@aksoymelihcan). I'll soon be writing small series about roadmap, challanges, alternatives and try - fails I've encountered during development in [Medium](https://medium.com/@aksoymelihcan).
+1 -2
View File
@@ -4,7 +4,6 @@ apply plugin: 'kotlin-kapt'
apply from: "$rootProject.projectDir/scripts/default_android_config.gradle" apply from: "$rootProject.projectDir/scripts/default_android_config.gradle"
apply from: "$rootProject.projectDir/scripts/sources.gradle" apply from: "$rootProject.projectDir/scripts/sources.gradle"
apply from: "$rootProject.projectDir/scripts/flavors.gradle"
android { android {
defaultConfig { defaultConfig {
@@ -26,7 +25,6 @@ dependencies {
implementation project(':features:launches') implementation project(':features:launches')
implementation project(':features:detail') implementation project(':features:detail')
implementation libraries.coroutines
implementation libraries.navigation implementation libraries.navigation
debugImplementation libraries.leakCanary debugImplementation libraries.leakCanary
@@ -37,6 +35,7 @@ dependencies {
compileOnly libraries.retrofit compileOnly libraries.retrofit
compileOnly libraries.room compileOnly libraries.room
compileOnly libraries.paging compileOnly libraries.paging
compileOnly libraries.swipeRefreshLayout
// Need for proper renders in xml previews // Need for proper renders in xml previews
compileOnly libraries.constraintLayout compileOnly libraries.constraintLayout
@@ -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]
) )
+4 -9
View File
@@ -1,20 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data class="MainActivityBinding" />
<LinearLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@null"
android:orientation="vertical"> android:orientation="vertical">
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?android:actionBarSize" android:layout_height="?android:actionBarSize" />
android:background="@color/colorPrimary" />
<fragment <fragment
android:id="@+id/nav_host_fragment" android:id="@+id/nav_host_fragment"
@@ -24,4 +20,3 @@
app:defaultNavHost="true" app:defaultNavHost="true"
app:navGraph="@navigation/nav_main" /> app:navGraph="@navigation/nav_main" />
</LinearLayout> </LinearLayout>
</layout>
+2 -2
View File
@@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.3.41' ext.kotlin_version = '1.3.50'
ext.nav_version = '2.2.0-alpha01' ext.nav_version = '2.2.0-alpha01'
repositories { repositories {
@@ -12,7 +12,7 @@ buildscript {
classpath 'com.android.tools.build:gradle:3.5.0' classpath 'com.android.tools.build:gradle:3.5.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:0.9.18" classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:0.9.18"
classpath "de.mannodermaus.gradle.plugins:android-junit5:1.5.0.0" classpath "de.mannodermaus.gradle.plugins:android-junit5:1.5.1.0"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
+1 -1
View File
@@ -4,7 +4,6 @@ apply plugin: 'kotlin-kapt'
apply from: "$rootProject.projectDir/scripts/default_android_config.gradle" apply from: "$rootProject.projectDir/scripts/default_android_config.gradle"
apply from: "$rootProject.projectDir/scripts/sources.gradle" apply from: "$rootProject.projectDir/scripts/sources.gradle"
apply from: "$rootProject.projectDir/scripts/flavors.gradle"
android { android {
dataBinding { dataBinding {
@@ -23,6 +22,7 @@ dependencies {
implementation libraries.liveDataKTX implementation libraries.liveDataKTX
implementation libraries.navigation implementation libraries.navigation
implementation libraries.picasso implementation libraries.picasso
implementation libraries.material
testImplementation testLibraries.jUnitApi testImplementation testLibraries.jUnitApi
testImplementation testLibraries.mockk testImplementation testLibraries.mockk
@@ -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]
@@ -20,6 +20,12 @@ import com.melih.repository.interactors.base.Reason
*/ */
abstract class BaseFragment<T : ViewDataBinding> : Fragment() { abstract class BaseFragment<T : ViewDataBinding> : Fragment() {
//region Abstractions
@LayoutRes
abstract fun getLayoutId(): Int
//endregion
//region Properties //region Properties
protected lateinit var binding: T protected lateinit var binding: T
@@ -46,8 +52,5 @@ abstract class BaseFragment<T : ViewDataBinding> : Fragment() {
block() block()
}.show() }.show()
} }
@LayoutRes
abstract fun getLayoutId(): Int
//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
@@ -32,7 +32,7 @@ abstract class BasePagingViewModel<T> : ViewModel() {
* 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
} }
} }
@@ -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)
}
+8 -4
View File
@@ -1,7 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="colorPrimary">#008577</color> <color name="primaryColor">#f9a825</color>
<color name="colorPrimaryDark">#00574B</color> <color name="primaryLightColor">#ffd95a</color>
<color name="colorAccent">#D81B60</color> <color name="primaryDarkColor">#c17900</color>
<color name="lightGray">#8F8F8F</color> <color name="secondaryColor">#757575</color>
<color name="secondaryLightColor">#a4a4a4</color>
<color name="secondaryDarkColor">#494949</color>
<color name="primaryTextColor">#000000</color>
<color name="secondaryTextColor">#686868</color>
</resources> </resources>
+2
View File
@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<dimen name="padding_standard">8dp</dimen> <dimen name="padding_standard">8dp</dimen>
<dimen name="padding_large">16dp</dimen>
<dimen name="corner_radius_light">4dp</dimen>
<dimen name="corner_radius_standard">11dp</dimen> <dimen name="corner_radius_standard">11dp</dimen>
</resources> </resources>
+26 -21
View File
@@ -1,35 +1,40 @@
<resources> <resources>
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<!-- Customize your theme here. --> <item name="colorPrimary">@color/primaryColor</item>
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/primaryDarkColor</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorSecondary">@color/secondaryColor</item>
<item name="colorAccent">@color/colorAccent</item> <item name="colorAccent">@color/secondaryDarkColor</item>
<item name="android:textColorPrimary">@color/primaryTextColor</item>
<item name="android:textColorSecondary">@color/secondaryTextColor</item>
<item name="toolbarStyle">@style/AppTheme.ToolbarStyle</item>
<item name="materialCardViewStyle">@style/AppTheme.CardViewStyle</item>
</style>
<!-- Widgets -->
<style name="AppTheme.ToolbarStyle" parent="Widget.MaterialComponents.Toolbar">
<item name="android:background">@color/primaryColor</item>
</style>
<style name="AppTheme.CardViewStyle" parent="Widget.MaterialComponents.CardView">
<item name="android:background">@null</item>
</style> </style>
<!--Styles--> <!--Styles-->
<style name="AppTheme.BaseTextViewStyle" parent="Widget.MaterialComponents.TextView" />
<style name="TitleTextStyle"> <style name="AppTheme.TextViewStyle.Title" parent="AppTheme.BaseTextViewStyle">
<item name="android:textAppearance">@style/TextAppearance.MaterialComponents.Headline5</item>
<item name="android:singleLine">true</item> <item name="android:singleLine">true</item>
</style> </style>
<style name="ShortDescriptionTextStyle"> <style name="AppTheme.TextViewStyle.Description" parent="AppTheme.BaseTextViewStyle">
<item name="android:ellipsize">end</item> <item name="android:ellipsize">end</item>
<item name="android:maxLines">@integer/common_max_lines</item> <item name="android:maxLines">@integer/common_max_lines</item>
<item name="android:gravity">center|left</item> <item name="android:textAppearance">@style/TextAppearance.MaterialComponents.Caption</item>
</style> </style>
<style name="DescriptionTextStyle">
</style>
<!--Text appearances-->
<style name="TitleTextAppearance" parent="TextAppearance.AppCompat.Title" />
<style name="DescriptionTextAppearance" parent="TextAppearance.AppCompat.Body1">
<item name="android:textColor">@color/lightGray</item>
</style>
</resources> </resources>
@@ -3,8 +3,8 @@ 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
@@ -13,8 +13,7 @@ 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
@@ -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
@@ -15,11 +15,11 @@ class DetailViewModel @Inject constructor(
//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,7 +27,7 @@ class DetailViewModel @Inject constructor(
} }
} }
val imageUrl = Transformations.map(successData) { val imageUrl = map(successData) {
it.rocket.imageURL it.rocket.imageURL
} }
//endregion //endregion
@@ -11,10 +11,6 @@
type="com.melih.detail.ui.DetailViewModel" /> type="com.melih.detail.ui.DetailViewModel" />
</data> </data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@@ -36,9 +32,9 @@
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/tvTitle" android:id="@+id/tvTitle"
style="@style/TitleTextStyle" style="@style/AppTheme.TextViewStyle.Title"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/padding_standard" android:layout_marginStart="@dimen/padding_standard"
@@ -47,13 +43,12 @@
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:text="@{viewModel.rocketName}" android:text="@{viewModel.rocketName}"
android:textAppearance="@style/TitleTextAppearance"
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_toBottomOf="@+id/imgRocket"
tools:text="@sample/launches.json/launches/name" /> tools:text="@sample/launches.json/launches/name" />
<TextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/tvDescription" android:id="@+id/tvDescription"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
@@ -64,12 +59,11 @@
android:layout_marginRight="@dimen/padding_standard" android:layout_marginRight="@dimen/padding_standard"
android:layout_marginBottom="@dimen/padding_standard" android:layout_marginBottom="@dimen/padding_standard"
android:text="@{viewModel.description}" android:text="@{viewModel.description}"
android:textAppearance="@style/DescriptionTextAppearance" android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
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/tvTitle" app:layout_constraintTop_toBottomOf="@+id/tvTitle"
tools:text="@sample/launches.json/launches/missions/description" /> tools:text="@sample/launches.json/launches/missions/description" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout> </layout>
+1
View File
@@ -8,6 +8,7 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation libraries.paging implementation libraries.paging
implementation libraries.swipeRefreshLayout
testImplementation testLibraries.coroutinesTest testImplementation testLibraries.coroutinesTest
} }
@@ -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,10 +62,12 @@ 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) {
if (it !is PersistenceEmpty) {
showSnackbarWithAction(it) { showSnackbarWithAction(it) {
viewModel.retry() viewModel.retry()
} }
} }
}
observe(viewModel.pagedList) { observe(viewModel.pagedList) {
launchesAdapter.submitList(it) launchesAdapter.submitList(it)
@@ -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
} }
@@ -19,27 +19,5 @@ class LaunchesViewModel @Inject constructor(
override val config: PagedList.Config override val config: PagedList.Config
get() = launchesPagingConfig get() = launchesPagingConfig
//private val _filteredItems = MediatorLiveData<PagedList<LaunchEntity>>()
//val filteredItems: LiveData<PagedList<LaunchEntity>>
// get() = _filteredItems
// endregion
//init {
// _filteredItems.addSource(pagedList, _filteredItems::setValue)
//}
// region Functions
//fun filterItemListBy(query: String?) {
//
// _filteredItems.value = if (!query.isNullOrBlank()) {
// pagedList.value
// ?.snapshot() as PagedList<LaunchEntity>
// } else {
// pagedList.value
// }
//}
//endregion //endregion
} }
@@ -13,24 +13,23 @@
<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"
android:background="@null"
app:layout_behavior="com.melih.core.utils.SnackbarBehaviour"> 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:background="@null"
android:layoutAnimation="@anim/layout_item_enter" android:layoutAnimation="@anim/layout_item_enter"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/row_launch" /> tools:listitem="@layout/row_launch" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -11,7 +11,7 @@
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"
@@ -19,7 +19,7 @@
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
@@ -31,8 +31,7 @@
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"
@@ -40,38 +39,31 @@
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"
style="@style/ShortDescriptionTextStyle"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="@dimen/padding_standard"
android:layout_marginLeft="@dimen/padding_standard"
android:layout_marginEnd="@dimen/padding_standard"
android:layout_marginRight="@dimen/padding_standard"
android:textAppearance="@style/DescriptionTextAppearance"
app:layout_constraintBottom_toBottomOf="@+id/imgRocket"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imgRocket"
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
tools:text="@sample/launches.json/launches/missions/description" />
<TextView
android:id="@+id/tvTitle" android:id="@+id/tvTitle"
style="@style/TitleTextStyle" style="@style/AppTheme.TextViewStyle.Title"
android:layout_width="0dp" android:layout_width="0dp"
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_marginEnd="@dimen/padding_large"
android:layout_marginEnd="@dimen/padding_standard"
android:layout_marginRight="@dimen/padding_standard"
android:text="@{entity.name}" android:text="@{entity.name}"
android:textAppearance="@style/TitleTextAppearance"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imgRocket" app:layout_constraintStart_toEndOf="@+id/imgRocket"
app:layout_constraintTop_toTopOf="@+id/imgRocket" app:layout_constraintTop_toTopOf="@+id/imgRocket"
tools:text="@sample/launches.json/launches/name" /> tools:text="@sample/launches.json/launches/name" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tvDescription"
style="@style/AppTheme.TextViewStyle.Description"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="@dimen/padding_standard"
android:layout_marginEnd="@dimen/padding_large"
app:layout_constraintBottom_toBottomOf="@+id/imgRocket"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imgRocket"
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
tools:text="@sample/launches.json/launches/missions/description" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView> </com.google.android.material.card.MaterialCardView>
</layout> </layout>
+2 -2
View File
@@ -1,6 +1,6 @@
#Thu Jun 13 10:50:25 CEST 2019 #Wed Sep 04 15:39:59 CEST 2019
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-all.zip
@@ -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,
@@ -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
@@ -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
} }
@@ -41,6 +41,8 @@ internal class ApiImpl @Inject constructor() : Api {
} }
//endregion //endregion
//region Functions
override suspend fun getNextLaunches( override suspend fun getNextLaunches(
count: Int, count: Int,
offset: Int offset: 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,11 +10,17 @@ 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
@@ -10,11 +10,17 @@ 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
@@ -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)
} }
@@ -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,6 +28,7 @@ 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
@@ -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) {
@@ -16,6 +16,7 @@ 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)
+1
View File
@@ -2,6 +2,7 @@ apply from: "$rootProject.projectDir/scripts/default_dependencies.gradle"
android { android {
compileSdkVersion versions.compileSdkVersion compileSdkVersion versions.compileSdkVersion
buildToolsVersion versions.buildToolsVersion
defaultConfig { defaultConfig {
minSdkVersion versions.minSdkVersion minSdkVersion versions.minSdkVersion
+11 -8
View File
@@ -2,21 +2,22 @@ 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.2.0-rc01", workManagerVersion : "2.3.0-alpha01",
constraintLayoutVesion: "2.0.0-beta1", constraintLayoutVesion : "2.0.0-beta2",
cardViewVersion : "1.0.0", cardViewVersion : "1.0.0",
recyclerViewVersion : "1.1.0-beta02", recyclerViewVersion : "1.1.0-beta03",
pagingVersion : "2.1.0", pagingVersion : "2.1.0",
viewPagerVersion : "1.0.0-beta03", viewPagerVersion : "1.0.0-beta03",
materialVersion : "1.1.0-alpha09",
swipeRefreshLayoutVersion: "1.1.0-alpha02",
collectionVersion : "1.1.0", collectionVersion : "1.1.0",
roomVersion : "2.2.0-alpha02", roomVersion : "2.2.0-rc01",
daggerVersion : "2.24", daggerVersion : "2.24",
okHttpVersion : "4.0.1", okHttpVersion : "4.0.1",
retrofitVersion : "2.6.1", retrofitVersion : "2.6.1",
@@ -41,6 +42,7 @@ ext {
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
@@ -61,6 +63,7 @@ ext {
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
-18
View File
@@ -1,18 +0,0 @@
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
testCoverageEnabled = true
}
dev {
initWith debug
}
}
}
-1
View File
@@ -1,3 +1,2 @@
apply from: "$rootProject.projectDir/scripts/default_android_config.gradle" apply from: "$rootProject.projectDir/scripts/default_android_config.gradle"
apply from: "$rootProject.projectDir/scripts/sources.gradle" apply from: "$rootProject.projectDir/scripts/sources.gradle"
apply from: "$rootProject.projectDir/scripts/flavors.gradle"
+17
View File
@@ -14,4 +14,21 @@ android {
jvmTarget = '1.8' jvmTarget = '1.8'
freeCompilerArgs += "-Xuse-experimental=kotlin.Experimental" freeCompilerArgs += "-Xuse-experimental=kotlin.Experimental"
} }
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
testCoverageEnabled = true
}
dev {
initWith debug
}
}
} }