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 2 years ago
parent 2b2bd496e0
commit ab7b25ef6a

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

@ -17,6 +17,9 @@
package com.google.samples.apps.nowinandroid
import android.app.Application
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.decode.SvgDecoder
import com.google.samples.apps.nowinandroid.sync.initializers.Sync
import dagger.hilt.android.HiltAndroidApp
@ -24,10 +27,25 @@ import dagger.hilt.android.HiltAndroidApp
* [Application] class for NiA
*/
@HiltAndroidApp
class NiAApp : Application() {
class NiAApp : Application(), ImageLoaderFactory {
override fun onCreate() {
super.onCreate()
// Initialize Sync; the system responsible for keeping data in the app up to date.
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,
TopicEntity::class,
],
version = 2,
version = 3,
autoMigrations = [
AutoMigration(from = 1, to = 2)
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3, spec = DatabaseMigrations.Schema2to3::class),
],
exportSchema = true,
)

@ -16,6 +16,7 @@
package com.google.samples.apps.nowinandroid.core.database.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@ -35,11 +36,20 @@ data class TopicEntity(
@PrimaryKey
val id: Int,
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(
id = id,
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(
id = id,
name = name,
description = description,
shortDescription = shortDescription,
longDescription = longDescription,
url = url,
imageUrl = imageUrl
)

@ -49,7 +49,10 @@ class FakeTopicsRepository @Inject constructor(
Topic(
id = it.id,
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(
id = 3,
name = "name",
description = "description",
shortDescription = "short description",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
)
),
)
@ -83,7 +86,10 @@ class PopulatedNewsResourceKtTest {
Topic(
id = 3,
name = "name",
description = "description",
shortDescription = "short description",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
)
)
),

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

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

@ -31,7 +31,10 @@ class TestTopicDao : TopicDao {
TopicEntity(
id = 1,
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(
val id: Int,
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 {
val sampleTopic = NetworkTopic(
id = 0,
name = "Headlines",
description = "At vero eos et accusamus et iusto odio dignissimos ducimus qui.",
id = 1,
name = "UI",
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(
id = 1,
@ -54,101 +57,166 @@ object FakeDataSource {
@Language("JSON")
val topicsData = """
[
{
"id": 0,
"name": "Headlines",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui."
},
{
"id": 1,
"name": "UI",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui."
},
{
"id": 2,
"name": "Testing",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui."
},
{
"id": 3,
"name": "Performance",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui."
},
{
"id": 4,
"name": "Camera & Media",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui."
},
{
"id": 5,
"name": "Android Studio",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui."
},
{
"id": 6,
"name": "New APIs & Libraries",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui."
},
{
"id": 7,
"name": "Data Storage",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui."
},
{
"id": 8,
"name": "Kotlin",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui."
},
{
"id": 9,
"name": "Compose",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui."
},
{
"id": 10,
"name": "Privacy & Security",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui."
},
{
"id": 11,
"name": "Publishing & Distribution",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui."
},
{
"id": 12,
"name": "Tools",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui."
},
{
"id": 13,
"name": "Platform & Releases",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui."
},
{
"id": 14,
"name": "Architecture",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui."
},
{
"id": 15,
"name": "Accessibility",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui."
},
{
"id": 16,
"name": "Android Auto",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui."
},
{
"id": 17,
"name": "Games",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui."
},
{
"id": 18,
"name": "Wear OS",
"description": "At vero eos et accusamus et iusto odio dignissimos ducimus qui."
}
{
"id": "1",
"name": "UI",
"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": "0",
"name": "Headlines",
"shortDescription": "News we want everyone to see",
"longDescription": "Stay up to date with the latest events and announcements from Android!",
"url": "",
"imageUrl": ""
},
{
"id": "2",
"name": "Testing",
"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",
"name": "Performance",
"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",
"name": "Camera & Media",
"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",
"name": "Android Studio",
"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",
"name": "New APIs & Libraries",
"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",
"name": "Data Storage",
"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",
"name": "Kotlin",
"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",
"name": "Compose",
"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",
"name": "Privacy & Security",
"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",
"name": "Publishing & Distribution",
"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",
"name": "Tools",
"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",
"name": "Platform & Releases",
"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",
"name": "Architecture",
"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",
"name": "Accessibility",
"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",
"name": "Android Auto",
"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",
"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",
"shortDescription": "",
"longDescription": "Learn about new tools and best practices to support your game app development and game performance.",
"url": "",
"imageUrl": ""
},
{
"id": "19",
"name": "Wear OS",
"shortDescription": "",
"longDescription": "Learn about new tools and best practices to support your Wear OS development and watch performance.",
"url": "",
"imageUrl": ""
}
]
""".trimIndent()

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

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

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

@ -98,7 +98,7 @@ class FollowingScreenTest {
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText(TOPIC_DESC)
.onAllNodesWithText(TOPIC_SHORT_DESC)
.assertCountEquals(testTopics.count())
composeTestRule
@ -133,14 +133,20 @@ class FollowingScreenTest {
private const val TOPIC_1_NAME = "Headlines"
private const val TOPIC_2_NAME = "UI"
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(
FollowableTopic(
Topic(
id = 0,
name = TOPIC_1_NAME,
description = TOPIC_DESC,
shortDescription = TOPIC_SHORT_DESC,
longDescription = TOPIC_LONG_DESC,
url = TOPIC_URL,
imageUrl = TOPIC_IMAGE_URL,
),
isFollowed = true
),
@ -148,7 +154,10 @@ private val testTopics = listOf(
Topic(
id = 1,
name = TOPIC_2_NAME,
description = TOPIC_DESC
shortDescription = TOPIC_SHORT_DESC,
longDescription = TOPIC_LONG_DESC,
url = TOPIC_URL,
imageUrl = TOPIC_IMAGE_URL,
),
isFollowed = false
),
@ -156,7 +165,10 @@ private val testTopics = listOf(
Topic(
id = 2,
name = TOPIC_3_NAME,
description = TOPIC_DESC
shortDescription = TOPIC_SHORT_DESC,
longDescription = TOPIC_LONG_DESC,
url = TOPIC_URL,
imageUrl = TOPIC_IMAGE_URL,
),
isFollowed = false
)

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

@ -111,14 +111,20 @@ class FollowingViewModelTest {
private const val TOPIC_1_NAME = "Android Studio"
private const val TOPIC_2_NAME = "Build"
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(
FollowableTopic(
Topic(
id = 0,
name = TOPIC_1_NAME,
description = TOPIC_DESC,
shortDescription = TOPIC_SHORT_DESC,
longDescription = TOPIC_LONG_DESC,
url = TOPIC_URL,
imageUrl = TOPIC_IMAGE_URL,
),
isFollowed = true
),
@ -126,7 +132,10 @@ private val testInputTopics = listOf(
Topic(
id = 1,
name = TOPIC_2_NAME,
description = TOPIC_DESC
shortDescription = TOPIC_SHORT_DESC,
longDescription = TOPIC_LONG_DESC,
url = TOPIC_URL,
imageUrl = TOPIC_IMAGE_URL,
),
isFollowed = false
),
@ -134,7 +143,10 @@ private val testInputTopics = listOf(
Topic(
id = 2,
name = TOPIC_3_NAME,
description = TOPIC_DESC
shortDescription = TOPIC_SHORT_DESC,
longDescription = TOPIC_LONG_DESC,
url = TOPIC_URL,
imageUrl = TOPIC_IMAGE_URL,
),
isFollowed = false
)
@ -145,7 +157,10 @@ private val testOutputTopics = listOf(
Topic(
id = 0,
name = TOPIC_1_NAME,
description = TOPIC_DESC,
shortDescription = TOPIC_SHORT_DESC,
longDescription = TOPIC_LONG_DESC,
url = TOPIC_URL,
imageUrl = TOPIC_IMAGE_URL,
),
isFollowed = true
),
@ -153,7 +168,10 @@ private val testOutputTopics = listOf(
Topic(
id = 1,
name = TOPIC_2_NAME,
description = TOPIC_DESC
shortDescription = TOPIC_SHORT_DESC,
longDescription = TOPIC_LONG_DESC,
url = TOPIC_URL,
imageUrl = TOPIC_IMAGE_URL,
),
isFollowed = true
),
@ -161,7 +179,10 @@ private val testOutputTopics = listOf(
Topic(
id = 2,
name = TOPIC_3_NAME,
description = TOPIC_DESC
shortDescription = TOPIC_SHORT_DESC,
longDescription = TOPIC_LONG_DESC,
url = TOPIC_URL,
imageUrl = TOPIC_IMAGE_URL,
),
isFollowed = false
)

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

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

@ -105,7 +105,10 @@ class ForYouViewModelTest {
topic = Topic(
id = 0,
name = "Headlines",
description = ""
shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
),
isFollowed = false
),
@ -113,7 +116,10 @@ class ForYouViewModelTest {
topic = Topic(
id = 1,
name = "UI",
description = ""
shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
),
isFollowed = false
),
@ -121,7 +127,10 @@ class ForYouViewModelTest {
topic = Topic(
id = 2,
name = "Tools",
description = "",
shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
),
isFollowed = false
),
@ -177,7 +186,10 @@ class ForYouViewModelTest {
topic = Topic(
id = 0,
name = "Headlines",
description = ""
shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
),
isFollowed = false
),
@ -185,7 +197,10 @@ class ForYouViewModelTest {
topic = Topic(
id = 1,
name = "UI",
description = ""
shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
),
isFollowed = true
),
@ -193,7 +208,10 @@ class ForYouViewModelTest {
topic = Topic(
id = 2,
name = "Tools",
description = "",
shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
),
isFollowed = false
)
@ -237,7 +255,10 @@ class ForYouViewModelTest {
topic = Topic(
id = 0,
name = "Headlines",
description = ""
shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
),
isFollowed = false
),
@ -245,7 +266,10 @@ class ForYouViewModelTest {
topic = Topic(
id = 1,
name = "UI",
description = ""
shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
),
isFollowed = false
),
@ -253,7 +277,10 @@ class ForYouViewModelTest {
topic = Topic(
id = 2,
name = "Tools",
description = "",
shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
),
isFollowed = false
)
@ -335,17 +362,26 @@ private val sampleTopics = listOf(
Topic(
id = 0,
name = "Headlines",
description = ""
shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
),
Topic(
id = 1,
name = "UI",
description = ""
shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
),
Topic(
id = 2,
name = "Tools",
description = ""
shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
)
)
@ -366,7 +402,10 @@ private val sampleNewsResources = listOf(
Topic(
id = 0,
name = "Headlines",
description = ""
shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
)
),
authors = emptyList()
@ -386,7 +425,10 @@ private val sampleNewsResources = listOf(
Topic(
id = 1,
name = "UI",
description = ""
shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
),
),
authors = emptyList()
@ -404,7 +446,10 @@ private val sampleNewsResources = listOf(
Topic(
id = 1,
name = "UI",
description = ""
shortDescription = "",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
),
),
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" }
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-svg = { group = "io.coil-kt", name = "coil-svg", version.ref = "coil"}
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-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hiltExt" }

Loading…
Cancel
Save