RoomList: add avatar for rooms and date formatting
This commit is contained in:
@@ -69,6 +69,7 @@ dependencies {
|
||||
implementation project(":features:login")
|
||||
implementation project(":features:roomlist")
|
||||
|
||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.2.0"
|
||||
implementation 'io.github.raamcosta.compose-destinations:core:1.7.23-beta'
|
||||
ksp 'io.github.raamcosta.compose-destinations:ksp:1.7.23-beta'
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ dependencies {
|
||||
implementation(project(":libraries:designsystem"))
|
||||
implementation(libs.mavericks.compose)
|
||||
implementation(libs.timber)
|
||||
implementation(libs.datetime)
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package io.element.android.x.features.roomlist
|
||||
|
||||
import android.text.format.DateFormat
|
||||
import android.text.format.DateUtils
|
||||
import kotlinx.datetime.*
|
||||
import kotlinx.datetime.TimeZone
|
||||
import java.time.Period
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
class LastMessageFormatter(
|
||||
private val clock: Clock = Clock.System,
|
||||
private val locale: Locale = Locale.getDefault()
|
||||
) {
|
||||
|
||||
private val onlyTimeFormatter: DateTimeFormatter by lazy {
|
||||
val pattern = DateFormat.getBestDateTimePattern(locale, "HH:mm")
|
||||
DateTimeFormatter.ofPattern(pattern)
|
||||
}
|
||||
|
||||
private val dateWithMonthFormatter: DateTimeFormatter by lazy {
|
||||
val pattern = DateFormat.getBestDateTimePattern(locale, "d MMM")
|
||||
DateTimeFormatter.ofPattern(pattern)
|
||||
}
|
||||
|
||||
private val dateWithYearFormatter: DateTimeFormatter by lazy {
|
||||
val pattern = DateFormat.getBestDateTimePattern(locale, "dd.MM.yyyy")
|
||||
DateTimeFormatter.ofPattern(pattern)
|
||||
}
|
||||
|
||||
|
||||
fun format(timestamp: Long?): String {
|
||||
if (timestamp == null) return ""
|
||||
val now: Instant = clock.now()
|
||||
val tsInstant = Instant.fromEpochSeconds(timestamp)
|
||||
val nowDateTime = now.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
val tsDateTime = tsInstant.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
val isSameDay = nowDateTime.date == tsDateTime.date
|
||||
return when {
|
||||
isSameDay -> {
|
||||
onlyTimeFormatter.format(tsDateTime.toJavaLocalDateTime())
|
||||
}
|
||||
else -> {
|
||||
formatDate(tsDateTime, nowDateTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatDate(
|
||||
date: LocalDateTime,
|
||||
currentDate: LocalDateTime,
|
||||
): String {
|
||||
val period = Period.between(date.date.toJavaLocalDate(), currentDate.date.toJavaLocalDate())
|
||||
return if (period.years.absoluteValue >= 1) {
|
||||
formatDateWithYear(date)
|
||||
} else if (period.days.absoluteValue < 2 && period.months.absoluteValue < 1) {
|
||||
getRelativeDay(date.toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds())
|
||||
} else {
|
||||
formatDateWithMonth(date)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatDateWithMonth(localDateTime: LocalDateTime): String {
|
||||
return dateWithMonthFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||
}
|
||||
|
||||
private fun formatDateWithYear(localDateTime: LocalDateTime): String {
|
||||
return dateWithYearFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||
}
|
||||
|
||||
private fun getRelativeDay(ts: Long): String {
|
||||
return DateUtils.getRelativeTimeSpanString(
|
||||
ts,
|
||||
clock.now().toEpochMilliseconds(),
|
||||
DateUtils.DAY_IN_MILLIS,
|
||||
DateUtils.FORMAT_SHOW_WEEKDAY
|
||||
).toString()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package io.element.android.x.features.roomlist
|
||||
|
||||
sealed interface RoomListActions {
|
||||
object LoadMore : RoomListActions
|
||||
object Logout : RoomListActions
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package io.element.android.x.features.roomlist
|
||||
|
||||
import android.widget.Space
|
||||
import Avatar
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ExitToApp
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
@@ -15,9 +17,11 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.airbnb.mvrx.Success
|
||||
@@ -25,10 +29,10 @@ import com.airbnb.mvrx.compose.collectAsState
|
||||
import com.airbnb.mvrx.compose.mavericksViewModel
|
||||
import io.element.android.x.core.data.LogCompositions
|
||||
import io.element.android.x.designsystem.LightGrey
|
||||
import io.element.android.x.designsystem.components.Avatar
|
||||
import io.element.android.x.features.roomlist.model.MatrixUser
|
||||
import io.element.android.x.features.roomlist.model.RoomListRoomSummary
|
||||
import io.element.android.x.features.roomlist.model.RoomListViewState
|
||||
import io.element.android.x.matrix.core.RoomId
|
||||
import io.element.android.x.matrix.room.RoomSummary
|
||||
|
||||
@Composable
|
||||
fun RoomListScreen(
|
||||
@@ -54,7 +58,7 @@ fun RoomListScreen(
|
||||
|
||||
@Composable
|
||||
fun RoomListContent(
|
||||
roomSummaries: List<RoomSummary>,
|
||||
roomSummaries: List<RoomListRoomSummary>,
|
||||
matrixUser: MatrixUser,
|
||||
onRoomClicked: (RoomId) -> Unit,
|
||||
onLogoutClicked: () -> Unit,
|
||||
@@ -69,12 +73,11 @@ fun RoomListContent(
|
||||
onLogoutClicked = onLogoutClicked
|
||||
)
|
||||
LazyColumn {
|
||||
items(roomSummaries, key = { it.identifier() }) { room ->
|
||||
items(roomSummaries, key = { it.id }) { room ->
|
||||
RoomItem(room = room) {
|
||||
onRoomClicked(it)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,7 +93,7 @@ fun RoomListTopBar(matrixUser: MatrixUser, onLogoutClicked: () -> Unit) {
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Avatar(data = matrixUser.avatarData)
|
||||
Avatar(data = matrixUser.avatarData, size = 32.dp)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("${matrixUser.username}")
|
||||
}
|
||||
@@ -108,18 +111,17 @@ fun RoomListTopBar(matrixUser: MatrixUser, onLogoutClicked: () -> Unit) {
|
||||
@Composable
|
||||
private fun RoomItem(
|
||||
modifier: Modifier = Modifier,
|
||||
room: RoomSummary,
|
||||
room: RoomListRoomSummary,
|
||||
onClick: (RoomId) -> Unit
|
||||
) {
|
||||
if (room !is RoomSummary.Filled) {
|
||||
if (room.isPlaceholder) {
|
||||
return
|
||||
}
|
||||
val details = room.details
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
onClick = { onClick(room.details.roomId) },
|
||||
onClick = { onClick(room.roomId) },
|
||||
indication = rememberRipple(),
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
),
|
||||
@@ -128,12 +130,9 @@ private fun RoomItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
) {
|
||||
Avatar(data = null)
|
||||
}
|
||||
Avatar(data = room.avatarData)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
@@ -143,31 +142,56 @@ private fun RoomItem(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.Black,
|
||||
text = details.name,
|
||||
text = room.name,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = details.lastMessage?.toString().orEmpty(),
|
||||
text = room.lastMessage?.toString().orEmpty(),
|
||||
color = LightGrey,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
Column(
|
||||
Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
Modifier.padding(horizontal = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
fontSize = 12.sp,
|
||||
text = "14:18",
|
||||
color = LightGrey
|
||||
text = room.timestamp ?: "",
|
||||
color = LightGrey,
|
||||
)
|
||||
Spacer(modifier.size(4.dp))
|
||||
val unreadIndicatorColor = if(room.hasUnread) Color.Black else Color.Transparent
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.clip(CircleShape)
|
||||
.background(unreadIndicatorColor)
|
||||
.align(Alignment.End),
|
||||
)
|
||||
Spacer(Modifier.size(20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PreviewableRoomListContent() {
|
||||
val roomSummaries = listOf(
|
||||
RoomListRoomSummary(
|
||||
name = "Room",
|
||||
hasUnread = true,
|
||||
timestamp = "14:18",
|
||||
lastMessage = "A message",
|
||||
avatarData = null,
|
||||
id = "roomId"
|
||||
)
|
||||
)
|
||||
RoomListContent(
|
||||
roomSummaries = roomSummaries,
|
||||
matrixUser = MatrixUser("User#1"),
|
||||
onRoomClicked = {},
|
||||
onLogoutClicked = {}
|
||||
)
|
||||
}
|
||||
@@ -4,9 +4,16 @@ import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.MavericksViewModel
|
||||
import com.airbnb.mvrx.Success
|
||||
import io.element.android.x.core.data.parallelMap
|
||||
import io.element.android.x.features.roomlist.model.MatrixUser
|
||||
import io.element.android.x.features.roomlist.model.RoomListRoomSummary
|
||||
import io.element.android.x.features.roomlist.model.RoomListViewState
|
||||
import io.element.android.x.matrix.MatrixClient
|
||||
import io.element.android.x.matrix.MatrixInstance
|
||||
import io.element.android.x.matrix.room.RoomSummary
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.rustcomponents.sdk.mediaSourceFromUrl
|
||||
|
||||
@@ -14,6 +21,7 @@ class RoomListViewModel(initialState: RoomListViewState) :
|
||||
MavericksViewModel<RoomListViewState>(initialState) {
|
||||
|
||||
private val matrix = MatrixInstance.getInstance()
|
||||
private val lastMessageFormatter = LastMessageFormatter()
|
||||
|
||||
init {
|
||||
handleInit()
|
||||
@@ -21,12 +29,11 @@ class RoomListViewModel(initialState: RoomListViewState) :
|
||||
|
||||
fun handle(action: RoomListActions) {
|
||||
when (action) {
|
||||
RoomListActions.LoadMore -> TODO()
|
||||
RoomListActions.Logout -> handleLogout()
|
||||
}
|
||||
}
|
||||
|
||||
fun logout(){
|
||||
fun logout() {
|
||||
handleLogout()
|
||||
}
|
||||
|
||||
@@ -36,26 +43,63 @@ class RoomListViewModel(initialState: RoomListViewState) :
|
||||
client.startSync()
|
||||
val userAvatarUrl = client.loadUserAvatarURLString().getOrNull()
|
||||
val userDisplayName = client.loadUserDisplayName().getOrNull()
|
||||
val avatarData = userAvatarUrl?.let {
|
||||
mediaSourceFromUrl(it)
|
||||
}?.let {
|
||||
client.loadMediaContentForSource(it)
|
||||
}
|
||||
val avatarData = loadAvatarData(client, userAvatarUrl)
|
||||
setState {
|
||||
copy(
|
||||
user = MatrixUser(
|
||||
username = userDisplayName,
|
||||
avatarUrl = userAvatarUrl,
|
||||
avatarData = avatarData?.getOrNull()
|
||||
avatarData = avatarData,
|
||||
)
|
||||
)
|
||||
}
|
||||
client.roomSummaryDataSource().roomSummaries().execute {
|
||||
copy(rooms = it)
|
||||
client.roomSummaryDataSource().roomSummaries()
|
||||
.map { roomSummaries ->
|
||||
mapRoomSummaries(client, roomSummaries)
|
||||
}
|
||||
.flowOn(Dispatchers.Default)
|
||||
.execute {
|
||||
copy(rooms = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun mapRoomSummaries(
|
||||
client: MatrixClient,
|
||||
roomSummaries: List<RoomSummary>
|
||||
): List<RoomListRoomSummary> {
|
||||
return roomSummaries.parallelMap { roomSummary ->
|
||||
when (roomSummary) {
|
||||
is RoomSummary.Empty -> RoomListRoomSummary(
|
||||
id = roomSummary.identifier,
|
||||
isPlaceholder = true
|
||||
)
|
||||
is RoomSummary.Filled -> {
|
||||
val avatarData = loadAvatarData(client, roomSummary.details.avatarURLString)
|
||||
RoomListRoomSummary(
|
||||
id = roomSummary.identifier(),
|
||||
name = roomSummary.details.name,
|
||||
hasUnread = roomSummary.details.unreadNotificationCount > 0,
|
||||
timestamp = lastMessageFormatter.format(roomSummary.details.lastMessageTimestamp),
|
||||
lastMessage = roomSummary.details.lastMessage,
|
||||
avatarData = avatarData,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadAvatarData(client: MatrixClient, url: String?, size: Long = 48): ByteArray? {
|
||||
val mediaContent = url?.let {
|
||||
val mediaSource = mediaSourceFromUrl(it)
|
||||
client.loadMediaThumbnailForSource(mediaSource, size, size)
|
||||
}
|
||||
return mediaContent?.fold(
|
||||
{ it },
|
||||
{ null }
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleLogout() {
|
||||
viewModelScope.launch {
|
||||
setState { copy(logoutAction = Loading()) }
|
||||
|
||||
@@ -6,5 +6,5 @@ import androidx.compose.runtime.Stable
|
||||
data class MatrixUser(
|
||||
val username: String? = null,
|
||||
val avatarUrl: String? = null,
|
||||
val avatarData: List<UByte>? = null,
|
||||
val avatarData: ByteArray? = null,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package io.element.android.x.features.roomlist.model
|
||||
|
||||
import io.element.android.x.matrix.core.RoomId
|
||||
|
||||
data class RoomListRoomSummary(
|
||||
val id: String,
|
||||
val roomId: RoomId = RoomId(id),
|
||||
val name: String = "",
|
||||
val hasUnread: Boolean = false,
|
||||
val timestamp: String? = null,
|
||||
val lastMessage: CharSequence? = null,
|
||||
val avatarData: ByteArray? = null,
|
||||
val isPlaceholder: Boolean = false,
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
package io.element.android.x.features.roomlist
|
||||
package io.element.android.x.features.roomlist.model
|
||||
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.MavericksState
|
||||
@@ -8,7 +8,7 @@ import io.element.android.x.matrix.room.RoomSummary
|
||||
|
||||
data class RoomListViewState(
|
||||
val user: MatrixUser = MatrixUser(),
|
||||
val rooms: Async<List<RoomSummary>> = Uninitialized,
|
||||
val rooms: Async<List<RoomListRoomSummary>> = Uninitialized,
|
||||
val canLoadMore: Boolean = false,
|
||||
val logoutAction: Async<Unit> = Uninitialized,
|
||||
) : MavericksState
|
||||
@@ -33,6 +33,7 @@ test_orchestrator = "1.4.1"
|
||||
mavericks = "3.0.1"
|
||||
timber = "5.0.1"
|
||||
coil = "2.2.1"
|
||||
datetime = "0.4.0"
|
||||
|
||||
[libraries]
|
||||
# Project
|
||||
@@ -70,6 +71,8 @@ test_orchestrator = { module = "androidx.test:orchestrator", version.ref = "test
|
||||
|
||||
mavericks_compose = { module = "com.airbnb.android:mavericks-compose", version.ref = "mavericks" }
|
||||
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
|
||||
coil_compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
|
||||
coil_compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
|
||||
datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" }
|
||||
|
||||
[bundles]
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package io.element.android.x.core.data
|
||||
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
|
||||
suspend fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> = coroutineScope {
|
||||
map { async { f(it) } }.awaitAll()
|
||||
}
|
||||
@@ -1,37 +1,31 @@
|
||||
package io.element.android.x.designsystem.components
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import coil.compose.AsyncImage
|
||||
|
||||
/**
|
||||
* TODO fallback Avatar
|
||||
*/
|
||||
@Composable
|
||||
fun Avatar(
|
||||
data: List<UByte>?,
|
||||
data: ByteArray?,
|
||||
size: Dp = 48.dp,
|
||||
) {
|
||||
Image(
|
||||
painter = rememberAsyncImagePainter(
|
||||
model = data?.toUByteArray()?.toByteArray(),
|
||||
onError = {
|
||||
Log.e("TAG", "Error $it\n${it.result}", it.result.throwable)
|
||||
}),
|
||||
AsyncImage(
|
||||
model = data,
|
||||
onError = {
|
||||
Log.e("TAG", "Error $it\n${it.result}", it.result.throwable)
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(size)
|
||||
.clip(CircleShape)
|
||||
.border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -96,10 +96,21 @@ class MatrixClient internal constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadMediaContentForSource(source: MediaSource): Result<List<UByte>> =
|
||||
suspend fun loadMediaContentForSource(source: MediaSource): Result<ByteArray> =
|
||||
withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
client.getMediaContent(source)
|
||||
client.getMediaContent(source).toUByteArray().toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadMediaThumbnailForSource(
|
||||
source: MediaSource,
|
||||
width: Long,
|
||||
height: Long
|
||||
): Result<ByteArray> =
|
||||
withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
client.getMediaThumbnail(source, width.toULong(), height.toULong()).toUByteArray().toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,5 +23,5 @@ data class RoomSummaryDetails(
|
||||
val avatarURLString: String?,
|
||||
val lastMessage: CharSequence?,
|
||||
val lastMessageTimestamp: Long?,
|
||||
val unreadNotificationCount: UInt,
|
||||
val unreadNotificationCount: Int,
|
||||
)
|
||||
|
||||
@@ -126,7 +126,7 @@ internal class RustRoomSummaryDataSource(
|
||||
name = room.name() ?: identifier,
|
||||
isDirect = room.isDm() ?: false,
|
||||
avatarURLString = room.fullRoom()?.avatarUrl(),
|
||||
unreadNotificationCount = room.unreadNotifications().notificationCount(),
|
||||
unreadNotificationCount = room.unreadNotifications().notificationCount().toInt(),
|
||||
lastMessage = latestRoomMessage?.body,
|
||||
lastMessageTimestamp = latestRoomMessage?.originServerTs
|
||||
)
|
||||
|
||||
@@ -7,15 +7,16 @@ import composeVersion
|
||||
import org.gradle.api.artifacts.VersionCatalog
|
||||
|
||||
fun CommonExtension<*, *, *, *>.androidConfig() {
|
||||
|
||||
|
||||
|
||||
defaultConfig {
|
||||
compileSdk = Versions.compileSdk
|
||||
minSdk = Versions.minSdk
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.isReturnDefaultValues = true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user