Skip to content

Commit

Permalink
Feature: Add App Theme Selection and Preferences DataStore (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
jidasetima authored Oct 24, 2024
1 parent 75b988f commit 751d96d
Show file tree
Hide file tree
Showing 74 changed files with 696 additions and 234 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ You can download APK iterations of the Movie Finder Android App from [Movie Fin
- Upcoming
- **Movie Details**: Select a movie from any category to view detailed information such as the overview, release date, and backdrop image.
- **Offline Support**: Previously fetched movie categories can be viewed even without an internet connection.
- **User Theme Selection**: Choose your preferred theme for the app, with the selection saved across sessions for a personalized experience.
- **Future Enhancements**:
- More features will be added (TBD).

Expand Down
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ dependencies {
implementation(libs.androidx.material3)
implementation(libs.androidx.lifecycle.runtime.compose.android)

// DataStore
implementation(libs.androidx.datastore)

// Hilt
implementation(libs.hilt)
ksp(libs.hilt.compiler)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.jogasoft.moviefinder.ui.navigation
package com.jogasoft.moviefinder.core.navigation

import androidx.activity.compose.setContent
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.navigation.compose.ComposeNavigator
import androidx.navigation.testing.TestNavHostController
import com.jogasoft.moviefinder.ui.HomeActivity
import com.jogasoft.moviefinder.core.ui.MainActivity
import com.jogasoft.moviefinder.core.ui.navigation.MovieFinderAppNavigationHost
import com.jogasoft.moviefinder.core.ui.navigation.NavHostTestTag
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import junit.framework.TestCase.assertEquals
Expand All @@ -20,7 +22,7 @@ class NavigationTest {
val hiltRule = HiltAndroidRule(this)

@get:Rule(order = 1)
val composeRule = createAndroidComposeRule<HomeActivity>()
val composeRule = createAndroidComposeRule<MainActivity>()

private lateinit var navController: TestNavHostController

Expand All @@ -38,10 +40,11 @@ class NavigationTest {
@Test
fun navHost_verifyStartDestination_isHomeScreen() {
val route = navController.currentBackStackEntry?.destination?.route
assertEquals(HOME_SCREEN_ROUTE, route)
assertEquals(SCREEN_ROUTE, route)
}

companion object {
const val HOME_SCREEN_ROUTE = "com.jogasoft.moviefinder.ui.navigation.HomeScreen"
//todo: find something better here
const val SCREEN_ROUTE = "com.jogasoft.moviefinder.core.ui.navigation.MovieCategoryListScreen"
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.jogasoft.moviefinder.ui.component
package com.jogasoft.moviefinder.core.ui.component

import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertIsDisplayed
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package com.jogasoft.moviefinder.data.local.database
package com.jogasoft.moviefinder.movie.data.local.database

import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.jogasoft.moviefinder.data.MovieCategory
import com.jogasoft.moviefinder.data.source.local.LocalMovie
import com.jogasoft.moviefinder.data.source.local.database.MovieDao
import com.jogasoft.moviefinder.data.source.local.database.MovieFinderDatabase
import com.jogasoft.moviefinder.movie.data.MovieCategory
import com.jogasoft.moviefinder.movie.data.source.local.LocalMovie
import com.jogasoft.moviefinder.movie.data.source.local.database.MovieDao
import com.jogasoft.moviefinder.movie.data.source.local.database.MovieFinderDatabase
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertTrue
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.jogasoft.moviefinder.ui.screen
package com.jogasoft.moviefinder.movie.ui.screen

import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertIsDisplayed
Expand All @@ -9,40 +9,41 @@ import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performScrollToNode
import com.jogasoft.moviefinder.R
import com.jogasoft.moviefinder.ui.viewModel.HomeUiState
import com.jogasoft.moviefinder.movie.ui.viewModel.MovieCategoryListUiState
import org.junit.Before
import org.junit.Rule
import org.junit.Test

class HomeScreenTest {
class MovieCategoryListScreenTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()

@Before
fun setUp() {
rule.setContent { HomeScreen(
uiState = HomeUiState(),
rule.setContent { MovieCategoryListScreen(
uiState = MovieCategoryListUiState(),
navigateToMovieDetail = {},
navigateToSearch = {}
navigateToSearch = {},
navigateToSettings = {}
) }
}

@Test
fun expected_movie_category_titles_appear() {
val nowPlayingText = rule.activity.getString(R.string.now_playing)
rule.onNodeWithTag(HomeLazyColumnTestTag).performScrollToNode(hasText(nowPlayingText))
rule.onNodeWithTag(MovieCategoryListLazyColumnTestTag).performScrollToNode(hasText(nowPlayingText))
rule.onNodeWithText(nowPlayingText).assertIsDisplayed()

val popularText = rule.activity.getString(R.string.popular)
rule.onNodeWithTag(HomeLazyColumnTestTag).performScrollToNode(hasText(popularText))
rule.onNodeWithTag(MovieCategoryListLazyColumnTestTag).performScrollToNode(hasText(popularText))
rule.onNodeWithText(popularText).assertIsDisplayed()

val topRatedText = rule.activity.getString(R.string.top_rated)
rule.onNodeWithTag(HomeLazyColumnTestTag).performScrollToNode(hasText(topRatedText))
rule.onNodeWithTag(MovieCategoryListLazyColumnTestTag).performScrollToNode(hasText(topRatedText))
rule.onNodeWithText(topRatedText).assertIsDisplayed()

val upcomingText = rule.activity.getString(R.string.upcoming)
rule.onNodeWithTag(HomeLazyColumnTestTag).performScrollToNode(hasText(upcomingText))
rule.onNodeWithTag(MovieCategoryListLazyColumnTestTag).performScrollToNode(hasText(upcomingText))
rule.onNodeWithText(upcomingText).assertIsDisplayed()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package com.jogasoft.moviefinder.ui.screen
package com.jogasoft.moviefinder.movie.ui.screen

import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import com.jogasoft.moviefinder.data.MovieDetail
import com.jogasoft.moviefinder.ui.viewModel.MovieDetailUiState
import com.jogasoft.moviefinder.movie.data.MovieDetail
import com.jogasoft.moviefinder.movie.ui.viewModel.MovieDetailUiState
import org.junit.Rule
import org.junit.Test

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.jogasoft.moviefinder.ui.screen
package com.jogasoft.moviefinder.movie.ui.screen

import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertIsDisplayed
Expand All @@ -9,7 +9,7 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
import com.jogasoft.moviefinder.R
import com.jogasoft.moviefinder.data.Movie
import com.jogasoft.moviefinder.movie.data.Movie
import kotlinx.coroutines.flow.flowOf
import org.junit.Before
import org.junit.Rule
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.jogasoft.moviefinder.userpreferences.data.source.local

import android.content.Context
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStoreFile
import androidx.test.core.app.ApplicationProvider
import com.jogasoft.moviefinder.userpreferences.data.model.ThemePreference
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNotSame
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.StandardTestDispatcher
import org.junit.After
import org.junit.Before
import org.junit.Test

class DefaultUserPreferencesLocalDataSourceTest {
private val context = ApplicationProvider.getApplicationContext<Context>()
private val testDispatcher = StandardTestDispatcher()
private val testCoroutineScope = CoroutineScope(testDispatcher)
private val dataStore = PreferenceDataStoreFactory.create(
scope = testCoroutineScope,
produceFile = {
context.preferencesDataStoreFile("test_user_preferences_dataStore")
}
)
private lateinit var defaultUserPreferencesLocalDataSource: DefaultUserPreferencesLocalDataSource

@Before
fun setUp() {
defaultUserPreferencesLocalDataSource = DefaultUserPreferencesLocalDataSource(dataStore)
}

@After
fun tearDown() {
testCoroutineScope.launch { dataStore.edit { it.clear() } }
testCoroutineScope.cancel()
}

@Test
fun confirm_default_theme_is_system() {
testCoroutineScope.launch {
val theme = defaultUserPreferencesLocalDataSource.observeThemePreference.first()
assertEquals(ThemePreference.SYSTEM, theme)
}
}

@Test
fun updateThemePreference_properly_updates_dataStore() {
testCoroutineScope.launch {
val initialTheme = defaultUserPreferencesLocalDataSource.observeThemePreference.first()
assertNotSame(ThemePreference.DARK, initialTheme)

defaultUserPreferencesLocalDataSource.updateThemePreference(ThemePreference.DARK)

val updatedTheme = defaultUserPreferencesLocalDataSource.observeThemePreference.first()

assertEquals(ThemePreference.DARK, updatedTheme)
}
}
}
4 changes: 2 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<uses-permission android:name="android.permission.INTERNET"/>

<application
android:name="com.jogasoft.MovieFinderApplication"
android:name=".core.MovieFinderApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
Expand All @@ -16,7 +16,7 @@
android:theme="@style/Theme.MovieFinder"
tools:targetApi="31">
<activity
android:name=".ui.HomeActivity"
android:name=".core.ui.MainActivity"
android:exported="true"
android:theme="@style/Theme.MovieFinder">
<intent-filter>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.jogasoft
package com.jogasoft.moviefinder.core

import android.app.Application
import dagger.hilt.android.HiltAndroidApp
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
package com.jogasoft.moviefinder.ui
package com.jogasoft.moviefinder.core.ui

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.rememberNavController
import com.jogasoft.moviefinder.ui.navigation.MovieFinderAppNavigationHost
import com.jogasoft.moviefinder.ui.theme.MovieFinderTheme
import com.jogasoft.moviefinder.core.ui.navigation.MovieFinderAppNavigationHost
import com.jogasoft.moviefinder.core.ui.theme.MovieFinderTheme
import com.jogasoft.moviefinder.userpreferences.data.model.ThemePreference
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class HomeActivity : ComponentActivity() {
class MainActivity : ComponentActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MovieFinderTheme {
val theme = viewModel.uiState.collectAsStateWithLifecycle().value.theme
val useDarkTheme = when(theme) {
ThemePreference.LIGHT -> false
ThemePreference.DARK -> true
ThemePreference.SYSTEM -> isSystemInDarkTheme()
}
MovieFinderTheme(
darkTheme = useDarkTheme
) {
Surface(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.jogasoft.moviefinder.core.ui

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.jogasoft.moviefinder.userpreferences.data.UserPreferenceRepository
import com.jogasoft.moviefinder.userpreferences.data.model.ThemePreference
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject

@OptIn(ExperimentalCoroutinesApi::class)
@HiltViewModel
class MainViewModel @Inject constructor(
userPreferenceRepository: UserPreferenceRepository
): ViewModel() {
val uiState = userPreferenceRepository.observeThemePreference.mapLatest { theme ->
MainUiState(theme)
}.stateIn(
started = SharingStarted.WhileSubscribed(5000L),
scope = viewModelScope,
initialValue = MainUiState()
)
}

data class MainUiState(
val theme: ThemePreference = ThemePreference.SYSTEM
)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.jogasoft.moviefinder.ui.component
package com.jogasoft.moviefinder.core.ui.component

import androidx.compose.foundation.layout.RowScope
import androidx.compose.material.icons.Icons
Expand All @@ -16,7 +16,7 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import com.jogasoft.moviefinder.R
import com.jogasoft.moviefinder.ui.theme.MovieFinderTheme
import com.jogasoft.moviefinder.core.ui.theme.MovieFinderTheme

//Test tags
const val AppBarBackButtonTestTag = "AppBarBackButtonTestTag"
Expand Down
Loading

0 comments on commit 751d96d

Please sign in to comment.