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)
RocketScience is a prototype application tries to serve an example for modularization & clean architecture in Android.
This is a prototype application tries to serve an example for modularization & clean architecture in Android.
While there are many blogs about good practices, it's hard to come by a complete example that merges these different popular topics & approaches.
RocketScience takes popular approaches and libraries to create an example on how to actually bind these components with each other.
This project takes popular approaches and libraries to create an example on how to actually bind these components with each other.
I'll soon be writing small series about roadmap, challanges, alternatives and try - fails I've encountered during development in [Medium](https://medium.com/@aksoymelihcan).
+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/sources.gradle"
apply from: "$rootProject.projectDir/scripts/flavors.gradle"
android {
defaultConfig {
@@ -26,7 +25,6 @@ dependencies {
implementation project(':features:launches')
implementation project(':features:detail')
implementation libraries.coroutines
implementation libraries.navigation
debugImplementation libraries.leakCanary
@@ -37,6 +35,7 @@ dependencies {
compileOnly libraries.retrofit
compileOnly libraries.room
compileOnly libraries.paging
compileOnly libraries.swipeRefreshLayout
// Need for proper renders in xml previews
compileOnly libraries.constraintLayout
@@ -1,24 +1,22 @@
package com.melih.rocketscience
import android.os.Bundle
import androidx.databinding.DataBindingUtil
import androidx.appcompat.widget.Toolbar
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.ui.NavigationUI
import com.melih.rocketscience.databinding.MainActivityBinding
import dagger.android.support.DaggerAppCompatActivity
class MainActivity : DaggerAppCompatActivity() {
private lateinit var binding: MainActivityBinding
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
setContentView(R.layout.activity_main)
navController = findNavController(R.id.nav_host_fragment)
NavigationUI.setupWithNavController(binding.toolbar, navController)
NavigationUI.setupWithNavController(findViewById<Toolbar>(R.id.toolbar), navController)
}
override fun onSupportNavigateUp(): Boolean {
@@ -8,8 +8,10 @@ import dagger.android.AndroidInjector
@AppScope
@Component(
modules = [AndroidInjectionModule::class,
AppModule::class],
modules = [
AndroidInjectionModule::class,
AppModule::class
],
dependencies = [CoreComponent::class]
)
+4 -9
View File
@@ -1,20 +1,16 @@
<?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
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@null"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?android:actionBarSize"
android:background="@color/colorPrimary" />
android:layout_height="?android:actionBarSize" />
<fragment
android:id="@+id/nav_host_fragment"
@@ -24,4 +20,3 @@
app:defaultNavHost="true"
app:navGraph="@navigation/nav_main" />
</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.
buildscript {
ext.kotlin_version = '1.3.41'
ext.kotlin_version = '1.3.50'
ext.nav_version = '2.2.0-alpha01'
repositories {
@@ -12,7 +12,7 @@ buildscript {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:0.9.18"
classpath "de.mannodermaus.gradle.plugins:android-junit5:1.5.0.0"
classpath "de.mannodermaus.gradle.plugins:android-junit5:1.5.1.0"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
+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/sources.gradle"
apply from: "$rootProject.projectDir/scripts/flavors.gradle"
android {
dataBinding {
@@ -23,6 +22,7 @@ dependencies {
implementation libraries.liveDataKTX
implementation libraries.navigation
implementation libraries.picasso
implementation libraries.material
testImplementation testLibraries.jUnitApi
testImplementation testLibraries.mockk
@@ -14,7 +14,7 @@ import javax.inject.Inject
* Parent of fragments which has injections. Aim is to seperate [BaseFragment] functionality for fragments which
* won't need any injection.
*
* Note that fragments that extends from [BaseDaggerFragment] should contribute android injector.
* Note that fragments that extends from [BaseDaggerFragment] should contribute their injector.
*
* This class provides [viewModelFactory] which serves as factory for view models
* in the project. It's injected by map of view models that this app is serving. Check [ViewModelFactory]
@@ -20,6 +20,12 @@ import com.melih.repository.interactors.base.Reason
*/
abstract class BaseFragment<T : ViewDataBinding> : Fragment() {
//region Abstractions
@LayoutRes
abstract fun getLayoutId(): Int
//endregion
//region Properties
protected lateinit var binding: T
@@ -46,8 +52,5 @@ abstract class BaseFragment<T : ViewDataBinding> : Fragment() {
block()
}.show()
}
@LayoutRes
abstract fun getLayoutId(): Int
//endregion
}
@@ -15,6 +15,8 @@ abstract class BasePagingListAdapter<T>(
private val clickListener: (T) -> Unit
) : PagedListAdapter<T, BaseViewHolder<T>>(callback) {
//region Abstractions
/**
* This method will be called to create view holder to obfuscate layout inflation creation / process
*
@@ -27,6 +29,9 @@ abstract class BasePagingListAdapter<T>(
parent: ViewGroup,
viewType: Int
): BaseViewHolder<T>
//endregion
//region Functions
/**
* [createViewHolder] will provide holders, no need to override this
@@ -51,6 +56,7 @@ abstract class BasePagingListAdapter<T>(
holder.bind(item)
}
}
//endregion
}
/**
@@ -58,6 +64,8 @@ abstract class BasePagingListAdapter<T>(
*/
abstract class BaseViewHolder<T>(binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) {
//region Functions
/**
* Items are delivered to [bind] via [BaseListAdapter.onBindViewHolder]
*
@@ -65,4 +73,5 @@ abstract class BaseViewHolder<T>(binding: ViewDataBinding) : RecyclerView.ViewHo
* @param position position from adapter
*/
abstract fun bind(item: T)
//endregion
}
@@ -1,7 +1,7 @@
package com.melih.core.base.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.Transformations.switchMap
import androidx.lifecycle.ViewModel
import androidx.paging.PagedList
import androidx.paging.toLiveData
@@ -32,7 +32,7 @@ abstract class BasePagingViewModel<T> : ViewModel() {
* Observe [stateData] to get notified of state of data
*/
val stateData: LiveData<State> by lazy {
Transformations.switchMap(factory.currentSource) {
switchMap(factory.currentSource) {
it.stateData
}
}
@@ -41,7 +41,7 @@ abstract class BasePagingViewModel<T> : ViewModel() {
* Observe [errorData] to get notified if an error occurs
*/
val errorData: LiveData<Reason> by lazy {
Transformations.switchMap(factory.currentSource) {
switchMap(factory.currentSource) {
it.reasonData
}
}
@@ -0,0 +1,27 @@
package com.melih.core.extensions
import androidx.recyclerview.widget.DiffUtil
/**
* Get [diff callback][DiffUtil.ItemCallback] for given type based on provided checker.
* It uses [itemCheck] for both [DiffUtil.ItemCallback.areItemsTheSame] and [DiffUtil.ItemCallback.areContentsTheSame].
*/
inline fun <T> createDiffCallback(crossinline itemCheck: (oldItem: T, newItem: T) -> Boolean) = createDiffCallback(itemCheck, itemCheck)
/**
* Get [diff callback][DiffUtil.ItemCallback] for given type based on provided checker
*/
inline fun <T> createDiffCallback(
crossinline itemCheck: (oldItem: T, newItem: T) -> Boolean,
crossinline contentCheck: (oldItem: T, newItem: T) -> Boolean
) = object : DiffUtil.ItemCallback<T>() {
//region Functions
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean =
itemCheck(oldItem, newItem)
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean =
contentCheck(oldItem, newItem)
//endregion
}
@@ -3,9 +3,6 @@ package com.melih.core.extensions
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
/**
* Reduces required boilerplate code to observe a live data
@@ -16,13 +13,3 @@ import androidx.lifecycle.ViewModelProviders
fun <T> Fragment.observe(data: LiveData<T>, block: (T) -> Unit) {
data.observe(this, Observer(block))
}
/**
* Method for getting viewModel from factory and run a block over it if required for easy access
*
* crossinline for unwanted returns
*/
inline fun <reified T : ViewModel> ViewModelProvider.Factory.createFor(
fragment: Fragment,
crossinline block: T.() -> Unit = {}
): T = ViewModelProviders.of(fragment, this)[T::class.java].apply(block)
@@ -1,14 +0,0 @@
package com.melih.core.extensions
import androidx.recyclerview.widget.DiffUtil
/**
* Get [diff callback][DiffUtil.ItemCallback] for given type based on provided checker
*/
inline fun <T> getDiffCallbackForType(crossinline itemCheck: (oldItem: T, newItem: T) -> Boolean) = object : DiffUtil.ItemCallback<T>() {
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean =
itemCheck(oldItem, newItem)
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean =
itemCheck(oldItem, newItem)
}
+8 -4
View File
@@ -1,7 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#008577</color>
<color name="colorPrimaryDark">#00574B</color>
<color name="colorAccent">#D81B60</color>
<color name="lightGray">#8F8F8F</color>
<color name="primaryColor">#f9a825</color>
<color name="primaryLightColor">#ffd95a</color>
<color name="primaryDarkColor">#c17900</color>
<color name="secondaryColor">#757575</color>
<color name="secondaryLightColor">#a4a4a4</color>
<color name="secondaryDarkColor">#494949</color>
<color name="primaryTextColor">#000000</color>
<color name="secondaryTextColor">#686868</color>
</resources>
+2
View File
@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="padding_standard">8dp</dimen>
<dimen name="padding_large">16dp</dimen>
<dimen name="corner_radius_light">4dp</dimen>
<dimen name="corner_radius_standard">11dp</dimen>
</resources>
+26 -21
View File
@@ -1,35 +1,40 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<item name="colorPrimary">@color/primaryColor</item>
<item name="colorPrimaryDark">@color/primaryDarkColor</item>
<item name="colorSecondary">@color/secondaryColor</item>
<item name="colorAccent">@color/secondaryDarkColor</item>
<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>
<!--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>
</style>
<style name="ShortDescriptionTextStyle">
<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:gravity">center|left</item>
<item name="android:textAppearance">@style/TextAppearance.MaterialComponents.Caption</item>
</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>
@@ -3,8 +3,8 @@ package com.melih.detail.ui
import android.os.Bundle
import android.text.method.ScrollingMovementMethod
import android.view.View
import androidx.fragment.app.viewModels
import com.melih.core.base.lifecycle.BaseDaggerFragment
import com.melih.core.extensions.createFor
import com.melih.core.extensions.observe
import com.melih.detail.R
import com.melih.detail.databinding.DetailBinding
@@ -13,8 +13,7 @@ class DetailFragment : BaseDaggerFragment<DetailBinding>() {
//region Properties
private val viewModel: DetailViewModel
get() = viewModelFactory.createFor(this)
private val viewModel by viewModels<DetailViewModel> { viewModelFactory }
//endregion
//region Functions
@@ -1,6 +1,6 @@
package com.melih.detail.ui
import androidx.lifecycle.Transformations
import androidx.lifecycle.Transformations.map
import com.melih.core.base.viewmodel.BaseViewModel
import com.melih.repository.entities.LaunchEntity
import com.melih.repository.interactors.GetLaunchDetails
@@ -15,11 +15,11 @@ class DetailViewModel @Inject constructor(
//region Properties
val rocketName = Transformations.map(successData) {
val rocketName = map(successData) {
it.rocket.name
}
val description = Transformations.map(successData) {
val description = map(successData) {
if (it.missions.isEmpty()) {
""
} else {
@@ -27,7 +27,7 @@ class DetailViewModel @Inject constructor(
}
}
val imageUrl = Transformations.map(successData) {
val imageUrl = map(successData) {
it.rocket.imageURL
}
//endregion
@@ -11,10 +11,6 @@
type="com.melih.detail.ui.DetailViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
@@ -36,9 +32,9 @@
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars[14]" />
<TextView
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tvTitle"
style="@style/TitleTextStyle"
style="@style/AppTheme.TextViewStyle.Title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/padding_standard"
@@ -47,13 +43,12 @@
android:layout_marginEnd="@dimen/padding_standard"
android:layout_marginRight="@dimen/padding_standard"
android:text="@{viewModel.rocketName}"
android:textAppearance="@style/TitleTextAppearance"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imgRocket"
tools:text="@sample/launches.json/launches/name" />
<TextView
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tvDescription"
android:layout_width="0dp"
android:layout_height="0dp"
@@ -64,12 +59,11 @@
android:layout_marginRight="@dimen/padding_standard"
android:layout_marginBottom="@dimen/padding_standard"
android:text="@{viewModel.description}"
android:textAppearance="@style/DescriptionTextAppearance"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
tools:text="@sample/launches.json/launches/missions/description" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
+1
View File
@@ -8,6 +8,7 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation libraries.paging
implementation libraries.swipeRefreshLayout
testImplementation testLibraries.coroutinesTest
}
@@ -2,29 +2,29 @@ package com.melih.list.ui
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.melih.core.actions.openDetail
import com.melih.core.base.lifecycle.BaseDaggerFragment
import com.melih.core.extensions.createFor
import com.melih.core.extensions.observe
import com.melih.list.R
import com.melih.list.databinding.ListBinding
import com.melih.list.ui.adapters.LaunchesAdapter
import com.melih.list.ui.vm.LaunchesViewModel
import com.melih.repository.entities.LaunchEntity
import com.melih.repository.interactors.base.PersistenceEmpty
import com.melih.repository.interactors.base.State
class LaunchesFragment : BaseDaggerFragment<ListBinding>(), SwipeRefreshLayout.OnRefreshListener {
//region Properties
private val viewModel: LaunchesViewModel
get() = viewModelFactory.createFor(this)
private val viewModel by viewModels<LaunchesViewModel> { viewModelFactory }
private val launchesAdapter = LaunchesAdapter(::onItemSelected)
//endregion
// region Functions
//region Lifecyle
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -36,6 +36,23 @@ class LaunchesFragment : BaseDaggerFragment<ListBinding>(), SwipeRefreshLayout.O
observeDataChanges()
}
override fun onResume() {
super.onResume()
// Workaround for SwipeRefreshLayout leak -> https://issuetracker.google.com/issues/136153683
binding.swipeRefreshLayout.isEnabled = true
}
override fun onPause() {
super.onPause()
// Workaround for SwipeRefreshLayout leak -> https://issuetracker.google.com/issues/136153683
binding.swipeRefreshLayout.isEnabled = false
}
//endregion
//region Functions
private fun observeDataChanges() {
// Observing state to show loading
@@ -45,10 +62,12 @@ class LaunchesFragment : BaseDaggerFragment<ListBinding>(), SwipeRefreshLayout.O
// Observing error to show toast with retry action
observe(viewModel.errorData) {
if (it !is PersistenceEmpty) {
showSnackbarWithAction(it) {
viewModel.retry()
}
}
}
observe(viewModel.pagedList) {
launchesAdapter.submitList(it)
@@ -4,25 +4,30 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import com.melih.core.base.recycler.BasePagingListAdapter
import com.melih.core.base.recycler.BaseViewHolder
import com.melih.core.extensions.getDiffCallbackForType
import com.melih.core.extensions.createDiffCallback
import com.melih.list.databinding.LaunchRowBinding
import com.melih.repository.entities.LaunchEntity
class LaunchesAdapter(itemClickListener: (LaunchEntity) -> Unit) : BasePagingListAdapter<LaunchEntity>(
getDiffCallbackForType { oldItem, newItem -> oldItem.id == newItem.id },
createDiffCallback { oldItem, newItem -> oldItem.id == newItem.id },
itemClickListener
) {
//region Functions
override fun createViewHolder(
inflater: LayoutInflater,
parent: ViewGroup,
viewType: Int
): BaseViewHolder<LaunchEntity> =
LaunchesViewHolder(LaunchRowBinding.inflate(inflater, parent, false))
//endregion
}
class LaunchesViewHolder(private val binding: LaunchRowBinding) : BaseViewHolder<LaunchEntity>(binding) {
//region Functions
override fun bind(item: LaunchEntity) {
binding.entity = item
@@ -31,4 +36,5 @@ class LaunchesViewHolder(private val binding: LaunchRowBinding) : BaseViewHolder
binding.executePendingBindings()
}
//endregion
}
@@ -8,6 +8,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
/**
* Uses [GetLaunches] to get data for pagination
*/
class LaunchesPagingSource @Inject constructor(
private val getLaunches: GetLaunches,
private val getLaunchesParams: GetLaunches.Params
@@ -10,5 +10,8 @@ class LaunchesPagingSourceFactory @Inject constructor(
private val sourceProvider: Provider<LaunchesPagingSource>
) : BasePagingFactory<LaunchEntity>() {
//region Functions
override fun createSource(): BasePagingDataSource<LaunchEntity> = sourceProvider.get()
//endregion
}
@@ -19,27 +19,5 @@ class LaunchesViewModel @Inject constructor(
override val config: PagedList.Config
get() = launchesPagingConfig
//private val _filteredItems = MediatorLiveData<PagedList<LaunchEntity>>()
//val filteredItems: LiveData<PagedList<LaunchEntity>>
// get() = _filteredItems
// endregion
//init {
// _filteredItems.addSource(pagedList, _filteredItems::setValue)
//}
// region Functions
//fun filterItemListBy(query: String?) {
//
// _filteredItems.value = if (!query.isNullOrBlank()) {
// pagedList.value
// ?.snapshot() as PagedList<LaunchEntity>
// } else {
// pagedList.value
// }
//}
//endregion
}
@@ -13,24 +13,23 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:background="@null">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@null"
app:layout_behavior="com.melih.core.utils.SnackbarBehaviour">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rocketList"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@null"
android:layoutAnimation="@anim/layout_item_enter"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/row_launch" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -11,7 +11,7 @@
type="com.melih.repository.entities.LaunchEntity" />
</data>
<androidx.cardview.widget.CardView
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/padding_standard"
@@ -19,7 +19,7 @@
android:layout_marginTop="@dimen/padding_standard"
android:layout_marginEnd="@dimen/padding_standard"
android:layout_marginRight="@dimen/padding_standard"
app:cardCornerRadius="@dimen/corner_radius_standard"
android:background="@null"
app:cardElevation="10dp">
<androidx.constraintlayout.widget.ConstraintLayout
@@ -31,8 +31,7 @@
imageUrl="@{entity.rocket.imageURL}"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginStart="@dimen/padding_standard"
android:layout_marginLeft="@dimen/padding_standard"
android:layout_marginStart="@dimen/padding_large"
android:contentDescription="@string/cd_rocket_image"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
@@ -40,38 +39,31 @@
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars[14]" />
<TextView
android:id="@+id/tvDescription"
style="@style/ShortDescriptionTextStyle"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="@dimen/padding_standard"
android:layout_marginLeft="@dimen/padding_standard"
android:layout_marginEnd="@dimen/padding_standard"
android:layout_marginRight="@dimen/padding_standard"
android:textAppearance="@style/DescriptionTextAppearance"
app:layout_constraintBottom_toBottomOf="@+id/imgRocket"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imgRocket"
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
tools:text="@sample/launches.json/launches/missions/description" />
<TextView
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tvTitle"
style="@style/TitleTextStyle"
style="@style/AppTheme.TextViewStyle.Title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/padding_standard"
android:layout_marginLeft="@dimen/padding_standard"
android:layout_marginEnd="@dimen/padding_standard"
android:layout_marginRight="@dimen/padding_standard"
android:layout_marginEnd="@dimen/padding_large"
android:text="@{entity.name}"
android:textAppearance="@style/TitleTextAppearance"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imgRocket"
app:layout_constraintTop_toTopOf="@+id/imgRocket"
tools:text="@sample/launches.json/launches/name" />
<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.cardview.widget.CardView>
</com.google.android.material.card.MaterialCardView>
</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
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-all.zip
@@ -1,5 +1,4 @@
package com.melih.repository
const val DEFAULT_NAME = "Default name"
const val EMPTY_STRING = ""
const val DEFAULT_IMAGE_SIZE = 480
internal const val DEFAULT_NAME = "Default name"
internal const val EMPTY_STRING = ""
@@ -4,10 +4,13 @@ import com.melih.repository.entities.LaunchEntity
import com.melih.repository.interactors.base.Result
/**
* Abstract class to create contract in sources to seperate low level business logic from build and return type
* Contract for sources to seperate low level business logic from build and return type
*/
abstract class Repository {
//region Abstractions
internal abstract suspend fun getNextLaunches(count: Int, page: Int): Result<List<LaunchEntity>>
internal abstract suspend fun getLaunchById(id: Long): Result<LaunchEntity>
//endregion
}
@@ -18,11 +18,16 @@ import javax.inject.Inject
@UseExperimental(ExperimentalCoroutinesApi::class)
class GetLaunchDetails @Inject constructor() : BaseInteractor<LaunchEntity, GetLaunchDetails.Params>() {
//region Properties
@field:Inject
internal lateinit var networkSource: NetworkSource
@field:Inject
internal lateinit var persistenceSource: PersistenceSource
//endregion
//region Functions
override suspend fun FlowCollector<Result<LaunchEntity>>.run(params: Params) {
val result = persistenceSource.getLaunchById(params.id)
@@ -42,6 +47,7 @@ class GetLaunchDetails @Inject constructor() : BaseInteractor<LaunchEntity, GetL
emit(result)
}
}
//endregion
data class Params(
val id: Long
@@ -19,11 +19,16 @@ const val DEFAULT_LAUNCHES_AMOUNT = 15
@UseExperimental(ExperimentalCoroutinesApi::class)
class GetLaunches @Inject constructor() : BaseInteractor<List<LaunchEntity>, GetLaunches.Params>() {
//region Properties
@field:Inject
internal lateinit var networkSource: NetworkSource
@field:Inject
internal lateinit var persistenceSource: PersistenceSource
//endregion
//region Functions
override suspend fun FlowCollector<Result<List<LaunchEntity>>>.run(params: Params) {
@@ -40,6 +45,7 @@ class GetLaunches @Inject constructor() : BaseInteractor<List<LaunchEntity>, Get
}
}
}
//endregion
data class Params(
val count: Int = DEFAULT_LAUNCHES_AMOUNT,
@@ -9,6 +9,8 @@ import com.melih.repository.R
*/
sealed class Reason(@StringRes val messageRes: Int)
//region Subclasses
class NetworkError : Reason(R.string.reason_network)
class EmptyResultError : Reason(R.string.reason_empty_body)
class GenericError : Reason(R.string.reason_generic)
@@ -16,3 +18,4 @@ class ResponseError : Reason(R.string.reason_response)
class TimeoutError : Reason(R.string.reason_timeout)
class PersistenceEmpty : Reason(R.string.reason_persistance_empty)
class NoNetworkPersistenceEmpty : Reason(R.string.reason_no_network_persistance_empty)
//endregion
@@ -12,6 +12,8 @@ import retrofit2.http.Query
*/
internal interface Api {
//region Get
@GET("launch/next/{count}")
suspend fun getNextLaunches(
@Path("count") count: Int,
@@ -22,4 +24,5 @@ internal interface Api {
suspend fun getLaunchById(
@Path("id") id: Long
): Response<LaunchEntity>
//endregion
}
@@ -41,6 +41,8 @@ internal class ApiImpl @Inject constructor() : Api {
}
//endregion
//region Functions
override suspend fun getNextLaunches(
count: Int,
offset: Int
@@ -51,4 +53,5 @@ internal class ApiImpl @Inject constructor() : Api {
id: Long
): Response<LaunchEntity> =
service.getLaunchById(id)
//endregion
}
@@ -28,6 +28,8 @@ const val DB_NAME = "LaunchesDB"
)
internal abstract class LaunchesDatabase : RoomDatabase() {
//region Companion
companion object {
private lateinit var instance: LaunchesDatabase
@@ -42,6 +44,10 @@ internal abstract class LaunchesDatabase : RoomDatabase() {
}
}
//endregion
//region Abstractions
internal abstract val launchesDao: LaunchesDao
//endregion
}
@@ -10,11 +10,17 @@ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
*/
abstract class BaseConverter<T> {
//region Abstractions
abstract fun getAdapter(moshi: Moshi): JsonAdapter<T>
//endregion
//region Properties
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
abstract fun getAdapter(moshi: Moshi): JsonAdapter<T>
//endregion
//region Functions
@@ -10,11 +10,17 @@ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
*/
abstract class BaseListConverter<T> {
//region Abstractions
abstract fun getAdapter(moshi: Moshi): JsonAdapter<List<T>>
//endregion
//region Properties
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
abstract fun getAdapter(moshi: Moshi): JsonAdapter<List<T>>
//endregion
//region Functions
@@ -9,6 +9,7 @@ import com.squareup.moshi.Moshi
* Converts [location][LocationEntity]
*/
class LocationConverter : BaseConverter<LocationEntity>() {
override fun getAdapter(moshi: Moshi): JsonAdapter<LocationEntity> =
LocationEntityJsonAdapter(moshi)
}
@@ -9,6 +9,7 @@ import com.squareup.moshi.Moshi
* Converts [rocket][RocketEntity]
*/
class RocketConverter : BaseConverter<RocketEntity>() {
override fun getAdapter(moshi: Moshi): JsonAdapter<RocketEntity> =
RocketEntityJsonAdapter(moshi)
}
@@ -1,7 +1,6 @@
package com.melih.repository.sources
import android.net.NetworkInfo
import com.melih.repository.DEFAULT_IMAGE_SIZE
import com.melih.repository.Repository
import com.melih.repository.entities.LaunchEntity
import com.melih.repository.interactors.DEFAULT_LAUNCHES_AMOUNT
@@ -19,6 +18,8 @@ import java.io.IOException
import javax.inject.Inject
import javax.inject.Provider
private const val DEFAULT_IMAGE_SIZE = 480
/**
* NetworkSource for fetching results using api and wrapping them as contracted in [repository][Repository],
* returning either [failure][Failure] with proper [reason][Reason] or [success][Success] with data
@@ -27,6 +28,7 @@ internal class NetworkSource @Inject constructor(
private val apiImpl: ApiImpl,
private val networkInfoProvider: Provider<NetworkInfo>
) : Repository() {
//region Properties
private val isNetworkConnected: Boolean
@@ -73,8 +75,8 @@ internal class NetworkSource @Inject constructor(
}
}
private suspend inline fun <T, R> safeExecute(
block: suspend () -> Response<T>,
private inline fun <T, R> safeExecute(
block: () -> Response<T>,
transform: (T) -> R
) =
if (isNetworkConnected) {
@@ -16,6 +16,7 @@ import javax.inject.Inject
internal class PersistenceSource @Inject constructor(
ctx: Context
) : Repository() {
//region Functions
private val launchesDatabase = LaunchesDatabase.getInstance(ctx)
+1
View File
@@ -2,6 +2,7 @@ apply from: "$rootProject.projectDir/scripts/default_dependencies.gradle"
android {
compileSdkVersion versions.compileSdkVersion
buildToolsVersion versions.buildToolsVersion
defaultConfig {
minSdkVersion versions.minSdkVersion
+11 -8
View File
@@ -2,21 +2,22 @@ ext {
versions = [
minSdkVersion : 21,
compileSdkVersion : 28,
targetSdkVersion : 28,
buildToolsVersion : "28.0.3",
supportLibraryVersion : "28.0.0",
compileSdkVersion : 29,
targetSdkVersion : 29,
buildToolsVersion : "29.0.2",
appCompatVersion : "1.1.0-rc01",
lifecycleVersion : "2.2.0-alpha03",
fragmentVersion : "1.2.0-alpha02",
workManagerVersion : "2.2.0-rc01",
constraintLayoutVesion: "2.0.0-beta1",
workManagerVersion : "2.3.0-alpha01",
constraintLayoutVesion : "2.0.0-beta2",
cardViewVersion : "1.0.0",
recyclerViewVersion : "1.1.0-beta02",
recyclerViewVersion : "1.1.0-beta03",
pagingVersion : "2.1.0",
viewPagerVersion : "1.0.0-beta03",
materialVersion : "1.1.0-alpha09",
swipeRefreshLayoutVersion: "1.1.0-alpha02",
collectionVersion : "1.1.0",
roomVersion : "2.2.0-alpha02",
roomVersion : "2.2.0-rc01",
daggerVersion : "2.24",
okHttpVersion : "4.0.1",
retrofitVersion : "2.6.1",
@@ -41,6 +42,7 @@ ext {
constraintLayout : "androidx.constraintlayout:constraintlayout:${versions.constraintLayoutVesion}",
multixDex : "androidx.multidex:multidex:2.0.1",
fragment : "androidx.fragment:fragment-ktx:${versions.fragmentVersion}",
material : "com.google.android.material:material:${versions.materialVersion}",
/**
* Jetpack
@@ -61,6 +63,7 @@ ext {
paging : "androidx.paging:paging-runtime-ktx:${versions.pagingVersion}",
viewPager : "androidx.viewpager2:viewpager2:${versions.viewPagerVersion}",
collection : "androidx.collection:collection-ktx:${versions.collectionVersion}",
swipeRefreshLayout: "androidx.swiperefreshlayout:swiperefreshlayout:${versions.swipeRefreshLayoutVersion}",
/**
* Kotlin
-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/sources.gradle"
apply from: "$rootProject.projectDir/scripts/flavors.gradle"
+17
View File
@@ -14,4 +14,21 @@ android {
jvmTarget = '1.8'
freeCompilerArgs += "-Xuse-experimental=kotlin.Experimental"
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
testCoverageEnabled = true
}
dev {
initWith debug
}
}
}