Jetcaster is a sample podcast app, built with Jetpack Compose. The goal of the sample is to showcase dynamic theming and full featured architecture.
To try out this sample app, use the latest stable version of Android Studio. You can clone this repository or import the project from Android Studio following the steps here.
Jetcaster is still in the early stages of development, and as such only one screen has been created so far. However, most of the app’s architecture has been implemented, as well as the data layer, and early stages of dynamic theming.
This sample contains 2 screens so far: the home screen, and a player screen.
The home screen is split into sub-screens for easy re-use:
The player screen displays media controls and the currently “playing” podcast (the sample currently doesn’t actually play any media). The player screen layout is adapting to different form factors, including a tabletop layout on foldable devices:
The home screen currently implements dynamic theming, using the artwork of the currently selected podcast from the carousel to update the primary
and onPrimary
colors. You can see it in action in the screenshots above: as the carousel item is changed, the background gradient is updated to match the artwork.
This is implemented in DynamicTheming.kt
, which provides the DynamicThemePrimaryColorsFromImage
composable, to automatically animate the theme colors based on the provided image URL, like so:
val dominantColorState: DominantColorState = rememberDominantColorState()
DynamicThemePrimaryColorsFromImage(dominantColorState) {
var imageUrl = remember { mutableStateOf("") }
// When the image url changes, call updateColorsFromImageUrl()
launchInComposition(imageUrl) {
dominantColorState.updateColorsFromImageUrl(imageUrl)
}
// Content which will be dynamically themed....
}
Underneath, DominantColorState
uses the Coil library to fetch the artwork image 🖼️, and then Palette to extract the dominant colors from the image 🎨.
Some other notable things which are implemented:
The app is built in a Redux-style, where each UI ‘screen’ has its own ViewModel, which exposes a single StateFlow containing the entire view state. Each ViewModel is responsible for subscribing to any data streams required for the view, as well as exposing functions which allow the UI to send events.
Using the example of the home screen in the com.example.jetcaster.ui.home
package:
HomeViewModel
, which exposes a StateFlow<HomeViewState>
for the UI to observe.HomeViewState
contains the complete view state for the home screen as an @Immutable
data class
.Home.kt
uses HomeViewModel
, and observes it’s HomeViewState
as Compose State, using collectAsStateWithLifecycle()
:val viewModel: HomeViewModel = viewModel()
val viewState by viewModel.state.collectAsStateWithLifecycle()
This pattern is used across the different screens:
com.example.jetcaster.ui.home
com.example.jetcaster.ui.home.discover
com.example.jetcaster.ui.category
The podcast data in this sample is dynamically fetched from a number of podcast RSS feeds, which are listed in Feeds.kt
.
The PodcastRepository
class is responsible for handling the data fetching of all podcast information:
PodcastFetcher
.PodcastStore
, EpisodeStore
& CategoryStore
for storage in the local Room JetcasterDatabase
database.### Follow podcasts
The sample allows users to ‘follow’ podcasts, which is implemented within the data layer in the PodcastFollowedEntry
entity class, and as functions in PodcastStore: followPodcast()
, unfollowPodcast()
.
### Date + time
The sample uses the JDK 8 date and time APIs through the desugaring support available in Android Gradle Plugin 4.0+. Relevant Room TypeConverters
are implemented in DateTimeTypeConverters.kt
.
Copyright 2020 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.