Magrate schedule domain to flow
This commit is contained in:
@@ -6,10 +6,13 @@ import androidx.activity.ComponentActivity
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import ru.fincode.tsudesk.core.common.model.DataResult
|
||||||
import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleType
|
import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleType
|
||||||
import ru.fincode.tsudesk.feature.schedule.domain.usecase.GetScheduleUseCase
|
import ru.fincode.tsudesk.feature.schedule.domain.usecase.GetScheduleUseCase
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private const val LOG_TAG = "NETWORK_DEBUG"
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@Inject
|
@Inject
|
||||||
@@ -19,13 +22,22 @@ class MainActivity : ComponentActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
getScheduleUseCase(
|
||||||
val result = getScheduleUseCase(
|
ScheduleType.Group("220631") // пример группы
|
||||||
ScheduleType.Group("220631")
|
).collect { result ->
|
||||||
)
|
when (result) {
|
||||||
Log.d("TSUDesk", result.toString())
|
is DataResult.Cache -> {
|
||||||
} catch (e: Exception) {
|
Log.d(LOG_TAG, "CACHE: ${result.data}")
|
||||||
Log.e("TSUDesk", "Error loading schedule", e)
|
}
|
||||||
|
|
||||||
|
is DataResult.Network -> {
|
||||||
|
Log.d(LOG_TAG, "NETWORK: ${result.data}")
|
||||||
|
}
|
||||||
|
|
||||||
|
is DataResult.Error -> {
|
||||||
|
Log.e(LOG_TAG, "ERROR: ${result.throwable}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import dagger.Provides
|
|||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import ru.fincode.tsudesk.BuildConfig
|
import ru.fincode.tsudesk.BuildConfig
|
||||||
import ru.fincode.tsudesk.core.common.config.AppConfig
|
import ru.fincode.tsudesk.core.common.app.AppConfig
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package ru.fincode.tsudesk.core.common.config
|
package ru.fincode.tsudesk.core.common.app
|
||||||
|
|
||||||
data class AppConfig(
|
data class AppConfig(
|
||||||
val isDebug: Boolean,
|
val isDebug: Boolean,
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package ru.fincode.tsudesk.core.common.model
|
||||||
|
|
||||||
|
sealed interface DataResult<out T> {
|
||||||
|
|
||||||
|
data class Cache<T>(
|
||||||
|
val data: T
|
||||||
|
) : DataResult<T>
|
||||||
|
|
||||||
|
data class Network<T>(
|
||||||
|
val data: T
|
||||||
|
) : DataResult<T>
|
||||||
|
|
||||||
|
data class Error(
|
||||||
|
val throwable: Throwable
|
||||||
|
) : DataResult<Nothing>
|
||||||
|
}
|
||||||
@@ -1,49 +1,55 @@
|
|||||||
package ru.fincode.tsudesk.core.database.schedule
|
package ru.fincode.tsudesk.core.database.schedule
|
||||||
|
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Delete
|
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Transaction
|
import androidx.room.Transaction
|
||||||
import ru.fincode.tsudesk.core.database.schedule.Query.DELETE_LESSON_BY_GROUP_KEY_QUERY
|
import kotlinx.coroutines.flow.Flow
|
||||||
import ru.fincode.tsudesk.core.database.schedule.Query.SELECT_LESSON_BY_KEY_QUERY
|
import ru.fincode.tsudesk.core.database.schedule.Query.SELECT_LESSON_BY_KEY_QUERY
|
||||||
import ru.fincode.tsudesk.core.database.schedule.Query.SELECT_SCHEDULE_BY_KEY_QUERY
|
import ru.fincode.tsudesk.core.database.schedule.Query.SELECT_SCHEDULE_BY_KEY_QUERY
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface ScheduleDao {
|
interface ScheduleDao {
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.Companion.REPLACE)
|
|
||||||
suspend fun upsertSchedule(schedule: ScheduleCacheEntity)
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.Companion.REPLACE)
|
|
||||||
suspend fun insertLessons(lessons: List<LessonCacheEntity>)
|
|
||||||
|
|
||||||
@Query(SELECT_SCHEDULE_BY_KEY_QUERY)
|
@Query(SELECT_SCHEDULE_BY_KEY_QUERY)
|
||||||
suspend fun getSchedule(key: String): ScheduleCacheEntity?
|
fun observeSchedule(key: String): Flow<ScheduleCacheEntity?>
|
||||||
|
|
||||||
@Query(SELECT_LESSON_BY_KEY_QUERY)
|
@Query(SELECT_LESSON_BY_KEY_QUERY)
|
||||||
|
fun observeLessons(key: String): Flow<List<LessonCacheEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM schedule_cache WHERE `key` = :key LIMIT 1")
|
||||||
|
suspend fun getSchedule(key: String): ScheduleCacheEntity?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM lesson_cache WHERE scheduleKey = :key")
|
||||||
suspend fun getLessons(key: String): List<LessonCacheEntity>
|
suspend fun getLessons(key: String): List<LessonCacheEntity>
|
||||||
|
|
||||||
@Query(DELETE_LESSON_BY_GROUP_KEY_QUERY)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun upsertSchedule(schedule: ScheduleCacheEntity)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertLessons(lessons: List<LessonCacheEntity>)
|
||||||
|
|
||||||
|
@Query("DELETE FROM lesson_cache WHERE scheduleKey = :key")
|
||||||
suspend fun deleteLessonsByKey(key: String)
|
suspend fun deleteLessonsByKey(key: String)
|
||||||
|
|
||||||
@Delete
|
@Query("DELETE FROM schedule_cache WHERE `key` = :key")
|
||||||
suspend fun deleteSchedule(entity: ScheduleCacheEntity)
|
suspend fun deleteScheduleByKey(key: String)
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
suspend fun replaceSchedule(
|
suspend fun replaceSchedule(
|
||||||
key: String, schedule: ScheduleCacheEntity, lessons: List<LessonCacheEntity>
|
key: String,
|
||||||
|
schedule: ScheduleCacheEntity,
|
||||||
|
lessons: List<LessonCacheEntity>
|
||||||
) {
|
) {
|
||||||
upsertSchedule(schedule)
|
|
||||||
deleteLessonsByKey(key)
|
deleteLessonsByKey(key)
|
||||||
|
upsertSchedule(schedule)
|
||||||
insertLessons(lessons)
|
insertLessons(lessons)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
suspend fun clearSchedule(key: String) {
|
suspend fun clearSchedule(key: String) {
|
||||||
deleteLessonsByKey(key)
|
deleteLessonsByKey(key)
|
||||||
val header = getSchedule(key) ?: return
|
deleteScheduleByKey(key)
|
||||||
deleteSchedule(header)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package ru.fincode.tsudesk.core.network
|
package ru.fincode.tsudesk.core.network
|
||||||
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import ru.fincode.tsudesk.core.common.config.AppConfig
|
import ru.fincode.tsudesk.core.common.app.AppConfig
|
||||||
import ru.fincode.tsudesk.core.network.interceptor.DebugInterceptor
|
import ru.fincode.tsudesk.core.network.interceptor.DebugInterceptor
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import dagger.hilt.InstallIn
|
|||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import ru.fincode.tsudesk.core.common.config.AppConfig
|
import ru.fincode.tsudesk.core.common.app.AppConfig
|
||||||
import ru.fincode.tsudesk.core.network.RetrofitFactory
|
import ru.fincode.tsudesk.core.network.RetrofitFactory
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
|||||||
@@ -43,4 +43,5 @@ dependencies {
|
|||||||
|
|
||||||
implementation(project(":core:network"))
|
implementation(project(":core:network"))
|
||||||
implementation(project(":core:database"))
|
implementation(project(":core:database"))
|
||||||
|
implementation(project(":core:common"))
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,13 @@
|
|||||||
package ru.fincode.tsudesk.feature.schedule.data
|
package ru.fincode.tsudesk.feature.schedule.data
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import ru.fincode.tsudesk.core.common.model.DataResult
|
||||||
|
import ru.fincode.tsudesk.core.common.model.DataResult.Error
|
||||||
import ru.fincode.tsudesk.core.network.model.NetworkResult
|
import ru.fincode.tsudesk.core.network.model.NetworkResult
|
||||||
import ru.fincode.tsudesk.core.network.model.map
|
import ru.fincode.tsudesk.core.network.model.map
|
||||||
import ru.fincode.tsudesk.feature.schedule.data.datasource.ScheduleLocalDataSource
|
import ru.fincode.tsudesk.feature.schedule.data.datasource.ScheduleLocalDataSource
|
||||||
@@ -7,6 +15,7 @@ import ru.fincode.tsudesk.feature.schedule.data.datasource.ScheduleRemoteDataSou
|
|||||||
import ru.fincode.tsudesk.feature.schedule.data.mapper.ScheduleCacheKey
|
import ru.fincode.tsudesk.feature.schedule.data.mapper.ScheduleCacheKey
|
||||||
import ru.fincode.tsudesk.feature.schedule.data.mapper.ScheduleNetworkMapper
|
import ru.fincode.tsudesk.feature.schedule.data.mapper.ScheduleNetworkMapper
|
||||||
import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleEntity
|
import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleEntity
|
||||||
|
import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleType
|
||||||
import ru.fincode.tsudesk.feature.schedule.domain.repository.ScheduleRepository
|
import ru.fincode.tsudesk.feature.schedule.domain.repository.ScheduleRepository
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -16,19 +25,50 @@ class ScheduleRepositoryImpl @Inject constructor(
|
|||||||
private val mapper: ScheduleNetworkMapper
|
private val mapper: ScheduleNetworkMapper
|
||||||
) : ScheduleRepository {
|
) : ScheduleRepository {
|
||||||
|
|
||||||
override suspend fun loadScheduleByGroup(number: String): NetworkResult<ScheduleEntity> {
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
val result = remote.loadScheduleByGroup(number).map(mapper::invoke)
|
override fun observeSchedule(type: ScheduleType): Flow<DataResult<ScheduleEntity>> =
|
||||||
if (result is NetworkResult.Success) {
|
channelFlow {
|
||||||
local.saveSchedule(ScheduleCacheKey.group(number), result.data)
|
val key = when (type) {
|
||||||
}
|
is ScheduleType.Group -> ScheduleCacheKey.group(type.number)
|
||||||
return result
|
is ScheduleType.Teacher -> ScheduleCacheKey.teacher(type.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun loadScheduleByTeacher(name: String): NetworkResult<ScheduleEntity> {
|
// кэш -> UI
|
||||||
val result = remote.loadScheduleByTeacher(name).map(mapper::invoke)
|
val cacheJob = launch {
|
||||||
if (result is NetworkResult.Success) {
|
local.observeSchedule(key).collect { cached ->
|
||||||
local.saveSchedule(ScheduleCacheKey.teacher(name), result.data)
|
if (cached != null) send(DataResult.Cache(cached))
|
||||||
}
|
}
|
||||||
return result
|
}
|
||||||
|
|
||||||
|
val networkResult: NetworkResult<ScheduleEntity> = when (type) {
|
||||||
|
is ScheduleType.Group ->
|
||||||
|
remote.loadScheduleByGroup(type.number).map(mapper::invoke)
|
||||||
|
|
||||||
|
is ScheduleType.Teacher ->
|
||||||
|
remote.loadScheduleByTeacher(type.name).map(mapper::invoke)
|
||||||
|
}
|
||||||
|
|
||||||
|
when (networkResult) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
|
// (опционально) сразу отдать сетевой результат
|
||||||
|
send(DataResult.Network(networkResult.data))
|
||||||
|
|
||||||
|
// single source of truth -> сохраняем в БД
|
||||||
|
local.saveSchedule(key, networkResult.data)
|
||||||
|
|
||||||
|
// дать Room время эмитнуть обновление
|
||||||
|
kotlinx.coroutines.yield()
|
||||||
|
|
||||||
|
// закрываем поток по твоему требованию
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
is NetworkResult.Error -> {
|
||||||
|
send(Error(Throwable(networkResult.error.toString())))
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
awaitClose { cacheJob.cancel() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
package ru.fincode.tsudesk.feature.schedule.data.datasource
|
package ru.fincode.tsudesk.feature.schedule.data.datasource
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleEntity
|
import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleEntity
|
||||||
|
|
||||||
interface ScheduleLocalDataSource {
|
interface ScheduleLocalDataSource {
|
||||||
suspend fun saveSchedule(key: String, schedule: ScheduleEntity)
|
suspend fun saveSchedule(key: String, schedule: ScheduleEntity)
|
||||||
suspend fun loadSchedule(key: String): ScheduleEntity?
|
suspend fun loadSchedule(key: String): ScheduleEntity?
|
||||||
|
fun observeSchedule(key: String): Flow<ScheduleEntity?>
|
||||||
suspend fun removeSchedule(key: String)
|
suspend fun removeSchedule(key: String)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package ru.fincode.tsudesk.feature.schedule.data.local
|
package ru.fincode.tsudesk.feature.schedule.data.local
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import ru.fincode.tsudesk.core.database.schedule.ScheduleDao
|
import ru.fincode.tsudesk.core.database.schedule.ScheduleDao
|
||||||
import ru.fincode.tsudesk.feature.schedule.data.datasource.ScheduleLocalDataSource
|
import ru.fincode.tsudesk.feature.schedule.data.datasource.ScheduleLocalDataSource
|
||||||
import ru.fincode.tsudesk.feature.schedule.data.mapper.toCache
|
import ru.fincode.tsudesk.feature.schedule.data.mapper.toCache
|
||||||
@@ -22,6 +24,13 @@ class ScheduleLocalDataSourceImpl @Inject constructor(
|
|||||||
return schedule.toDomain(lessons)
|
return schedule.toDomain(lessons)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun observeSchedule(key: String): Flow<ScheduleEntity?> {
|
||||||
|
return dao.observeSchedule(key)
|
||||||
|
.combine(dao.observeLessons(key)) { schedule, lessons ->
|
||||||
|
schedule?.toDomain(lessons)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun removeSchedule(key: String) {
|
override suspend fun removeSchedule(key: String) {
|
||||||
dao.deleteLessonsByKey(key)
|
dao.deleteLessonsByKey(key)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package ru.fincode.tsudesk.feature.schedule.domain.repository
|
package ru.fincode.tsudesk.feature.schedule.domain.repository
|
||||||
|
|
||||||
import ru.fincode.tsudesk.core.network.model.NetworkResult
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import ru.fincode.tsudesk.core.common.model.DataResult
|
||||||
import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleEntity
|
import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleEntity
|
||||||
|
import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleType
|
||||||
|
|
||||||
interface ScheduleRepository {
|
interface ScheduleRepository {
|
||||||
suspend fun loadScheduleByGroup(number: String): NetworkResult<ScheduleEntity>
|
fun observeSchedule(type: ScheduleType): Flow<DataResult<ScheduleEntity>>
|
||||||
suspend fun loadScheduleByTeacher(name: String): NetworkResult<ScheduleEntity>
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package ru.fincode.tsudesk.feature.schedule.domain.usecase
|
package ru.fincode.tsudesk.feature.schedule.domain.usecase
|
||||||
|
|
||||||
import ru.fincode.tsudesk.core.network.model.NetworkResult
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import ru.fincode.tsudesk.core.common.model.DataResult
|
||||||
import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleEntity
|
import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleEntity
|
||||||
import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleType
|
import ru.fincode.tsudesk.feature.schedule.domain.model.ScheduleType
|
||||||
import ru.fincode.tsudesk.feature.schedule.domain.repository.ScheduleRepository
|
import ru.fincode.tsudesk.feature.schedule.domain.repository.ScheduleRepository
|
||||||
@@ -9,8 +10,6 @@ import javax.inject.Inject
|
|||||||
class GetScheduleUseCase @Inject constructor(
|
class GetScheduleUseCase @Inject constructor(
|
||||||
private val repository: ScheduleRepository
|
private val repository: ScheduleRepository
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(type: ScheduleType): NetworkResult<ScheduleEntity> = when (type) {
|
operator fun invoke(type: ScheduleType): Flow<DataResult<ScheduleEntity>> =
|
||||||
is ScheduleType.Group -> repository.loadScheduleByGroup(type.number)
|
repository.observeSchedule(type)
|
||||||
is ScheduleType.Teacher -> repository.loadScheduleByTeacher(type.name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,17 +24,13 @@ lifecycle = "2.7.0"
|
|||||||
coroutines = "1.8.1"
|
coroutines = "1.8.1"
|
||||||
|
|
||||||
room = "2.6.1"
|
room = "2.6.1"
|
||||||
junit = "4.13.2"
|
|
||||||
junitVersion = "1.1.5"
|
|
||||||
espressoCore = "3.5.1"
|
|
||||||
|
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
#Android
|
# Android
|
||||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
||||||
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
|
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
|
||||||
#UI: AndroidX, Jetpack Compose
|
|
||||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||||
|
# UI: AndroidX, Jetpack Compose
|
||||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||||
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
|
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
|
||||||
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
||||||
|
|||||||
Reference in New Issue
Block a user