Milestones/ms1 (#16)

* Closes #11

* Closes #13

* Closes #17

* Closes #18

* Closes #19

* Closes #6

* Closes #3

* Closes #12

* Closes #15
This commit is contained in:
Melihcan Aksoy
2019-07-26 13:38:51 +02:00
committed by Melih Aksoy
parent 11889446cb
commit 625776609d
103 changed files with 4367 additions and 716 deletions

View File

@@ -0,0 +1,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.melih.list">
<application>
<activity
android:name="com.melih.list.ui.LaunchesActivity"
android:theme="@style/AppTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,27 @@
package com.melih.list.di
import com.melih.list.di.modules.LaunchesBinds
import com.melih.list.di.modules.LaunchesProvides
import com.melih.list.di.scopes.LaunchesFragmentScope
import com.melih.list.ui.LaunchesFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
/**
* Contributes fragments & view models in this module
*/
@Module
abstract class LaunchesContributor {
// region Contributes
@ContributesAndroidInjector(
modules = [
LaunchesProvides::class,
LaunchesBinds::class
]
)
@LaunchesFragmentScope
abstract fun launchesFragment(): LaunchesFragment
// endregion
}

View File

@@ -0,0 +1,24 @@
package com.melih.list.di
import com.melih.list.di.scopes.LaunchesScope
import com.melih.list.ui.LaunchesActivity
import dagger.Module
import dagger.android.ContributesAndroidInjector
/**
* Contributes fragments & view models in this module
*/
@Module
abstract class LaunchesFeatureModule {
// region Contributes
@ContributesAndroidInjector(
modules = [
LaunchesContributor::class
]
)
@LaunchesScope
abstract fun launchesActivity(): LaunchesActivity
// endregion
}

View File

@@ -0,0 +1,20 @@
package com.melih.list.di.modules
import androidx.lifecycle.ViewModel
import com.melih.core.di.keys.ViewModelKey
import com.melih.list.ui.vm.LaunchesViewModel
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap
@Module
abstract class LaunchesBinds {
// region ViewModels
@Binds
@IntoMap
@ViewModelKey(LaunchesViewModel::class)
abstract fun listViewModel(listViewModel: LaunchesViewModel): ViewModel
// endregion
}

View File

@@ -0,0 +1,24 @@
package com.melih.list.di.modules
import androidx.paging.Config
import com.melih.repository.interactors.DEFAULT_LAUNCHES_AMOUNT
import com.melih.repository.interactors.GetLaunches
import dagger.Module
import dagger.Provides
@Module
class LaunchesProvides {
/**
* Provides lauches, using default value of 15
*/
@Provides
fun provideGetLaunchesParams() = GetLaunches.Params(page = 0)
@Provides
fun getPagingConfig() = Config(
DEFAULT_LAUNCHES_AMOUNT,
prefetchDistance = 2,
enablePlaceholders = false
)
}

View File

@@ -0,0 +1,7 @@
package com.melih.list.di.scopes
import javax.inject.Scope
@Scope
@Retention(AnnotationRetention.BINARY)
annotation class LaunchesFragmentScope

View File

@@ -0,0 +1,7 @@
package com.melih.list.di.scopes
import javax.inject.Scope
@Scope
@Retention(AnnotationRetention.BINARY)
annotation class LaunchesScope

View File

@@ -0,0 +1,26 @@
package com.melih.list.ui
import android.os.Bundle
import androidx.navigation.fragment.NavHostFragment
import com.melih.core.base.lifecycle.BaseActivity
import com.melih.list.R
import com.melih.list.databinding.LaunchesActivityBinding
class LaunchesActivity : BaseActivity<LaunchesActivityBinding>() {
// region Functions
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setSupportActionBar(binding.toolbar)
}
override fun getLayoutId(): Int = R.layout.activity_launches
override fun createNavHostFragment() =
NavHostFragment.create(R.navigation.nav_launches)
override fun addNavHostTo(): Int = R.id.container
// endregion
}

View File

@@ -0,0 +1,97 @@
package com.melih.list.ui
import android.os.Bundle
import android.view.View
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.melih.core.actions.Actions
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.State
class LaunchesFragment : BaseDaggerFragment<ListBinding>(), SwipeRefreshLayout.OnRefreshListener {
// region Properties
private val viewModel: LaunchesViewModel
get() = viewModelFactory.createFor(this)
private val launchesAdapter = LaunchesAdapter(::onItemSelected)
// endregion
// region Functions
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//setHasOptionsMenu(true)
binding.rocketList.adapter = launchesAdapter
binding.swipeRefreshLayout.setOnRefreshListener(this)
observeDataChanges()
}
//override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// inflater.inflate(R.menu.menu_rocket_list, menu)
//
// with(menu.findItem(R.id.search)) {
// onExpandOrCollapse(::onSearchExpand, ::onSearchCollapse)
// setSearchQueryListener(actionView as SearchView)
// }
//
// super.onCreateOptionsMenu(menu, inflater)
//}
private fun observeDataChanges() {
// Observing state to show loading
observe(viewModel.stateData) {
binding.swipeRefreshLayout.isRefreshing = it is State.Loading
}
// Observing error to show toast with retry action
observe(viewModel.errorData) {
showSnackbarWithAction(it) {
viewModel.retry()
}
}
observe(viewModel.pagedList) {
launchesAdapter.submitList(it)
}
//observe(viewModel.filteredItems) {
// launchesAdapter.submitList(it)
//}
}
private fun onItemSelected(item: LaunchEntity?) {
startActivity(Actions.openDetailFor(item?.id ?: -1L))
}
//private fun onSearchExpand() {
// binding.swipeRefreshLayout.isEnabled = false
//}
//private fun onSearchCollapse() {
// binding.swipeRefreshLayout.isEnabled = true
//}
//private fun setSearchQueryListener(searchView: SearchView) {
// searchView.setOnQueryChangedListener {
// viewModel.filterItemListBy(it)
// }
//}
override fun onRefresh() {
viewModel.refresh()
}
override fun getLayoutId(): Int = R.layout.fragment_launches
// endregion
}

View File

@@ -0,0 +1,41 @@
package com.melih.list.ui.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import com.melih.core.base.recycler.BasePagingListAdapter
import com.melih.core.base.recycler.BaseViewHolder
import com.melih.list.databinding.LaunchRowBinding
import com.melih.repository.entities.LaunchEntity
class LaunchesAdapter(itemClickListener: (LaunchEntity?) -> Unit) : BasePagingListAdapter<LaunchEntity>(
object : DiffUtil.ItemCallback<LaunchEntity>() {
override fun areItemsTheSame(oldItem: LaunchEntity, newItem: LaunchEntity): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: LaunchEntity, newItem: LaunchEntity): Boolean =
oldItem.name == newItem.name
},
itemClickListener
) {
override fun createViewHolder(
inflater: LayoutInflater,
parent: ViewGroup,
viewType: Int
): BaseViewHolder<LaunchEntity> =
LaunchesViewHolder(LaunchRowBinding.inflate(inflater, parent, false))
}
class LaunchesViewHolder(private val binding: LaunchRowBinding) : BaseViewHolder<LaunchEntity>(binding) {
override fun bind(item: LaunchEntity?) {
binding.entity = item
val missions = item?.missions
binding.tvDescription.text = if (!missions.isNullOrEmpty()) missions[0].description else ""
binding.executePendingBindings()
}
}

View File

@@ -0,0 +1,26 @@
package com.melih.list.ui.paging
import com.melih.core.base.paging.BasePagingDataSource
import com.melih.repository.entities.LaunchEntity
import com.melih.repository.interactors.GetLaunches
import com.melih.repository.interactors.base.Result
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class LaunchesPagingSource @Inject constructor(
private val getLaunches: GetLaunches,
private val getLaunchesParams: GetLaunches.Params
) : BasePagingDataSource<LaunchEntity>() {
//region Functions
@UseExperimental(ExperimentalCoroutinesApi::class)
override fun loadDataForPage(page: Int): Flow<Result<List<LaunchEntity>>> =
getLaunches(
getLaunchesParams.copy(
page = page
)
)
//endregion
}

View File

@@ -0,0 +1,14 @@
package com.melih.list.ui.paging
import com.melih.core.base.paging.BasePagingDataSource
import com.melih.core.base.paging.BasePagingFactory
import com.melih.repository.entities.LaunchEntity
import javax.inject.Inject
import javax.inject.Provider
class LaunchesPagingSourceFactory @Inject constructor(
private val sourceProvider: Provider<LaunchesPagingSource>
) : BasePagingFactory<LaunchEntity>() {
override fun createSource(): BasePagingDataSource<LaunchEntity> = sourceProvider.get()
}

View File

@@ -0,0 +1,45 @@
package com.melih.list.ui.vm
import androidx.paging.PagedList
import com.melih.core.base.paging.BasePagingFactory
import com.melih.core.base.viewmodel.BasePagingViewModel
import com.melih.list.ui.paging.LaunchesPagingSourceFactory
import com.melih.repository.entities.LaunchEntity
import javax.inject.Inject
class LaunchesViewModel @Inject constructor(
private val launchesPagingSourceFactory: LaunchesPagingSourceFactory,
private val launchesPagingConfig: PagedList.Config
) : BasePagingViewModel<LaunchEntity>() {
// region Properties
override val factory: BasePagingFactory<LaunchEntity>
get() = launchesPagingSourceFactory
override val config: PagedList.Config
get() = launchesPagingConfig
//private val _filteredItems = MediatorLiveData<PagedList<LaunchEntity>>()
//val filteredItems: LiveData<PagedList<LaunchEntity>>
// get() = _filteredItems
// endregion
//init {
// _filteredItems.addSource(pagedList, _filteredItems::setValue)
//}
// region Functions
//fun filterItemListBy(query: String?) {
//
// _filteredItems.value = if (!query.isNullOrBlank()) {
// pagedList.value
// ?.snapshot() as PagedList<LaunchEntity>
// } else {
// pagedList.value
// }
//}
// endregion
}

View File

@@ -0,0 +1,22 @@
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@integer/anim_duration">
<translate
android:fromYDelta="-20%"
android:interpolator="@android:anim/decelerate_interpolator"
android:toYDelta="0" />
<alpha
android:fromAlpha="0"
android:interpolator="@android:anim/decelerate_interpolator"
android:toAlpha="1" />
<scale
android:fromXScale="105%"
android:fromYScale="105%"
android:interpolator="@android:anim/decelerate_interpolator"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="100%"
android:toYScale="100%" />
</set>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
android:animation="@anim/item_enter"
android:animationOrder="normal"
android:delay="15%" />

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data class="LaunchesActivityBinding" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?android:actionBarSize"
android:background="@color/colorPrimary" />
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</layout>

View File

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

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data class="LaunchRowBinding">
<variable
name="entity"
type="com.melih.repository.entities.LaunchEntity" />
</data>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/padding_standard"
android:layout_marginLeft="@dimen/padding_standard"
android:layout_marginTop="@dimen/padding_standard"
android:layout_marginEnd="@dimen/padding_standard"
android:layout_marginRight="@dimen/padding_standard"
app:cardCornerRadius="@dimen/corner_radius_standard"
app:cardElevation="10dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="160dp">
<ImageView
android:id="@+id/imgRocket"
imageUrl="@{entity.rocket.imageURL}"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginStart="@dimen/padding_standard"
android:layout_marginLeft="@dimen/padding_standard"
android:contentDescription="@string/cd_rocket_image"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars[14]" />
<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
android:id="@+id/tvTitle"
style="@style/TitleTextStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/padding_standard"
android:layout_marginLeft="@dimen/padding_standard"
android:layout_marginEnd="@dimen/padding_standard"
android:layout_marginRight="@dimen/padding_standard"
android:text="@{entity.name}"
android:textAppearance="@style/TitleTextAppearance"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imgRocket"
app:layout_constraintTop_toTopOf="@+id/imgRocket"
tools:text="@sample/launches.json/launches/name" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</layout>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/search"
android:icon="@android:drawable/ic_menu_search"
android:title="@string/search"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="always|collapseActionView" />
</menu>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_launches"
app:startDestination="@id/launchesFragment">
<fragment
android:id="@+id/launchesFragment"
android:name="com.melih.list.ui.LaunchesFragment"
android:label="LaunchesFragment"
tools:layout="@layout/fragment_launches" />
</navigation>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="anim_duration">350</integer>
</resources>

View File

@@ -0,0 +1,4 @@
<resources>
<string name="cd_rocket_image">Image of the rocket</string>
<string name="search">Search</string>
</resources>

View File

@@ -0,0 +1,27 @@
@file:UseExperimental(ExperimentalCoroutinesApi::class)
package com.melih.list
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
abstract class BaseTestWithMainThread {
protected val dispatcher = TestCoroutineDispatcher()
@BeforeEach
fun setUp() {
Dispatchers.setMain(dispatcher)
}
@AfterEach
fun tearDown() {
Dispatchers.resetMain()
dispatcher.cleanupTestCoroutines()
}
}