Add new fields to Topic and display SVG icon.

This CL makes the following changes to the Topic model:

Renamed description -> shortDescription
Added longDescription, url and imageUrl (this is the URL of the icon)

This makes the model of Topic in the app match the model in the back end.

The icon (an SVG file) for each topic is displayed in the Topic list UI.

Change-Id: Ibbb35c8be879ae4d6e45b69fa0eafa88f3537052
pull/2/head
Don Turner 4 years ago
parent 2b2bd496e0
commit ab7b25ef6a

@ -139,6 +139,9 @@ dependencies {
implementation libs.material3 implementation libs.material3
implementation libs.androidx.profileinstaller implementation libs.androidx.profileinstaller
implementation libs.coil.kt
implementation libs.coil.kt.svg
implementation libs.hilt.android implementation libs.hilt.android
kapt libs.hilt.compiler kapt libs.hilt.compiler
kaptAndroidTest libs.hilt.compiler kaptAndroidTest libs.hilt.compiler

@ -17,6 +17,9 @@
package com.google.samples.apps.nowinandroid package com.google.samples.apps.nowinandroid
import android.app.Application import android.app.Application
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.decode.SvgDecoder
import com.google.samples.apps.nowinandroid.sync.initializers.Sync import com.google.samples.apps.nowinandroid.sync.initializers.Sync
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
@ -24,10 +27,25 @@ import dagger.hilt.android.HiltAndroidApp
* [Application] class for NiA * [Application] class for NiA
*/ */
@HiltAndroidApp @HiltAndroidApp
class NiAApp : Application() { class NiAApp : Application(), ImageLoaderFactory {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
// Initialize Sync; the system responsible for keeping data in the app up to date. // Initialize Sync; the system responsible for keeping data in the app up to date.
Sync.initialize(context = this) Sync.initialize(context = this)
} }
/**
* Since we're displaying SVGs in the app, Coil needs an ImageLoader which supports this
* format. During Coil's initialization it will call `applicationContext.newImageLoader()` to
* obtain an ImageLoader.
*
* @see https://github.com/coil-kt/coil/blob/main/coil-singleton/src/main/java/coil/Coil.kt#L63
*/
override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(this)
.components {
add(SvgDecoder.Factory())
}
.build()
}
} }

@ -0,0 +1,387 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "f593c030a1a8b5af8e13c6ac6a0926a9",
"entities": [
{
"tableName": "authors",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `image_url` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "imageUrl",
"columnName": "image_url",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_authors_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_authors_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
},
{
"tableName": "episodes_authors",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`episode_id` INTEGER NOT NULL, `author_id` INTEGER NOT NULL, PRIMARY KEY(`episode_id`, `author_id`), FOREIGN KEY(`episode_id`) REFERENCES `episodes`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`author_id`) REFERENCES `authors`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "episodeId",
"columnName": "episode_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "authorId",
"columnName": "author_id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"episode_id",
"author_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "episodes",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"episode_id"
],
"referencedColumns": [
"id"
]
},
{
"table": "authors",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"author_id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "episodes",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `publish_date` INTEGER NOT NULL, `alternate_video` TEXT, `alternate_audio` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "publishDate",
"columnName": "publish_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alternateVideo",
"columnName": "alternate_video",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "alternateAudio",
"columnName": "alternate_audio",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "news_resources_authors",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`news_resource_id` INTEGER NOT NULL, `author_id` INTEGER NOT NULL, PRIMARY KEY(`news_resource_id`, `author_id`), FOREIGN KEY(`news_resource_id`) REFERENCES `news_resources`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`author_id`) REFERENCES `authors`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "newsResourceId",
"columnName": "news_resource_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "authorId",
"columnName": "author_id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"news_resource_id",
"author_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "news_resources",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"news_resource_id"
],
"referencedColumns": [
"id"
]
},
{
"table": "authors",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"author_id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "news_resources",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `episode_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `url` TEXT NOT NULL, `header_image_url` TEXT, `publish_date` INTEGER NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`episode_id`) REFERENCES `episodes`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "episodeId",
"columnName": "episode_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "headerImageUrl",
"columnName": "header_image_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "publishDate",
"columnName": "publish_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "episodes",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"episode_id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "news_resources_topics",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`news_resource_id` INTEGER NOT NULL, `topic_id` INTEGER NOT NULL, PRIMARY KEY(`news_resource_id`, `topic_id`), FOREIGN KEY(`news_resource_id`) REFERENCES `news_resources`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`topic_id`) REFERENCES `topics`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "newsResourceId",
"columnName": "news_resource_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "topicId",
"columnName": "topic_id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"news_resource_id",
"topic_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "news_resources",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"news_resource_id"
],
"referencedColumns": [
"id"
]
},
{
"table": "topics",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"topic_id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "topics",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `shortDescription` TEXT NOT NULL, `longDescription` TEXT NOT NULL DEFAULT '', `url` TEXT NOT NULL DEFAULT '', `imageUrl` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "shortDescription",
"columnName": "shortDescription",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "longDescription",
"columnName": "longDescription",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "imageUrl",
"columnName": "imageUrl",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_topics_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_topics_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f593c030a1a8b5af8e13c6ac6a0926a9')"
]
}
}

@ -0,0 +1,37 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.database
import androidx.room.RenameColumn
import androidx.room.migration.AutoMigrationSpec
/**
* Automatic schema migrations sometimes require extra instructions to perform the migration, for
* example, when a column is renamed. These extra instructions are placed here by creating a class
* using the following naming convention `SchemaXtoY` where X is the schema version you're migrating
* from and Y is the schema version you're migrating to. The class should implement
* `AutoMigrationSpec`.
*/
class DatabaseMigrations {
@RenameColumn(
tableName = "topics",
fromColumnName = "description",
toColumnName = "shortDescription"
)
class Schema2to3 : AutoMigrationSpec
}

@ -44,9 +44,10 @@ import com.google.samples.apps.nowinandroid.core.database.util.NewsResourceTypeC
NewsResourceTopicCrossRef::class, NewsResourceTopicCrossRef::class,
TopicEntity::class, TopicEntity::class,
], ],
version = 2, version = 3,
autoMigrations = [ autoMigrations = [
AutoMigration(from = 1, to = 2) AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3, spec = DatabaseMigrations.Schema2to3::class),
], ],
exportSchema = true, exportSchema = true,
) )

@ -16,6 +16,7 @@
package com.google.samples.apps.nowinandroid.core.database.model package com.google.samples.apps.nowinandroid.core.database.model
import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Index import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
@ -35,11 +36,20 @@ data class TopicEntity(
@PrimaryKey @PrimaryKey
val id: Int, val id: Int,
val name: String, val name: String,
val description: String, val shortDescription: String,
@ColumnInfo(defaultValue = "")
val longDescription: String,
@ColumnInfo(defaultValue = "")
val url: String,
@ColumnInfo(defaultValue = "")
val imageUrl: String,
) )
fun TopicEntity.asExternalModel() = Topic( fun TopicEntity.asExternalModel() = Topic(
id = id, id = id,
name = name, name = name,
description = description, shortDescription = shortDescription,
longDescription = longDescription,
url = url,
imageUrl = imageUrl,
) )

@ -22,5 +22,8 @@ import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
fun NetworkTopic.asEntity() = TopicEntity( fun NetworkTopic.asEntity() = TopicEntity(
id = id, id = id,
name = name, name = name,
description = description, shortDescription = shortDescription,
longDescription = longDescription,
url = url,
imageUrl = imageUrl
) )

@ -49,7 +49,10 @@ class FakeTopicsRepository @Inject constructor(
Topic( Topic(
id = it.id, id = it.id,
name = it.name, name = it.name,
description = it.description shortDescription = it.shortDescription,
longDescription = it.longDescription,
url = it.url,
imageUrl = it.imageUrl
) )
} }
) )

@ -56,7 +56,10 @@ class PopulatedNewsResourceKtTest {
TopicEntity( TopicEntity(
id = 3, id = 3,
name = "name", name = "name",
description = "description", shortDescription = "short description",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
) )
), ),
) )
@ -83,7 +86,10 @@ class PopulatedNewsResourceKtTest {
Topic( Topic(
id = 3, id = 3,
name = "name", name = "name",
description = "description", shortDescription = "short description",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
) )
) )
), ),

@ -48,13 +48,19 @@ class NetworkEntityKtTest {
val networkModel = NetworkTopic( val networkModel = NetworkTopic(
id = 0, id = 0,
name = "Test", name = "Test",
description = "something" shortDescription = "short description",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
) )
val entity = networkModel.asEntity() val entity = networkModel.asEntity()
assertEquals(0, entity.id) assertEquals(0, entity.id)
assertEquals("Test", entity.name) assertEquals("Test", entity.name)
assertEquals("something", entity.description) assertEquals("short description", entity.shortDescription)
assertEquals("long description", entity.longDescription)
assertEquals("URL", entity.url)
assertEquals("image URL", entity.imageUrl)
} }
@Test @Test

@ -94,7 +94,10 @@ private fun NewsResourceEntity.asPopulatedNewsResource() = PopulatedNewsResource
TopicEntity( TopicEntity(
id = filteredTopicIds.random(), id = filteredTopicIds.random(),
name = "name", name = "name",
description = "description", shortDescription = "short description",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
) )
), ),
) )

@ -31,7 +31,10 @@ class TestTopicDao : TopicDao {
TopicEntity( TopicEntity(
id = 1, id = 1,
name = "Topic", name = "Topic",
description = "A topic", shortDescription = "short description",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
) )
) )

@ -22,5 +22,8 @@ package com.google.samples.apps.nowinandroid.core.model.data
data class Topic( data class Topic(
val id: Int, val id: Int,
val name: String, val name: String,
val description: String, val shortDescription: String,
val longDescription: String,
val url: String,
val imageUrl: String,
) )

@ -26,9 +26,12 @@ import org.intellij.lang.annotations.Language
object FakeDataSource { object FakeDataSource {
val sampleTopic = NetworkTopic( val sampleTopic = NetworkTopic(
id = 0, id = 1,
name = "Headlines", name = "UI",
description = "At vero eos et accusamus et iusto odio dignissimos ducimus qui.", shortDescription = "Material Design, Navigation, Text, Paging, Compose, Accessibility (a11y), Internationalization (i18n), Localization (l10n), Animations, Large Screens, Widgets",
longDescription = "Learn how to optimize your app's user interface - everything that users can see and interact with. Stay up to date on tocpis such as Material Design, Navigation, Text, Paging, Compose, Accessibility (a11y), Internationalization (i18n), Localization (l10n), Animations, Large Screens, Widgets, and many more!",
url = "url",
imageUrl = "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_UI.svg?alt=media&token=5d1d25a8-db1b-4cf1-9706-82ba0d133bf9"
) )
val sampleResource = NetworkNewsResource( val sampleResource = NetworkNewsResource(
id = 1, id = 1,
@ -55,99 +58,164 @@ object FakeDataSource {
val topicsData = """ val topicsData = """
[ [
{ {
"id": 0, "id": "1",
"name": "Headlines", "name": "UI",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui." "shortDescription": "Material Design, Navigation, Text, Paging, Compose, Accessibility (a11y), Internationalization (i18n), Localization (l10n), Animations, Large Screens, Widgets",
"longDescription": "Learn how to optimize your app's user interface - everything that users can see and interact with. Stay up to date on tocpis such as Material Design, Navigation, Text, Paging, Compose, Accessibility (a11y), Internationalization (i18n), Localization (l10n), Animations, Large Screens, Widgets, and many more!",
"url": "url",
"imageUrl": "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_UI.svg?alt=media&token=5d1d25a8-db1b-4cf1-9706-82ba0d133bf9"
}, },
{ {
"id": 1, "id": "0",
"name": "UI", "name": "Headlines",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui." "shortDescription": "News we want everyone to see",
"longDescription": "Stay up to date with the latest events and announcements from Android!",
"url": "",
"imageUrl": ""
}, },
{ {
"id": 2, "id": "2",
"name": "Testing", "name": "Testing",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui." "shortDescription": "CI, Espresso, TestLab, etc",
"longDescription": "Testing is an integral part of the app development process. By running tests against your app consistently, you can verify your app's correctness, functional behavior, and usability before you release it publicly. Stay up to date on the latest tricks in CI, Espresso, and Firebase TestLab.",
"url": "",
"imageUrl": "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_Testing.svg?alt=media&token=0d11b0b9-3eee-438e-8f64-b420ba6d445c"
}, },
{ {
"id": 3, "id": "3",
"name": "Performance", "name": "Performance",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui." "shortDescription": "Optimization, profiling",
"longDescription": "Topics here will try to optimize your app perfoamnce by profiling and identifying areas in which your app makes inefficient use of resources such as the CPU, memory, graphics, network, or the device battery.",
"url": "",
"imageUrl": "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_Performance.svg?alt=media&token=2becab75-8ba0-4af8-8f46-1aee1b299463"
}, },
{ {
"id": 4, "id": "4",
"name": "Camera & Media", "name": "Camera & Media",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui." "shortDescription": "",
"longDescription": "Learn about Android's robust APIs for playing and recording media, help add video, audio, and photo capabilities to your app!",
"url": "",
"imageUrl": "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_Camera%20%26%20Media.svg?alt=media&token=1c4efeec-88fa-4777-b50b-fb79e5cdfef9"
}, },
{ {
"id": 5, "id": "5",
"name": "Android Studio", "name": "Android Studio",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui." "shortDescription": "",
"longDescription": "Android Studio is the official integrated development environment (IDE) for Android development. It provides the fastest tools for building apps on every type of Android device.",
"url": "",
"imageUrl": "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_Android%20Studio.svg?alt=media&token=b946fbef-5a27-49e6-8f58-12d89d6b6512"
}, },
{ {
"id": 6, "id": "6",
"name": "New APIs & Libraries", "name": "New APIs & Libraries",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui." "shortDescription": "New Jetpack libraries",
"longDescription": "Stay up to date with the latest new APIs & libraires",
"url": "",
"imageUrl": "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_New%20APIs%20%26%20Libraries.svg?alt=media&token=317397c4-a173-435b-9a07-2ca35b7beaf6"
}, },
{ {
"id": 7, "id": "7",
"name": "Data Storage", "name": "Data Storage",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui." "shortDescription": "Room, Data Store",
"longDescription": "Android uses a file system that's similar to disk-based file systems on other platforms. The system provides several options for you to save your app data: App-specific storage, shared storage, preferences, and databases - learn about Room and Data Store!",
"url": "",
"imageUrl": "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_Data%20Storage.svg?alt=media&token=1dcddccc-b088-45a4-a23d-d874bd047eab"
}, },
{ {
"id": 8, "id": "8",
"name": "Kotlin", "name": "Kotlin",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui." "shortDescription": "",
"longDescription": "Kotlin is a modern statically typed programming language used by over 60% of professional Android developers that helps boost productivity, developer satisfaction, and code safety.",
"url": "",
"imageUrl": "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_Kotlin.svg?alt=media&token=e0bc5290-3670-4abb-b6a3-abf47327c332"
}, },
{ {
"id": 9, "id": "9",
"name": "Compose", "name": "Compose",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui." "shortDescription": "",
"longDescription": "Jetpack Compose is Androids modern toolkit for building native UI. It simplifies and accelerates UI development on Android. Quickly bring your app to life with less code, powerful tools, and intuitive Kotlin APIs.",
"url": "",
"imageUrl": "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_Compose.svg?alt=media&token=c7cee979-5062-49a9-a653-6fb10530d59d"
}, },
{ {
"id": 10, "id": "10",
"name": "Privacy & Security", "name": "Privacy & Security",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui." "shortDescription": "Privacy, Security",
"longDescription": "Learn about best practices and resources to help developers design and implement safe, secure, and private apps.",
"url": "",
"imageUrl": "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_Privacy%20%26%20Security.svg?alt=media&token=48cb3487-32f9-40fc-bf62-c488973150fc"
}, },
{ {
"id": 11, "id": "11",
"name": "Publishing & Distribution", "name": "Publishing & Distribution",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui." "shortDescription": "Google Play",
"longDescription": "Learn about Google Play publish and distrubution system to make your Android applications available to users.",
"url": "",
"imageUrl": "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_Publishing%20%26%20Distribution.svg?alt=media&token=e65d36cb-4050-4f56-be9f-34c599d38805"
}, },
{ {
"id": 12, "id": "12",
"name": "Tools", "name": "Tools",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui." "shortDescription": "Gradle, Memory Safety, Debugging",
"longDescription": "Android Studio, Compose tooling, APK Analyzer, Fast emulator, Intelligent code editor, Flexible build system, Realtime profilers, Gradle, Memory Safety, Debugging",
"url": "",
"imageUrl": "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_Tools.svg?alt=media&token=4df6167c-06ef-4fdd-9f7b-94a5d7f3376b"
}, },
{ {
"id": 13, "id": "13",
"name": "Platform & Releases", "name": "Platform & Releases",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui." "shortDescription": "Android 12, Android 13, etc",
"longDescription": "Stay up to date with the latest Android releases and features!",
"url": "",
"imageUrl": "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_Platform%20%26%20Releases.svg?alt=media&token=57779dd8-3b19-4e58-9959-25ff4aeef5a2"
}, },
{ {
"id": 14, "id": "14",
"name": "Architecture", "name": "Architecture",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui." "shortDescription": "Lifecycle, Dependency Injection, WorkManager",
"longDescription": "Lifecycle, Dependency Injection, WorkManager",
"url": "",
"imageUrl": "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_Architecture.svg?alt=media&token=8f946cb6-2efa-462f-94b9-fb5112bcee48"
}, },
{ {
"id": 15, "id": "15",
"name": "Accessibility", "name": "Accessibility",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui." "shortDescription": "",
"longDescription": "Accessibility is an important part of any app. Whether you're developing a new app or improving an existing one, consider the accessibility of your app's components.\n\nBy integrating accessibility features and services, you can improve your app's usability, particularly for users with disabilities.",
"url": "",
"imageUrl": "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_Accessibility.svg?alt=media&token=6333941e-eeaf-4ab5-bec7-19920cc81d97"
}, },
{ {
"id": 16, "id": "16",
"name": "Android Auto", "name": "Android Auto",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui." "shortDescription": "",
"longDescription": "Lean about how to build apps that help users connect on the road through Android Automotive OS and Android Auto",
"url": "",
"imageUrl": ""
}, },
{ {
"id": 17, "id": "17",
"name": "Android TV",
"shortDescription": "",
"longDescription": "Learn about how to build a great user experience for your TV app: create immersive content on the big screen and for a remote control",
"url": "",
"imageUrl": ""
},
{
"id": "18",
"name": "Games", "name": "Games",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui." "shortDescription": "",
"longDescription": "Learn about new tools and best practices to support your game app development and game performance.",
"url": "",
"imageUrl": ""
}, },
{ {
"id": 18, "id": "19",
"name": "Wear OS", "name": "Wear OS",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui." "shortDescription": "",
"longDescription": "Learn about new tools and best practices to support your Wear OS development and watch performance.",
"url": "",
"imageUrl": ""
} }
] ]
""".trimIndent() """.trimIndent()

@ -26,6 +26,9 @@ import kotlinx.serialization.Serializable
data class NetworkTopic( data class NetworkTopic(
val id: Int, val id: Int,
val name: String = "", val name: String = "",
val description: String = "", val shortDescription: String = "",
val longDescription: String = "",
val url: String = "",
val imageUrl: String = "",
val followed: Boolean = false, val followed: Boolean = false,
) )

@ -291,7 +291,10 @@ private val newsResource = NewsResource(
Topic( Topic(
id = 1, id = 1,
name = "Name", name = "Name",
description = "Description", shortDescription = "Short description",
longDescription = "Long description",
url = "URL",
imageUrl = "image URL"
) )
) )
) )

@ -52,6 +52,9 @@ dependencies {
testImplementation project(':core-testing') testImplementation project(':core-testing')
androidTestImplementation project(':core-testing') androidTestImplementation project(':core-testing')
implementation libs.coil.kt
implementation libs.coil.kt.compose
implementation libs.kotlinx.coroutines.android implementation libs.kotlinx.coroutines.android
implementation libs.androidx.hilt.navigation.compose implementation libs.androidx.hilt.navigation.compose

@ -98,7 +98,7 @@ class FollowingScreenTest {
.assertIsDisplayed() .assertIsDisplayed()
composeTestRule composeTestRule
.onAllNodesWithText(TOPIC_DESC) .onAllNodesWithText(TOPIC_SHORT_DESC)
.assertCountEquals(testTopics.count()) .assertCountEquals(testTopics.count())
composeTestRule composeTestRule
@ -133,14 +133,20 @@ class FollowingScreenTest {
private const val TOPIC_1_NAME = "Headlines" private const val TOPIC_1_NAME = "Headlines"
private const val TOPIC_2_NAME = "UI" private const val TOPIC_2_NAME = "UI"
private const val TOPIC_3_NAME = "Tools" private const val TOPIC_3_NAME = "Tools"
private const val TOPIC_DESC = "At vero eos et accusamus et iusto odio dignissimos ducimus qui." private const val TOPIC_SHORT_DESC = "At vero eos et accusamus."
private const val TOPIC_LONG_DESC = "At vero eos et accusamus et iusto odio dignissimos ducimus."
private const val TOPIC_URL = "URL"
private const val TOPIC_IMAGE_URL = "Image URL"
private val testTopics = listOf( private val testTopics = listOf(
FollowableTopic( FollowableTopic(
Topic( Topic(
id = 0, id = 0,
name = TOPIC_1_NAME, name = TOPIC_1_NAME,
description = TOPIC_DESC, shortDescription = TOPIC_SHORT_DESC,
longDescription = TOPIC_LONG_DESC,
url = TOPIC_URL,
imageUrl = TOPIC_IMAGE_URL,
), ),
isFollowed = true isFollowed = true
), ),
@ -148,7 +154,10 @@ private val testTopics = listOf(
Topic( Topic(
id = 1, id = 1,
name = TOPIC_2_NAME, name = TOPIC_2_NAME,
description = TOPIC_DESC shortDescription = TOPIC_SHORT_DESC,
longDescription = TOPIC_LONG_DESC,
url = TOPIC_URL,
imageUrl = TOPIC_IMAGE_URL,
), ),
isFollowed = false isFollowed = false
), ),
@ -156,7 +165,10 @@ private val testTopics = listOf(
Topic( Topic(
id = 2, id = 2,
name = TOPIC_3_NAME, name = TOPIC_3_NAME,
description = TOPIC_DESC shortDescription = TOPIC_SHORT_DESC,
longDescription = TOPIC_LONG_DESC,
url = TOPIC_URL,
imageUrl = TOPIC_IMAGE_URL,
), ),
isFollowed = false isFollowed = false
) )

@ -45,6 +45,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import coil.compose.AsyncImage
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.ui.NiaLoadingIndicator import com.google.samples.apps.nowinandroid.core.ui.NiaLoadingIndicator
@ -140,16 +141,17 @@ fun FollowingTopicCard(
) { ) {
TopicIcon( TopicIcon(
modifier = Modifier.padding(end = 24.dp), modifier = Modifier.padding(end = 24.dp),
topicImageUrl = followableTopic.topic.imageUrl,
onClick = onTopicClick onClick = onTopicClick
) )
Column( Column(
Modifier Modifier
.wrapContentSize(Alignment.Center) .wrapContentSize(Alignment.CenterStart)
.weight(1f) .weight(1f)
.clickable { onTopicClick() } .clickable { onTopicClick() }
) { ) {
TopicTitle(topicName = followableTopic.topic.name) TopicTitle(topicName = followableTopic.topic.name)
TopicDescription(topicDescription = followableTopic.topic.description) TopicDescription(topicDescription = followableTopic.topic.shortDescription)
} }
FollowButton( FollowButton(
topicId = followableTopic.topic.id, topicId = followableTopic.topic.id,
@ -176,23 +178,35 @@ fun TopicDescription(topicDescription: String) {
Text( Text(
text = topicDescription, text = topicDescription,
style = MaterialTheme.typography.body2, style = MaterialTheme.typography.body2,
modifier = Modifier.wrapContentSize(Alignment.Center) modifier = Modifier.wrapContentSize(Alignment.CenterStart)
) )
} }
@Composable @Composable
fun TopicIcon( fun TopicIcon(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
topicImageUrl: String,
onClick: () -> Unit onClick: () -> Unit
) { ) {
val iconModifier = modifier.size(64.dp)
.clickable { onClick() }
val contentDescription = stringResource(id = R.string.following_topic_card_icon_content_desc)
if (topicImageUrl.isEmpty()) {
Icon( Icon(
imageVector = Icons.Filled.Android, imageVector = Icons.Filled.Android,
tint = Color.Magenta, tint = Color.Magenta,
contentDescription = stringResource(id = R.string.following_topic_card_icon_content_desc), contentDescription = contentDescription,
modifier = modifier modifier = iconModifier
.size(64.dp) )
.clickable { onClick() } } else {
AsyncImage(
model = topicImageUrl,
contentDescription = contentDescription,
modifier = iconModifier
) )
}
} }
@Composable @Composable
@ -249,7 +263,10 @@ fun TopicCardPreview() {
Topic( Topic(
id = 0, id = 0,
name = "Compose", name = "Compose",
description = "Description" shortDescription = "Short description",
longDescription = "Long description",
url = "URL",
imageUrl = "imageUrl"
), ),
isFollowed = false isFollowed = false
), ),

@ -111,14 +111,20 @@ class FollowingViewModelTest {
private const val TOPIC_1_NAME = "Android Studio" private const val TOPIC_1_NAME = "Android Studio"
private const val TOPIC_2_NAME = "Build" private const val TOPIC_2_NAME = "Build"
private const val TOPIC_3_NAME = "Compose" private const val TOPIC_3_NAME = "Compose"
private const val TOPIC_DESC = "At vero eos et accusamus et iusto odio dignissimos ducimus qui." private const val TOPIC_SHORT_DESC = "At vero eos et accusamus."
private const val TOPIC_LONG_DESC = "At vero eos et accusamus et iusto odio dignissimos ducimus."
private const val TOPIC_URL = "URL"
private const val TOPIC_IMAGE_URL = "Image URL"
private val testInputTopics = listOf( private val testInputTopics = listOf(
FollowableTopic( FollowableTopic(
Topic( Topic(
id = 0, id = 0,
name = TOPIC_1_NAME, name = TOPIC_1_NAME,
description = TOPIC_DESC, shortDescription = TOPIC_SHORT_DESC,
longDescription = TOPIC_LONG_DESC,
url = TOPIC_URL,
imageUrl = TOPIC_IMAGE_URL,
), ),
isFollowed = true isFollowed = true
), ),
@ -126,7 +132,10 @@ private val testInputTopics = listOf(
Topic( Topic(
id = 1, id = 1,
name = TOPIC_2_NAME, name = TOPIC_2_NAME,
description = TOPIC_DESC shortDescription = TOPIC_SHORT_DESC,
longDescription = TOPIC_LONG_DESC,
url = TOPIC_URL,
imageUrl = TOPIC_IMAGE_URL,
), ),
isFollowed = false isFollowed = false
), ),
@ -134,7 +143,10 @@ private val testInputTopics = listOf(
Topic( Topic(
id = 2, id = 2,
name = TOPIC_3_NAME, name = TOPIC_3_NAME,
description = TOPIC_DESC shortDescription = TOPIC_SHORT_DESC,
longDescription = TOPIC_LONG_DESC,
url = TOPIC_URL,
imageUrl = TOPIC_IMAGE_URL,
), ),
isFollowed = false isFollowed = false
) )
@ -145,7 +157,10 @@ private val testOutputTopics = listOf(
Topic( Topic(
id = 0, id = 0,
name = TOPIC_1_NAME, name = TOPIC_1_NAME,
description = TOPIC_DESC, shortDescription = TOPIC_SHORT_DESC,
longDescription = TOPIC_LONG_DESC,
url = TOPIC_URL,
imageUrl = TOPIC_IMAGE_URL,
), ),
isFollowed = true isFollowed = true
), ),
@ -153,7 +168,10 @@ private val testOutputTopics = listOf(
Topic( Topic(
id = 1, id = 1,
name = TOPIC_2_NAME, name = TOPIC_2_NAME,
description = TOPIC_DESC shortDescription = TOPIC_SHORT_DESC,
longDescription = TOPIC_LONG_DESC,
url = TOPIC_URL,
imageUrl = TOPIC_IMAGE_URL,
), ),
isFollowed = true isFollowed = true
), ),
@ -161,7 +179,10 @@ private val testOutputTopics = listOf(
Topic( Topic(
id = 2, id = 2,
name = TOPIC_3_NAME, name = TOPIC_3_NAME,
description = TOPIC_DESC shortDescription = TOPIC_SHORT_DESC,
longDescription = TOPIC_LONG_DESC,
url = TOPIC_URL,
imageUrl = TOPIC_IMAGE_URL,
), ),
isFollowed = false isFollowed = false
) )

@ -63,7 +63,7 @@ class ForYouScreenTest {
topic = Topic( topic = Topic(
id = 0, id = 0,
name = "Headlines", name = "Headlines",
description = "" shortDescription = ""
), ),
isFollowed = false isFollowed = false
), ),
@ -71,7 +71,7 @@ class ForYouScreenTest {
topic = Topic( topic = Topic(
id = 1, id = 1,
name = "UI", name = "UI",
description = "" shortDescription = ""
), ),
isFollowed = false isFollowed = false
), ),
@ -79,7 +79,7 @@ class ForYouScreenTest {
topic = Topic( topic = Topic(
id = 2, id = 2,
name = "Tools", name = "Tools",
description = "", shortDescription = "",
), ),
isFollowed = false isFollowed = false
), ),
@ -127,7 +127,7 @@ class ForYouScreenTest {
topic = Topic( topic = Topic(
id = 0, id = 0,
name = "Headlines", name = "Headlines",
description = "" shortDescription = ""
), ),
isFollowed = false isFollowed = false
), ),
@ -135,7 +135,7 @@ class ForYouScreenTest {
topic = Topic( topic = Topic(
id = 1, id = 1,
name = "UI", name = "UI",
description = "" shortDescription = ""
), ),
isFollowed = true isFollowed = true
), ),
@ -143,7 +143,7 @@ class ForYouScreenTest {
topic = Topic( topic = Topic(
id = 2, id = 2,
name = "Tools", name = "Tools",
description = "", shortDescription = "",
), ),
isFollowed = false isFollowed = false
), ),

@ -190,7 +190,10 @@ fun ForYouScreenTopicSelection() {
topic = Topic( topic = Topic(
id = 0, id = 0,
name = "Headlines", name = "Headlines",
description = "" shortDescription = "",
longDescription = "",
url = "",
imageUrl = ""
), ),
isFollowed = false isFollowed = false
), ),
@ -198,7 +201,10 @@ fun ForYouScreenTopicSelection() {
topic = Topic( topic = Topic(
id = 1, id = 1,
name = "UI", name = "UI",
description = "" shortDescription = "",
longDescription = "",
url = "",
imageUrl = ""
), ),
isFollowed = false isFollowed = false
), ),
@ -206,7 +212,10 @@ fun ForYouScreenTopicSelection() {
topic = Topic( topic = Topic(
id = 2, id = 2,
name = "Tools", name = "Tools",
description = "", shortDescription = "",
longDescription = "",
url = "",
imageUrl = ""
), ),
isFollowed = false isFollowed = false
), ),
@ -229,7 +238,10 @@ fun ForYouScreenTopicSelection() {
Topic( Topic(
id = 0, id = 0,
name = "Headlines", name = "Headlines",
description = "" shortDescription = "",
longDescription = "",
url = "",
imageUrl = ""
) )
), ),
authors = emptyList() authors = emptyList()
@ -253,7 +265,10 @@ fun ForYouScreenTopicSelection() {
Topic( Topic(
id = 1, id = 1,
name = "UI", name = "UI",
description = "" shortDescription = "",
longDescription = "",
url = "",
imageUrl = ""
), ),
), ),
authors = emptyList() authors = emptyList()
@ -274,7 +289,10 @@ fun ForYouScreenTopicSelection() {
Topic( Topic(
id = 1, id = 1,
name = "UI", name = "UI",
description = "" shortDescription = "",
longDescription = "",
url = "",
imageUrl = ""
), ),
), ),
authors = emptyList() authors = emptyList()

@ -105,7 +105,10 @@ class ForYouViewModelTest {
topic = Topic( topic = Topic(
id = 0, id = 0,
name = "Headlines", name = "Headlines",
description = "" shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
), ),
isFollowed = false isFollowed = false
), ),
@ -113,7 +116,10 @@ class ForYouViewModelTest {
topic = Topic( topic = Topic(
id = 1, id = 1,
name = "UI", name = "UI",
description = "" shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
), ),
isFollowed = false isFollowed = false
), ),
@ -121,7 +127,10 @@ class ForYouViewModelTest {
topic = Topic( topic = Topic(
id = 2, id = 2,
name = "Tools", name = "Tools",
description = "", shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
), ),
isFollowed = false isFollowed = false
), ),
@ -177,7 +186,10 @@ class ForYouViewModelTest {
topic = Topic( topic = Topic(
id = 0, id = 0,
name = "Headlines", name = "Headlines",
description = "" shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
), ),
isFollowed = false isFollowed = false
), ),
@ -185,7 +197,10 @@ class ForYouViewModelTest {
topic = Topic( topic = Topic(
id = 1, id = 1,
name = "UI", name = "UI",
description = "" shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
), ),
isFollowed = true isFollowed = true
), ),
@ -193,7 +208,10 @@ class ForYouViewModelTest {
topic = Topic( topic = Topic(
id = 2, id = 2,
name = "Tools", name = "Tools",
description = "", shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
), ),
isFollowed = false isFollowed = false
) )
@ -237,7 +255,10 @@ class ForYouViewModelTest {
topic = Topic( topic = Topic(
id = 0, id = 0,
name = "Headlines", name = "Headlines",
description = "" shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
), ),
isFollowed = false isFollowed = false
), ),
@ -245,7 +266,10 @@ class ForYouViewModelTest {
topic = Topic( topic = Topic(
id = 1, id = 1,
name = "UI", name = "UI",
description = "" shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
), ),
isFollowed = false isFollowed = false
), ),
@ -253,7 +277,10 @@ class ForYouViewModelTest {
topic = Topic( topic = Topic(
id = 2, id = 2,
name = "Tools", name = "Tools",
description = "", shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
), ),
isFollowed = false isFollowed = false
) )
@ -335,17 +362,26 @@ private val sampleTopics = listOf(
Topic( Topic(
id = 0, id = 0,
name = "Headlines", name = "Headlines",
description = "" shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
), ),
Topic( Topic(
id = 1, id = 1,
name = "UI", name = "UI",
description = "" shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
), ),
Topic( Topic(
id = 2, id = 2,
name = "Tools", name = "Tools",
description = "" shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
) )
) )
@ -366,7 +402,10 @@ private val sampleNewsResources = listOf(
Topic( Topic(
id = 0, id = 0,
name = "Headlines", name = "Headlines",
description = "" shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
) )
), ),
authors = emptyList() authors = emptyList()
@ -386,7 +425,10 @@ private val sampleNewsResources = listOf(
Topic( Topic(
id = 1, id = 1,
name = "UI", name = "UI",
description = "" shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
), ),
), ),
authors = emptyList() authors = emptyList()
@ -404,7 +446,10 @@ private val sampleNewsResources = listOf(
Topic( Topic(
id = 1, id = 1,
name = "UI", name = "UI",
description = "" shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
), ),
), ),
authors = emptyList() authors = emptyList()

@ -82,6 +82,7 @@ androidx-work-ktx = { group = "androidx.work", name = "work-runtime-ktx", versio
androidx-work-testing = { group = "androidx.work", name = "work-testing", version.ref = "androidxWork" } androidx-work-testing = { group = "androidx.work", name = "work-testing", version.ref = "androidxWork" }
coil-kt = { group = "io.coil-kt", name = "coil", version.ref = "coil"} coil-kt = { group = "io.coil-kt", name = "coil", version.ref = "coil"}
coil-kt-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil"} coil-kt-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil"}
coil-kt-svg = { group = "io.coil-kt", name = "coil-svg", version.ref = "coil"}
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-ext-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "hiltExt" } hilt-ext-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "hiltExt" }
hilt-ext-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hiltExt" } hilt-ext-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hiltExt" }

Loading…
Cancel
Save