Jetcaster is a sample podcast app, built with Jetpack Compose. The goal of the sample is to showcase building with Compose across multiple form factors (mobile, TV, and Wear) 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.
This sample has 3 components: the home screen, the podcast details screen, and the player screen
The home screen is split into sub-screens for easy re-use:
- Home, allowing the user to see their subscribed podcasts (top carousel), and navigate between 'Your Library' and 'Discover'
- Discover, allowing the user to browse podcast categories
- Podcast Category, allowing the user to see a list of recent episodes for podcasts in a given category.
Multiple panes will also be shown depending on the device's window size class.
The player screen displays media controls and the currently "playing" podcast (the sample currently does not actually play any media—the behavior is simply mocked). The player screen layout is adapting to different form factors, including a tabletop layout on foldable devices:
Some other notable things which are implemented:
- Images are all provided from each podcast's RSS feed, and loaded using Coil library.
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:
- The ViewModel is implemented as
HomeViewModel
, which exposes aStateFlow<HomeViewState>
for the UI to observe. HomeViewState
contains the complete view state for the home screen as an@Immutable
data class
.- The Home Compose UI in
Home.kt
usesHomeViewModel
, and observes it'sHomeViewState
as Compose State, usingcollectAsStateWithLifecycle()
:
val viewModel: HomeViewModel = viewModel()
val viewState by viewModel.state.collectAsStateWithLifecycle()
This pattern is used across the different screens:
- Home:
com.example.jetcaster.ui.home
- Discover:
com.example.jetcaster.ui.home.discover
- Podcast Category:
com.example.jetcaster.ui.category
This sample showcases a 2-screen pager which allows navigation between the Player and the Library. From the Library, users can access latest episodes from subscribed podcasts, and queue. From the podcast, users can access episode details and add episodes to the queue. From the Player screen, users can access a volume screen and a playback speed screen.
The sample implements Wear UX best practices for media apps, such as:
- Support rotating side button (RSB) and Bezel for scrollable screens
- Display scrollbar on scrolling
- Display the time on top of the screens
The sample is built using the Media Toolkit which is an open source project part of Horologist to ease the development of media apps on Wear OS built on top of Compose for Wear. It provides ready to use UI screens, such the EntityScreen that is used in this sample to implement many screens such as Podcast, LatestEpisodes and Queue. Horologist also provides a VolumeScreen that can be reused by media apps to conveniently control volume either by interacting with the rotating side button(RSB)/Bezel or by using the provided buttons. For simplicity, this sample uses a mock Player which is reused across form factors, if you want to see an advanced Media sample built on Compose that uses Exoplayer and plays media content, refer to the Media Toolkit sample.
The official media app guidance for Wear OS advices to download content on the watch before listening to preserve power, this feature will be added to this sample in future iterations. You can refer to the Media Toolkit sample to learn how to implement the media download feature.
The architecture of the Wear app is similar to the phone app architecture: each UI 'screen' has its
own ViewModel which exposes a StateFlow<ScreenState>
for the UI to observe.
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:
- Each podcast is fetched using OkHttp, and then parsed using Rome, within
PodcastFetcher
. - The parsed entities are then added to the local data stores:
PodcastStore
,EpisodeStore
&CategoryStore
for storage in the local RoomJetcasterDatabase
database.
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()
.
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.