WIP: feature/abstractions (#45)

* Abstraction layer backup

* Removed DataEntity, was unnecessary for now

* Separated network, persistence, entities and interaction, closes #29

* Renamed binding

* Removed build files, example tests

Removed build files, example tests

* Fixed build files were not being ignored all around app

* Updated CI ymls

* Small changes

* Fixed legacy repository package names

* Fixed CQ findings

* Updated Fastlane

* Packaging changes and version upgrades

* Removed core from interactors

* Version bumps

* Added new module graph
This commit is contained in:
Melih Aksoy
2019-10-30 17:27:53 +01:00
committed by GitHub
parent 83e39400a9
commit 88022629e1
103 changed files with 1098 additions and 921 deletions

View File

@@ -14,7 +14,5 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':repository')
testImplementation testLibraries.coroutinesTest
}

View File

@@ -0,0 +1,10 @@
package com.melih.launches.data
import com.melih.abstractions.data.ViewEntity
data class LaunchDetailItem(
val id: Long,
val imageUrl: String,
val rocketName: String,
val missionDescription: String
) : ViewEntity

View File

@@ -0,0 +1,18 @@
package com.melih.launches.data
import com.melih.abstractions.mapper.Mapper
import com.melih.definitions.entities.LaunchEntity
import javax.inject.Inject
class LaunchDetailMapper @Inject constructor() : Mapper<LaunchEntity, LaunchDetailItem>() {
override fun convert(launchEntity: LaunchEntity) =
with(launchEntity) {
LaunchDetailItem(
id,
rocket.imageURL,
rocket.name,
if (!missions.isNullOrEmpty()) missions[0].description else ""
)
}
}

View File

@@ -2,11 +2,15 @@ package com.melih.detail.di.modules
import androidx.lifecycle.ViewModel
import androidx.navigation.fragment.navArgs
import com.melih.abstractions.mapper.Mapper
import com.melih.core.di.keys.ViewModelKey
import com.melih.definitions.entities.LaunchEntity
import com.melih.detail.ui.DetailFragment
import com.melih.detail.ui.DetailFragmentArgs
import com.melih.detail.ui.DetailViewModel
import com.melih.repository.interactors.GetLaunchDetails
import com.melih.interactors.GetLaunchDetails
import com.melih.launches.data.LaunchDetailItem
import com.melih.launches.data.LaunchDetailMapper
import dagger.Binds
import dagger.Module
import dagger.Provides
@@ -21,6 +25,9 @@ abstract class DetailFragmentModule {
@IntoMap
@ViewModelKey(DetailViewModel::class)
abstract fun detailViewModel(detailViewModel: DetailViewModel): ViewModel
@Binds
abstract fun detailMapper(mapper: LaunchDetailMapper): Mapper<LaunchEntity, LaunchDetailItem>
//endregion
@Module

View File

@@ -1,34 +1,31 @@
package com.melih.detail.ui
import androidx.lifecycle.Transformations.map
import com.melih.abstractions.deliverable.handle
import com.melih.core.base.viewmodel.BaseViewModel
import com.melih.repository.entities.LaunchEntity
import com.melih.repository.interactors.GetLaunchDetails
import com.melih.repository.interactors.base.handle
import com.melih.interactors.GetLaunchDetails
import com.melih.launches.data.LaunchDetailItem
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import javax.inject.Inject
class DetailViewModel @Inject constructor(
private val getLaunchDetails: GetLaunchDetails,
private val getLaunchDetails: GetLaunchDetails<LaunchDetailItem>,
private val getLaunchDetailsParams: GetLaunchDetails.Params
) : BaseViewModel<LaunchEntity>() {
) : BaseViewModel<LaunchDetailItem>() {
//region Properties
val rocketName = map(successData) {
it.rocket.name
it.rocketName
}
val description = map(successData) {
if (it.missions.isEmpty()) {
""
} else {
it.missions[0].description
}
it.missionDescription
}
val imageUrl = map(successData) {
it.rocket.imageURL
it.imageUrl
}
//endregion

View File

@@ -1,69 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data class="DetailBinding">
<data class="DetailBinding">
<variable
name="viewModel"
type="com.melih.detail.ui.DetailViewModel" />
</data>
<variable
name="viewModel"
type="com.melih.detail.ui.DetailViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imgRocket"
imageUrl="@{viewModel.imageUrl}"
android:layout_width="0dp"
android:layout_height="220dp"
android:layout_marginStart="@dimen/padding_standard"
android:layout_marginLeft="@dimen/padding_standard"
android:layout_marginTop="@dimen/padding_standard"
android:layout_marginEnd="@dimen/padding_standard"
android:layout_marginRight="@dimen/padding_standard"
android:contentDescription="@string/cd_rocket_image"
android:scaleType="centerCrop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars[14]" />
<ImageView
android:id="@+id/imgRocket"
imageUrl="@{viewModel.imageUrl}"
android:layout_width="0dp"
android:layout_height="220dp"
android:layout_marginStart="@dimen/padding_standard"
android:layout_marginLeft="@dimen/padding_standard"
android:layout_marginTop="@dimen/padding_standard"
android:layout_marginEnd="@dimen/padding_standard"
android:layout_marginRight="@dimen/padding_standard"
android:contentDescription="@string/cd_rocket_image"
android:scaleType="centerCrop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars[14]" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tvTitle"
style="@style/AppTheme.TextViewStyle.Title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/padding_standard"
android:layout_marginLeft="@dimen/padding_standard"
android:layout_marginTop="@dimen/padding_standard"
android:layout_marginEnd="@dimen/padding_standard"
android:layout_marginRight="@dimen/padding_standard"
android:text="@{viewModel.rocketName}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imgRocket"
tools:text="@sample/launches.json/launches/name" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tvTitle"
style="@style/AppTheme.TextViewStyle.Title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/padding_standard"
android:layout_marginLeft="@dimen/padding_standard"
android:layout_marginTop="@dimen/padding_standard"
android:layout_marginEnd="@dimen/padding_standard"
android:layout_marginRight="@dimen/padding_standard"
android:text="@{viewModel.rocketName}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imgRocket"
tools:text="@sample/launches.json/launches/name" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tvDescription"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="@dimen/padding_standard"
android:layout_marginLeft="@dimen/padding_standard"
android:layout_marginTop="@dimen/padding_standard"
android:layout_marginEnd="@dimen/padding_standard"
android:layout_marginRight="@dimen/padding_standard"
android:layout_marginBottom="@dimen/padding_standard"
android:text="@{viewModel.description}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
tools:text="@sample/launches.json/launches/missions/description" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tvDescription"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="@dimen/padding_standard"
android:layout_marginLeft="@dimen/padding_standard"
android:layout_marginTop="@dimen/padding_standard"
android:layout_marginEnd="@dimen/padding_standard"
android:layout_marginRight="@dimen/padding_standard"
android:layout_marginBottom="@dimen/padding_standard"
android:text="@{viewModel.description}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
tools:text="@sample/launches.json/launches/missions/description" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -1,7 +1,8 @@
package com.melih.detail
import com.melih.detail.ui.DetailViewModel
import com.melih.repository.interactors.GetLaunchDetails
import com.melih.interactors.GetLaunchDetails
import com.melih.launches.data.LaunchDetailItem
import io.mockk.mockk
import io.mockk.slot
import io.mockk.spyk
@@ -19,7 +20,7 @@ import org.junit.jupiter.api.Test
@UseExperimental(ExperimentalCoroutinesApi::class)
class DetailViewModelTest : BaseTestWithMainThread() {
private val getLaunchDetails: GetLaunchDetails = mockk(relaxed = true)
private val getLaunchDetails: GetLaunchDetails<LaunchDetailItem> = mockk(relaxed = true)
private val getLaunchDetailsParams = GetLaunchDetails.Params(1013)
private val viewModel = spyk(DetailViewModel(getLaunchDetails, getLaunchDetailsParams))

View File

@@ -7,8 +7,6 @@ apply from: "$rootProject.projectDir/scripts/feature_module.gradle"
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':repository')
implementation libraries.paging
implementation libraries.swipeRefreshLayout

View File

@@ -0,0 +1,10 @@
package com.melih.launches.data
import com.melih.abstractions.data.ViewEntity
data class LaunchItem(
val id: Long,
val imageUrl: String,
val rocketName: String,
val missionDescription: String
) : ViewEntity

View File

@@ -0,0 +1,18 @@
package com.melih.launches.data
import com.melih.abstractions.mapper.Mapper
import com.melih.definitions.entities.LaunchEntity
import javax.inject.Inject
class LaunchMapper @Inject constructor() : Mapper<LaunchEntity, LaunchItem>() {
override fun convert(launchEntity: LaunchEntity) =
with(launchEntity) {
LaunchItem(
id,
rocket.imageURL,
rocket.name,
if (!missions.isNullOrEmpty()) missions[0].description else ""
)
}
}

View File

@@ -2,10 +2,14 @@ package com.melih.launches.di.modules
import androidx.lifecycle.ViewModel
import androidx.paging.Config
import com.melih.abstractions.mapper.Mapper
import com.melih.core.di.keys.ViewModelKey
import com.melih.definitions.entities.LaunchEntity
import com.melih.interactors.DEFAULT_LAUNCHES_AMOUNT
import com.melih.interactors.GetLaunches
import com.melih.launches.data.LaunchItem
import com.melih.launches.data.LaunchMapper
import com.melih.launches.ui.vm.LaunchesViewModel
import com.melih.repository.interactors.DEFAULT_LAUNCHES_AMOUNT
import com.melih.repository.interactors.GetLaunches
import dagger.Binds
import dagger.Module
import dagger.Provides
@@ -14,12 +18,15 @@ import dagger.multibindings.IntoMap
@Module
abstract class LaunchesFragmentModule {
//region ViewModels
//region Binds
@Binds
@IntoMap
@ViewModelKey(LaunchesViewModel::class)
abstract fun listViewModel(listViewModel: LaunchesViewModel): ViewModel
abstract fun launchesViewModel(listViewModel: LaunchesViewModel): ViewModel
@Binds
abstract fun launchMapper(mapper: LaunchMapper): Mapper<LaunchEntity, LaunchItem>
//endregion
@Module

View File

@@ -4,18 +4,18 @@ import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.melih.abstractions.deliverable.State
import com.melih.core.actions.openDetail
import com.melih.core.base.lifecycle.BaseDaggerFragment
import com.melih.core.extensions.observe
import com.melih.interactors.error.PersistenceEmptyError
import com.melih.launches.R
import com.melih.launches.databinding.ListBinding
import com.melih.launches.data.LaunchItem
import com.melih.launches.databinding.LaunchesBinding
import com.melih.launches.ui.adapters.LaunchesAdapter
import com.melih.launches.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 {
class LaunchesFragment : BaseDaggerFragment<LaunchesBinding>(), SwipeRefreshLayout.OnRefreshListener {
//region Properties
@@ -67,7 +67,7 @@ class LaunchesFragment : BaseDaggerFragment<ListBinding>(), SwipeRefreshLayout.O
// Observing error to show toast with retry action
observe(viewModel.errorData) {
if (it !is PersistenceEmpty) {
if (it !is PersistenceEmptyError) {
showSnackbarWithAction(it) {
viewModel.retry()
}
@@ -79,7 +79,7 @@ class LaunchesFragment : BaseDaggerFragment<ListBinding>(), SwipeRefreshLayout.O
}
}
private fun onItemSelected(item: LaunchEntity) {
private fun onItemSelected(item: LaunchItem) {
openDetail(item.id)
}

View File

@@ -5,10 +5,10 @@ import android.view.ViewGroup
import com.melih.core.base.recycler.BasePagingListAdapter
import com.melih.core.base.recycler.BaseViewHolder
import com.melih.core.extensions.createDiffCallback
import com.melih.launches.data.LaunchItem
import com.melih.launches.databinding.LaunchRowBinding
import com.melih.repository.entities.LaunchEntity
class LaunchesAdapter(itemClickListener: (LaunchEntity) -> Unit) : BasePagingListAdapter<LaunchEntity>(
class LaunchesAdapter(itemClickListener: (LaunchItem) -> Unit) : BasePagingListAdapter<LaunchItem>(
createDiffCallback { oldItem, newItem -> oldItem.id == newItem.id },
itemClickListener
) {
@@ -19,21 +19,18 @@ class LaunchesAdapter(itemClickListener: (LaunchEntity) -> Unit) : BasePagingLis
inflater: LayoutInflater,
parent: ViewGroup,
viewType: Int
): BaseViewHolder<LaunchEntity> =
): BaseViewHolder<LaunchItem> =
LaunchesViewHolder(LaunchRowBinding.inflate(inflater, parent, false))
//endregion
}
class LaunchesViewHolder(private val binding: LaunchRowBinding) : BaseViewHolder<LaunchEntity>(binding) {
class LaunchesViewHolder(private val binding: LaunchRowBinding) :
BaseViewHolder<LaunchItem>(binding) {
//region Functions
override fun bind(item: LaunchEntity) {
override fun bind(item: LaunchItem) {
binding.entity = item
val missions = item.missions
binding.tvDescription.text = if (!missions.isNullOrEmpty()) missions[0].description else ""
binding.executePendingBindings()
}
//endregion

View File

@@ -1,25 +1,23 @@
package com.melih.launches.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 com.melih.interactors.GetLaunches
import com.melih.launches.data.LaunchItem
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 getLaunches: GetLaunches<LaunchItem>,
private val getLaunchesParams: GetLaunches.Params
) : BasePagingDataSource<LaunchEntity>() {
) : BasePagingDataSource<LaunchItem>() {
//region Functions
@UseExperimental(ExperimentalCoroutinesApi::class)
override fun loadDataForPage(page: Int): Flow<Result<List<LaunchEntity>>> =
override fun loadDataForPage(page: Int) =
getLaunches(
getLaunchesParams.copy(
page = page

View File

@@ -2,16 +2,16 @@ package com.melih.launches.ui.paging
import com.melih.core.base.paging.BasePagingDataSource
import com.melih.core.base.paging.BasePagingFactory
import com.melih.repository.entities.LaunchEntity
import com.melih.launches.data.LaunchItem
import javax.inject.Inject
import javax.inject.Provider
class LaunchesPagingSourceFactory @Inject constructor(
private val sourceProvider: Provider<LaunchesPagingSource>
) : BasePagingFactory<LaunchEntity>() {
) : BasePagingFactory<LaunchItem>() {
//region Functions
override fun createSource(): BasePagingDataSource<LaunchEntity> = sourceProvider.get()
override fun createSource(): BasePagingDataSource<LaunchItem> = sourceProvider.get()
//endregion
}

View File

@@ -3,18 +3,18 @@ package com.melih.launches.ui.vm
import androidx.paging.PagedList
import com.melih.core.base.paging.BasePagingFactory
import com.melih.core.base.viewmodel.BasePagingViewModel
import com.melih.launches.data.LaunchItem
import com.melih.launches.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>() {
) : BasePagingViewModel<LaunchItem>() {
//region Properties
override val factory: BasePagingFactory<LaunchEntity>
override val factory: BasePagingFactory<LaunchItem>
get() = launchesPagingSourceFactory
override val config: PagedList.Config

View File

@@ -4,7 +4,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data class="ListBinding">
<data class="LaunchesBinding">
<variable
name="viewModel"

View File

@@ -1,69 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data class="LaunchRowBinding">
<data class="LaunchRowBinding">
<variable
name="entity"
type="com.melih.repository.entities.LaunchEntity" />
</data>
<variable
name="entity"
type="com.melih.launches.data.LaunchItem" />
</data>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/padding_standard"
android:layout_marginLeft="@dimen/padding_standard"
android:layout_marginTop="@dimen/padding_standard"
android:layout_marginEnd="@dimen/padding_standard"
android:layout_marginRight="@dimen/padding_standard"
android:background="@null"
app:cardElevation="10dp">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/padding_standard"
android:layout_marginLeft="@dimen/padding_standard"
android:layout_marginTop="@dimen/padding_standard"
android:layout_marginEnd="@dimen/padding_standard"
android:layout_marginRight="@dimen/padding_standard"
android:background="@null"
app:cardElevation="10dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="160dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="160dp">
<ImageView
android:id="@+id/imgRocket"
imageUrl="@{entity.rocket.imageURL}"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginStart="@dimen/padding_large"
android:contentDescription="@string/cd_rocket_image"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars[14]" />
<ImageView
android:id="@+id/imgRocket"
imageUrl="@{entity.imageUrl}"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginStart="@dimen/padding_large"
android:contentDescription="@string/cd_rocket_image"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars[14]" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tvTitle"
style="@style/AppTheme.TextViewStyle.Title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/padding_standard"
android:layout_marginEnd="@dimen/padding_large"
android:text="@{entity.name}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imgRocket"
app:layout_constraintTop_toTopOf="@+id/imgRocket"
tools:text="@sample/launches.json/launches/name" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tvTitle"
style="@style/AppTheme.TextViewStyle.Title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/padding_standard"
android:layout_marginEnd="@dimen/padding_large"
android:text="@{entity.rocketName}"
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>
</com.google.android.material.card.MaterialCardView>
<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"
android:text="@{entity.missionDescription}"
app:layout_constraintBottom_toBottomOf="@+id/imgRocket"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imgRocket"
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
tools:text="@sample/launches.json/launches/missions/description" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</layout>