Skip to content
This repository has been archived by the owner on Jan 5, 2023. It is now read-only.

Commit

Permalink
Initial implementation of dark mode
Browse files Browse the repository at this point in the history
Current the UI setting a binary toggle, but the
internal logic is an enum. I'll move the setting
to a list in a dialog in the next CL.

I took a quick pass over the schedule UI to make it
semi-usable but the UI is pretty ugly right now.

Change-Id: I6bd79ad2b8f38b1ac36cec6575f41e6ef2744d80
  • Loading branch information
Chris Banes authored and thagikura committed Aug 14, 2019
1 parent a662118 commit c635ca7
Show file tree
Hide file tree
Showing 32 changed files with 433 additions and 42 deletions.
Expand Up @@ -20,6 +20,7 @@ import com.google.samples.apps.iosched.MainApplication
import com.google.samples.apps.iosched.shared.di.ServiceBindingModule
import com.google.samples.apps.iosched.shared.di.SharedModule
import com.google.samples.apps.iosched.shared.di.ViewModelModule
import com.google.samples.apps.iosched.ui.ThemedActivityDelegateModule
import com.google.samples.apps.iosched.ui.signin.SignInViewModelDelegateModule
import dagger.Component
import dagger.android.AndroidInjector
Expand All @@ -43,7 +44,9 @@ import javax.inject.Singleton
ServiceBindingModule::class,
SharedModule::class,
SignInModule::class,
SignInViewModelDelegateModule::class]
SignInViewModelDelegateModule::class,
ThemedActivityDelegateModule::class
]
)
interface AppComponent : AndroidInjector<MainApplication> {
@Component.Builder
Expand Down
Expand Up @@ -24,6 +24,7 @@ import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.drawerlayout.widget.DrawerLayout.DrawerListener
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.firebase.ui.auth.IdpResponse
import com.google.android.material.navigation.NavigationView
Expand All @@ -36,6 +37,7 @@ import com.google.samples.apps.iosched.ui.messages.SnackbarMessageManager
import com.google.samples.apps.iosched.ui.schedule.ScheduleFragment
import com.google.samples.apps.iosched.ui.schedule.ScheduleViewModel
import com.google.samples.apps.iosched.util.signin.FirebaseAuthErrorCodeConverter
import com.google.samples.apps.iosched.util.updateForTheme
import dagger.android.support.DaggerAppCompatActivity
import timber.log.Timber
import java.util.UUID
Expand Down Expand Up @@ -68,12 +70,15 @@ class MainActivity : DaggerAppCompatActivity(), DrawerListener {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
drawer = findViewById(R.id.drawer)
navigation = findViewById(R.id.navigation)

// This VM instance is shared between activity and fragments, as it's scoped to MainActivity
scheduleViewModel = viewModelProvider(viewModelFactory)
// Update for Dark Mode straight away
updateForTheme(scheduleViewModel.currentTheme)

setContentView(R.layout.activity_main)
drawer = findViewById(R.id.drawer)
navigation = findViewById(R.id.navigation)

drawer.addDrawerListener(this)
navigation.setNavigationItemSelectedListener {
Expand All @@ -93,6 +98,8 @@ class MainActivity : DaggerAppCompatActivity(), DrawerListener {
supportFragmentManager.findFragmentById(FRAGMENT_ID) as? MainNavigationFragment
?: throw IllegalStateException("Activity recreated, but no fragment found!")
}

scheduleViewModel.theme.observe(this, Observer(::updateForTheme))
}

override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
Expand Down
@@ -0,0 +1,72 @@
/*
* Copyright 2019 Google LLC
*
* 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.iosched.ui

import androidx.lifecycle.LiveData
import com.google.samples.apps.iosched.model.Theme
import com.google.samples.apps.iosched.shared.domain.settings.GetThemeUseCase
import com.google.samples.apps.iosched.shared.domain.settings.ObserveThemeModeUseCase
import com.google.samples.apps.iosched.shared.result.Result.Success
import com.google.samples.apps.iosched.shared.util.map
import javax.inject.Inject
import kotlin.LazyThreadSafetyMode.NONE

/**
* Interface to implement activity theming via a ViewModel.
*
* You can inject a implementation of this via Dagger2, then use the implementation as an interface
* delegate to add the functionality without writing any code
*
* Example usage:
* ```
* class MyViewModel @Inject constructor(
* themedActivityDelegate: ThemedActivityDelegate
* ) : ViewModel(), ThemedActivityDelegate by themedActivityDelegate {
* ```
*/
interface ThemedActivityDelegate {
/**
* Allows observing of the current theme
*/
val theme: LiveData<Theme>

/**
* Allows querying of the current theme synchronously
*/
val currentTheme: Theme
}

class ThemedActivityDelegateImpl @Inject constructor(
private val observeThemeUseCase: ObserveThemeModeUseCase,
private val getThemeUseCase: GetThemeUseCase
) : ThemedActivityDelegate {
override val theme: LiveData<Theme> by lazy(NONE) {
observeThemeUseCase.observe().map {
if (it is Success) it.data else Theme.SYSTEM
}
}

override val currentTheme: Theme
get() = getThemeUseCase.executeNow(Unit).let {
if (it is Success) it.data else Theme.SYSTEM
}

init {
// Observe updates in dark mode setting
observeThemeUseCase.execute(Unit)
}
}
@@ -0,0 +1,28 @@
/*
* Copyright 2019 Google LLC
*
* 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.iosched.ui

import dagger.Binds
import dagger.Module
import javax.inject.Singleton

@Module
abstract class ThemedActivityDelegateModule {
@Singleton
@Binds
abstract fun provideThemedActivityDelegate(impl: ThemedActivityDelegateImpl): ThemedActivityDelegate
}
Expand Up @@ -19,11 +19,14 @@ package com.google.samples.apps.iosched.ui.info
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.google.samples.apps.iosched.model.Theme
import com.google.samples.apps.iosched.shared.domain.prefs.NotificationsPrefSaveActionUseCase
import com.google.samples.apps.iosched.shared.domain.settings.GetAnalyticsSettingUseCase
import com.google.samples.apps.iosched.shared.domain.settings.GetThemeUseCase
import com.google.samples.apps.iosched.shared.domain.settings.GetNotificationsSettingUseCase
import com.google.samples.apps.iosched.shared.domain.settings.GetTimeZoneUseCase
import com.google.samples.apps.iosched.shared.domain.settings.SetAnalyticsSettingUseCase
import com.google.samples.apps.iosched.shared.domain.settings.SetThemeUseCase
import com.google.samples.apps.iosched.shared.domain.settings.SetTimeZoneUseCase
import com.google.samples.apps.iosched.shared.result.Result
import com.google.samples.apps.iosched.shared.result.Result.Success
Expand All @@ -36,21 +39,27 @@ class SettingsViewModel @Inject constructor(
val notificationsPrefSaveActionUseCase: NotificationsPrefSaveActionUseCase,
getNotificationsSettingUseCase: GetNotificationsSettingUseCase,
val setAnalyticsSettingUseCase: SetAnalyticsSettingUseCase,
getAnalyticsSettingUseCase: GetAnalyticsSettingUseCase
getAnalyticsSettingUseCase: GetAnalyticsSettingUseCase,
val setThemeUseCase: SetThemeUseCase,
getThemeUseCase: GetThemeUseCase
) : ViewModel() {

// Time Zone setting
private val preferConferenceTimeZoneResult = MutableLiveData<Result<Boolean>>()
val preferConferenceTimeZone: LiveData<Boolean>

// Notifications setting
val enableNotificationsResult = MutableLiveData<Result<Boolean>>()
private val enableNotificationsResult = MutableLiveData<Result<Boolean>>()
val enableNotifications: LiveData<Boolean>

// Analytics setting
private val sendUsageStatisticsResult = MutableLiveData<Result<Boolean>>()
val sendUsageStatistics: LiveData<Boolean>

// Theme setting
private val darkModeResult = MutableLiveData<Result<Theme>>()
val darkMode: LiveData<Boolean>

init {
getTimeZoneUseCase(Unit, preferConferenceTimeZoneResult)
preferConferenceTimeZone = preferConferenceTimeZoneResult.map {
Expand All @@ -66,6 +75,11 @@ class SettingsViewModel @Inject constructor(
enableNotifications = enableNotificationsResult.map {
(it as? Success<Boolean>)?.data ?: false
}

getThemeUseCase(Unit, darkModeResult)
darkMode = darkModeResult.map {
(it as? Success<Theme>)?.data == Theme.DARK
}
}

fun toggleTimeZone(checked: Boolean) {
Expand All @@ -79,4 +93,8 @@ class SettingsViewModel @Inject constructor(
fun toggleEnableNotifications(checked: Boolean) {
notificationsPrefSaveActionUseCase(checked, enableNotificationsResult)
}

fun toggleDarkMode(checked: Boolean) {
setThemeUseCase(if (checked) Theme.DARK else Theme.LIGHT, darkModeResult)
}
}
Expand Up @@ -20,15 +20,22 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.google.samples.apps.iosched.R
import com.google.samples.apps.iosched.shared.util.inTransaction
import com.google.samples.apps.iosched.shared.util.viewModelProvider
import com.google.samples.apps.iosched.util.updateForTheme
import dagger.android.support.DaggerAppCompatActivity
import javax.inject.Inject

/** Shell activity hosting a [MapFragment] */
class MapActivity : DaggerAppCompatActivity() {

private lateinit var fragment: MapFragment

@Inject lateinit var viewModelFactory: ViewModelProvider.Factory

companion object {
const val EXTRA_FEATURE_ID = "extra.FEATURE_ID"
const val FRAGMENT_ID = R.id.fragment_container
Expand All @@ -42,6 +49,10 @@ class MapActivity : DaggerAppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val viewModel: MapViewModel = viewModelProvider(viewModelFactory)
updateForTheme(viewModel.currentTheme)

setContentView(R.layout.activity_map)

if (savedInstanceState == null) {
Expand All @@ -57,6 +68,8 @@ class MapActivity : DaggerAppCompatActivity() {
} else {
fragment = supportFragmentManager.findFragmentById(FRAGMENT_ID) as MapFragment
}

viewModel.theme.observe(this, Observer(::updateForTheme))
}

override fun onBackPressed() {
Expand Down
Expand Up @@ -29,7 +29,7 @@ import com.google.android.gms.maps.MapView
import com.google.android.gms.maps.model.Marker
import com.google.samples.apps.iosched.databinding.FragmentMapBinding
import com.google.samples.apps.iosched.shared.analytics.AnalyticsHelper
import com.google.samples.apps.iosched.shared.util.viewModelProvider
import com.google.samples.apps.iosched.shared.util.activityViewModelProvider
import com.google.samples.apps.iosched.ui.MainNavigationFragment
import com.google.samples.apps.iosched.widget.BottomSheetBehavior
import com.google.samples.apps.iosched.widget.BottomSheetBehavior.BottomSheetCallback
Expand Down Expand Up @@ -75,7 +75,7 @@ class MapFragment : DaggerFragment(), MainNavigationFragment, OnMarkerClickListe
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel = viewModelProvider(viewModelFactory)
viewModel = activityViewModelProvider(viewModelFactory)
binding = FragmentMapBinding.inflate(inflater, container, false).apply {
setLifecycleOwner(this@MapFragment)
viewModel = this@MapFragment.viewModel
Expand Down
Expand Up @@ -36,14 +36,16 @@ import com.google.samples.apps.iosched.shared.result.Event
import com.google.samples.apps.iosched.shared.result.Result
import com.google.samples.apps.iosched.shared.result.Result.Success
import com.google.samples.apps.iosched.shared.util.map
import com.google.samples.apps.iosched.ui.ThemedActivityDelegate
import com.google.samples.apps.iosched.widget.BottomSheetBehavior
import javax.inject.Inject

class MapViewModel @Inject constructor(
loadMapTileProviderUseCase: LoadMapTileProviderUseCase,
private val loadGeoJsonFeaturesUseCase: LoadGeoJsonFeaturesUseCase,
private val analyticsHelper: AnalyticsHelper
) : ViewModel() {
private val analyticsHelper: AnalyticsHelper,
themedActivityDelegate: ThemedActivityDelegate
) : ViewModel(), ThemedActivityDelegate by themedActivityDelegate {

/**
* Area covered by the venue. Determines the viewport of the map.
Expand Down
Expand Up @@ -52,6 +52,7 @@ import com.google.samples.apps.iosched.shared.util.TimeUtils
import com.google.samples.apps.iosched.shared.util.TimeUtils.ConferenceDays
import com.google.samples.apps.iosched.shared.util.map
import com.google.samples.apps.iosched.ui.SnackbarMessage
import com.google.samples.apps.iosched.ui.ThemedActivityDelegate
import com.google.samples.apps.iosched.ui.messages.SnackbarMessageManager
import com.google.samples.apps.iosched.ui.schedule.filters.EventFilter
import com.google.samples.apps.iosched.ui.schedule.filters.EventFilter.MyEventsFilter
Expand Down Expand Up @@ -84,8 +85,10 @@ class ScheduleViewModel @Inject constructor(
observeConferenceDataUseCase: ObserveConferenceDataUseCase,
loadSelectedFiltersUseCase: LoadSelectedFiltersUseCase,
private val saveSelectedFiltersUseCase: SaveSelectedFiltersUseCase,
private val analyticsHelper: AnalyticsHelper
) : ViewModel(), ScheduleEventListener, SignInViewModelDelegate by signInViewModelDelegate {
private val analyticsHelper: AnalyticsHelper,
themedActivityDelegate: ThemedActivityDelegate
) : ViewModel(), ScheduleEventListener, SignInViewModelDelegate by signInViewModelDelegate,
ThemedActivityDelegate by themedActivityDelegate {

val isLoading: LiveData<Boolean>

Expand Down
Expand Up @@ -20,13 +20,17 @@ import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.firebase.ui.auth.IdpResponse
import com.google.samples.apps.iosched.R
import com.google.samples.apps.iosched.model.SessionId
import com.google.samples.apps.iosched.shared.util.inTransaction
import com.google.samples.apps.iosched.shared.util.viewModelProvider
import com.google.samples.apps.iosched.ui.SnackbarMessage
import com.google.samples.apps.iosched.ui.messages.SnackbarMessageManager
import com.google.samples.apps.iosched.util.signin.FirebaseAuthErrorCodeConverter
import com.google.samples.apps.iosched.util.updateForTheme
import dagger.android.support.DaggerAppCompatActivity
import timber.log.Timber
import java.util.UUID
Expand All @@ -37,8 +41,14 @@ class SessionDetailActivity : DaggerAppCompatActivity() {
@Inject
lateinit var snackbarMessageManager: SnackbarMessageManager

@Inject lateinit var viewModelFactory: ViewModelProvider.Factory

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val viewModel: SessionDetailViewModel = viewModelProvider(viewModelFactory)
updateForTheme(viewModel.currentTheme)

setContentView(R.layout.activity_session_detail)

if (savedInstanceState == null) {
Expand All @@ -47,6 +57,8 @@ class SessionDetailActivity : DaggerAppCompatActivity() {
add(R.id.session_detail_container, SessionDetailFragment.newInstance(sessionId))
}
}

viewModel.theme.observe(this, Observer(::updateForTheme))
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
Expand Down
Expand Up @@ -86,9 +86,6 @@ class SessionDetailFragment : DaggerFragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {

// TODO: Scoping the VM to the activity because of bug
// https://issuetracker.google.com/issues/74139250 (fixed in Supportlib 28.0.0-alpha1)
sessionDetailViewModel = activityViewModelProvider(viewModelFactory)

val binding = FragmentSessionDetailBinding.inflate(inflater, container, false).apply {
Expand Down

0 comments on commit c635ca7

Please sign in to comment.