1
0
Fork 0

For #1994: Re-architect state handling code (#2382)

master
Colin Lee 2019-05-09 18:06:12 -05:00 committed by Jeff Boek
parent 27d8c09def
commit ccbc14a71f
29 changed files with 557 additions and 212 deletions

View File

@ -51,4 +51,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- #1429 - Updated site permissions ui for MVP
- #1599 - Fixed a crash creating a bookmark for a custom tab
- #1414 - Fixed site permissions settings getting reset in Android 6.
- #1994 - Made app state persist better when rotating the screen
### Removed

View File

@ -337,6 +337,7 @@ dependencies {
implementation Deps.androidx_navigation_ui
implementation Deps.androidx_recyclerview
implementation Deps.androidx_lifecycle_viewmodel_ktx
implementation Deps.androidx_lifecycle_viewmodel_ss
implementation Deps.androidx_core
implementation Deps.androidx_transition

View File

@ -132,6 +132,7 @@ class BrowserFragment : Fragment(), BackHandler, CoroutineScope,
toolbarComponent = ToolbarComponent(
view.browserLayout,
this,
ActionBusFactory.get(this), customTabSessionId,
(activity as HomeActivity).browsingModeManager.isPrivate,
SearchState("", getSessionById()?.searchTerms ?: "", isEditing = false),
@ -156,6 +157,7 @@ class BrowserFragment : Fragment(), BackHandler, CoroutineScope,
QuickActionComponent(
view.nestedScrollQuickAction,
this,
ActionBusFactory.get(this),
QuickActionState(
readable = getSessionById()?.readerable ?: false,

View File

@ -5,6 +5,10 @@ package org.mozilla.fenix.collections
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.Observable
import org.mozilla.fenix.home.sessioncontrol.Tab
import org.mozilla.fenix.home.sessioncontrol.TabCollection
import org.mozilla.fenix.mvi.Action
@ -12,6 +16,7 @@ import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.Reducer
import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIComponentViewModel
import org.mozilla.fenix.mvi.ViewState
sealed class SaveCollectionStep {
@ -51,34 +56,63 @@ sealed class CollectionCreationAction : Action {
class CollectionCreationComponent(
private val container: ViewGroup,
owner: Fragment,
bus: ActionBusFactory,
override var initialState: CollectionCreationState = CollectionCreationState()
) : UIComponent<CollectionCreationState, CollectionCreationAction, CollectionCreationChange>(
owner,
bus.getManagedEmitter(CollectionCreationAction::class.java),
bus.getSafeManagedObservable(CollectionCreationChange::class.java)
) {
override val reducer: Reducer<CollectionCreationState, CollectionCreationChange> =
{ state, change ->
when (change) {
is CollectionCreationChange.AddAllTabs -> state.copy(selectedTabs = state.tabs.toSet())
is CollectionCreationChange.TabListChange -> state.copy(tabs = change.tabs)
is CollectionCreationChange.TabAdded -> {
val selectedTabs = state.selectedTabs + setOf(change.tab)
state.copy(selectedTabs = selectedTabs)
}
is CollectionCreationChange.TabRemoved -> {
val selectedTabs = state.selectedTabs - setOf(change.tab)
state.copy(selectedTabs = selectedTabs)
}
is CollectionCreationChange.StepChanged -> {
state.copy(saveCollectionStep = change.saveCollectionStep)
}
}
}
override fun initView() = CollectionCreationUIView(container, actionEmitter, changesObservable)
override fun render(): Observable<CollectionCreationState> =
ViewModelProvider(owner, CollectionCreationViewModel.Factory(initialState, changesObservable)).get(
CollectionCreationViewModel::class.java
).render(uiView)
init {
render(reducer)
render()
}
}
class CollectionCreationViewModel(
initialState: CollectionCreationState,
changesObservable: Observable<CollectionCreationChange>
) :
UIComponentViewModel<CollectionCreationState, CollectionCreationAction, CollectionCreationChange>(
initialState,
changesObservable,
reducer
) {
class Factory(
private val initialState: CollectionCreationState,
private val changesObservable: Observable<CollectionCreationChange>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
CollectionCreationViewModel(initialState, changesObservable) as T
}
companion object {
val reducer: Reducer<CollectionCreationState, CollectionCreationChange> =
{ state, change ->
when (change) {
is CollectionCreationChange.AddAllTabs -> state.copy(selectedTabs = state.tabs.toSet())
is CollectionCreationChange.TabListChange -> state.copy(tabs = change.tabs)
is CollectionCreationChange.TabAdded -> {
val selectedTabs = state.selectedTabs + setOf(change.tab)
state.copy(selectedTabs = selectedTabs)
}
is CollectionCreationChange.TabRemoved -> {
val selectedTabs = state.selectedTabs - setOf(change.tab)
state.copy(selectedTabs = selectedTabs)
}
is CollectionCreationChange.StepChanged -> {
state.copy(saveCollectionStep = change.saveCollectionStep)
}
}
}
}
}

View File

@ -50,6 +50,7 @@ class CreateCollectionFragment : DialogFragment() {
collectionCreationComponent = CollectionCreationComponent(
view.create_collection_wrapper,
this,
ActionBusFactory.get(this),
CollectionCreationState(
tabs = tabs,

View File

@ -7,6 +7,11 @@ package org.mozilla.fenix.components.toolbar
import android.view.ViewGroup
import android.widget.ImageView
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
import io.reactivex.Observable
import kotlinx.android.synthetic.main.component_search.*
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.toolbar.BrowserToolbar
@ -17,10 +22,12 @@ import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.Reducer
import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIComponentViewModel
import org.mozilla.fenix.mvi.ViewState
class ToolbarComponent(
private val container: ViewGroup,
owner: Fragment,
bus: ActionBusFactory,
private val sessionId: String?,
private val isPrivate: Boolean,
@ -28,21 +35,13 @@ class ToolbarComponent(
private val engineIconView: ImageView? = null
) :
UIComponent<SearchState, SearchAction, SearchChange>(
owner,
bus.getManagedEmitter(SearchAction::class.java),
bus.getSafeManagedObservable(SearchChange::class.java)
) {
fun getView(): BrowserToolbar = uiView.toolbar
override val reducer: Reducer<SearchState, SearchChange> = { state, change ->
when (change) {
is SearchChange.ToolbarClearedFocus -> state.copy(focused = false)
is SearchChange.ToolbarRequestedFocus -> state.copy(focused = true)
is SearchChange.SearchShortcutEngineSelected ->
state.copy(engine = change.engine)
}
}
override fun initView() = ToolbarUIView(
sessionId,
isPrivate,
@ -52,8 +51,12 @@ class ToolbarComponent(
engineIconView
)
override fun render(): Observable<SearchState> =
ViewModelProviders.of(owner, ToolbarViewModel.Factory(initialState, changesObservable))
.get(ToolbarViewModel::class.java).render(uiView)
init {
render(reducer)
render()
applyTheme()
}
@ -95,3 +98,27 @@ sealed class SearchChange : Change {
object ToolbarClearedFocus : SearchChange()
data class SearchShortcutEngineSelected(val engine: SearchEngine) : SearchChange()
}
class ToolbarViewModel(initialState: SearchState, changesObservable: Observable<SearchChange>) :
UIComponentViewModel<SearchState, SearchAction, SearchChange>(initialState, changesObservable, reducer) {
class Factory(
private val initialState: SearchState,
val changesObservable: Observable<SearchChange>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
ToolbarViewModel(initialState, changesObservable) as T
}
companion object {
val reducer: Reducer<SearchState, SearchChange> = { state, change ->
when (change) {
is SearchChange.ToolbarClearedFocus -> state.copy(focused = false)
is SearchChange.ToolbarRequestedFocus -> state.copy(focused = true)
is SearchChange.SearchShortcutEngineSelected ->
state.copy(engine = change.engine)
}
}
}
}

View File

@ -45,7 +45,7 @@ class ToolbarUIView(
init {
val sessionManager = view.context.components.core.sessionManager
val session = sessionId?.let { sessionManager.findSessionById(it) }
?: sessionManager.selectedSession
?: sessionManager.selectedSession
view.apply {
elevation = resources.pxToDp(TOOLBAR_ELEVATION).toFloat()

View File

@ -4,10 +4,16 @@
package org.mozilla.fenix.exceptions
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
import io.reactivex.Observable
import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIComponentViewModel
import org.mozilla.fenix.mvi.ViewState
import org.mozilla.fenix.test.Mockable
@ -16,24 +22,24 @@ data class ExceptionsItem(val url: String)
@Mockable
class ExceptionsComponent(
private val container: ViewGroup,
owner: Fragment,
bus: ActionBusFactory,
override var initialState: ExceptionsState = ExceptionsState(emptyList())
) :
UIComponent<ExceptionsState, ExceptionsAction, ExceptionsChange>(
owner,
bus.getManagedEmitter(ExceptionsAction::class.java),
bus.getSafeManagedObservable(ExceptionsChange::class.java)
) {
override val reducer: (ExceptionsState, ExceptionsChange) -> ExceptionsState = { state, change ->
when (change) {
is ExceptionsChange.Change -> state.copy(items = change.list)
}
}
override fun render(): Observable<ExceptionsState> =
ViewModelProviders.of(owner, ExceptionsViewModel.Factory(initialState, changesObservable))
.get(ExceptionsViewModel::class.java).render(uiView)
override fun initView() = ExceptionsUIView(container, actionEmitter, changesObservable)
init {
render(reducer)
render()
}
}
@ -49,3 +55,28 @@ sealed class ExceptionsAction : Action {
sealed class ExceptionsChange : Change {
data class Change(val list: List<ExceptionsItem>) : ExceptionsChange()
}
class ExceptionsViewModel(initialState: ExceptionsState, changesObservable: Observable<ExceptionsChange>) :
UIComponentViewModel<ExceptionsState, ExceptionsAction, ExceptionsChange>(
initialState,
changesObservable,
reducer
) {
class Factory(
private val initialState: ExceptionsState,
private val changesObservable: Observable<ExceptionsChange>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
ExceptionsViewModel(initialState, changesObservable) as T
}
companion object {
val reducer: (ExceptionsState, ExceptionsChange) -> ExceptionsState = { state, change ->
when (change) {
is ExceptionsChange.Change -> state.copy(items = change.list)
}
}
}
}

View File

@ -48,6 +48,7 @@ class ExceptionsFragment : Fragment(), CoroutineScope {
val view = inflater.inflate(R.layout.fragment_exceptions, container, false)
exceptionsComponent = ExceptionsComponent(
view.exceptions_layout,
this,
ActionBusFactory.get(this),
ExceptionsState(loadAndMapExceptions())
)

View File

@ -89,6 +89,7 @@ class HomeFragment : Fragment(), CoroutineScope {
sessionControlComponent = SessionControlComponent(
view.homeLayout,
this,
bus,
SessionControlState(listOf(), listOf(), mode)
)

View File

@ -6,38 +6,42 @@ package org.mozilla.fenix.home.sessioncontrol
import android.graphics.Bitmap
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observable
import io.reactivex.Observer
import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIComponentViewModel
import org.mozilla.fenix.mvi.ViewState
class SessionControlComponent(
private val container: ViewGroup,
owner: Fragment,
bus: ActionBusFactory,
override var initialState: SessionControlState = SessionControlState(emptyList(), emptyList(), Mode.Normal)
) :
UIComponent<SessionControlState, SessionControlAction, SessionControlChange>(
owner,
bus.getManagedEmitter(SessionControlAction::class.java),
bus.getSafeManagedObservable(SessionControlChange::class.java)
) {
override val reducer: (SessionControlState, SessionControlChange) -> SessionControlState = { state, change ->
when (change) {
is SessionControlChange.CollectionsChange -> state.copy(collections = change.collections)
is SessionControlChange.TabsChange -> state.copy(tabs = change.tabs)
is SessionControlChange.ModeChange -> state.copy(mode = change.mode)
}
}
override fun initView() = SessionControlUIView(container, actionEmitter, changesObservable)
val view: RecyclerView
get() = uiView.view as RecyclerView
override fun render(): Observable<SessionControlState> =
ViewModelProviders.of(owner, SessionControlViewModel.Factory(initialState, changesObservable))
.get(SessionControlViewModel::class.java).render(uiView)
init {
render(reducer)
render()
}
}
@ -109,3 +113,30 @@ sealed class SessionControlChange : Change {
data class ModeChange(val mode: Mode) : SessionControlChange()
data class CollectionsChange(val collections: List<TabCollection>) : SessionControlChange()
}
class SessionControlViewModel(initialState: SessionControlState, changesObservable: Observable<SessionControlChange>) :
UIComponentViewModel<SessionControlState, SessionControlAction, SessionControlChange>(
initialState,
changesObservable,
reducer
) {
class Factory(
private val initialState: SessionControlState,
private val changesObservable: Observable<SessionControlChange>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
SessionControlViewModel(initialState, changesObservable) as T
}
companion object {
val reducer: (SessionControlState, SessionControlChange) -> SessionControlState = { state, change ->
when (change) {
is SessionControlChange.CollectionsChange -> state.copy(collections = change.collections)
is SessionControlChange.TabsChange -> state.copy(tabs = change.tabs)
is SessionControlChange.ModeChange -> state.copy(mode = change.mode)
}
}
}
}

View File

@ -5,12 +5,17 @@
package org.mozilla.fenix.library.bookmarks
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.Observable
import mozilla.components.concept.storage.BookmarkNode
import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.Reducer
import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIComponentViewModel
import org.mozilla.fenix.mvi.UIView
import org.mozilla.fenix.mvi.ViewState
import org.mozilla.fenix.test.Mockable
@ -18,51 +23,28 @@ import org.mozilla.fenix.test.Mockable
@Mockable
class BookmarkComponent(
private val container: ViewGroup,
owner: Fragment,
bus: ActionBusFactory,
override var initialState: BookmarkState =
BookmarkState(null, BookmarkState.Mode.Normal)
) :
UIComponent<BookmarkState, BookmarkAction, BookmarkChange>(
owner,
bus.getManagedEmitter(BookmarkAction::class.java),
bus.getSafeManagedObservable(BookmarkChange::class.java)
) {
override val reducer: Reducer<BookmarkState, BookmarkChange> = { state, change ->
when (change) {
is BookmarkChange.Change -> {
val mode =
if (state.mode is BookmarkState.Mode.Selecting) {
val items = state.mode.selectedItems.filter {
it in change.tree
}.toSet()
if (items.isEmpty()) BookmarkState.Mode.Normal else BookmarkState.Mode.Selecting(items)
} else state.mode
state.copy(tree = change.tree, mode = mode)
}
is BookmarkChange.IsSelected -> {
val selectedItems = if (state.mode is BookmarkState.Mode.Selecting) {
state.mode.selectedItems + change.newlySelectedItem
} else setOf(change.newlySelectedItem)
state.copy(mode = BookmarkState.Mode.Selecting(selectedItems))
}
is BookmarkChange.IsDeselected -> {
val selectedItems = if (state.mode is BookmarkState.Mode.Selecting) {
state.mode.selectedItems - change.newlyDeselectedItem
} else setOf()
val mode = if (selectedItems.isEmpty()) BookmarkState.Mode.Normal else BookmarkState.Mode.Selecting(
selectedItems
)
state.copy(mode = mode)
}
is BookmarkChange.ClearSelection -> state.copy(mode = BookmarkState.Mode.Normal)
}
}
override fun initView(): UIView<BookmarkState, BookmarkAction, BookmarkChange> =
BookmarkUIView(container, actionEmitter, changesObservable)
override fun render(): Observable<BookmarkState> {
return ViewModelProvider(
owner,
BookmarkViewModel.Factory(initialState, changesObservable)
).get(BookmarkViewModel::class.java).render(uiView)
}
init {
render(reducer)
render()
}
}
@ -98,3 +80,49 @@ sealed class BookmarkChange : Change {
operator fun BookmarkNode.contains(item: BookmarkNode): Boolean {
return children?.contains(item) ?: false
}
class BookmarkViewModel(initialState: BookmarkState, changesObservable: Observable<BookmarkChange>) :
UIComponentViewModel<BookmarkState, BookmarkAction, BookmarkChange>(initialState, changesObservable, reducer) {
class Factory(
private val initialState: BookmarkState,
private val changesObservable: Observable<BookmarkChange>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
BookmarkViewModel(initialState, changesObservable) as T
}
companion object {
val reducer: Reducer<BookmarkState, BookmarkChange> = { state, change ->
when (change) {
is BookmarkChange.Change -> {
val mode =
if (state.mode is BookmarkState.Mode.Selecting) {
val items = state.mode.selectedItems.filter {
it in change.tree
}.toSet()
if (items.isEmpty()) BookmarkState.Mode.Normal else BookmarkState.Mode.Selecting(items)
} else state.mode
state.copy(tree = change.tree, mode = mode)
}
is BookmarkChange.IsSelected -> {
val selectedItems = if (state.mode is BookmarkState.Mode.Selecting) {
state.mode.selectedItems + change.newlySelectedItem
} else setOf(change.newlySelectedItem)
state.copy(mode = BookmarkState.Mode.Selecting(selectedItems))
}
is BookmarkChange.IsDeselected -> {
val selectedItems = if (state.mode is BookmarkState.Mode.Selecting) {
state.mode.selectedItems - change.newlyDeselectedItem
} else setOf()
val mode = if (selectedItems.isEmpty()) BookmarkState.Mode.Normal else BookmarkState.Mode.Selecting(
selectedItems
)
state.copy(mode = mode)
}
is BookmarkChange.ClearSelection -> state.copy(mode = BookmarkState.Mode.Normal)
}
}
}
}

View File

@ -74,8 +74,8 @@ class BookmarkFragment : Fragment(), CoroutineScope, BackHandler, AccountObserve
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_bookmark, container, false)
bookmarkComponent = BookmarkComponent(view.bookmark_layout, ActionBusFactory.get(this))
signInComponent = SignInComponent(view.bookmark_layout, ActionBusFactory.get(this))
bookmarkComponent = BookmarkComponent(view.bookmark_layout, this, ActionBusFactory.get(this))
signInComponent = SignInComponent(view.bookmark_layout, this, ActionBusFactory.get(this))
return view
}

View File

@ -5,36 +5,41 @@
package org.mozilla.fenix.library.bookmarks
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.Observable
import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.Reducer
import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIComponentViewModel
import org.mozilla.fenix.mvi.UIView
import org.mozilla.fenix.mvi.ViewState
class SignInComponent(
private val container: ViewGroup,
owner: Fragment,
bus: ActionBusFactory,
override var initialState: SignInState =
SignInState(false)
) : UIComponent<SignInState, SignInAction, SignInChange>(
owner,
bus.getManagedEmitter(SignInAction::class.java),
bus.getSafeManagedObservable(SignInChange::class.java)
) {
override val reducer: Reducer<SignInState, SignInChange> = { state, change ->
when (change) {
SignInChange.SignedIn -> state.copy(signedIn = true)
SignInChange.SignedOut -> state.copy(signedIn = false)
}
}
override fun initView(): UIView<SignInState, SignInAction, SignInChange> =
SignInUIView(container, actionEmitter, changesObservable)
override fun render(): Observable<SignInState> =
ViewModelProvider(
owner,
SignInViewModel.Factory(initialState, changesObservable)
).get(SignInViewModel::class.java).render(uiView)
init {
render(reducer)
render()
}
}
@ -48,3 +53,29 @@ sealed class SignInChange : Change {
object SignedIn : SignInChange()
object SignedOut : SignInChange()
}
class SignInViewModel(initialState: SignInState, changesObservable: Observable<SignInChange>) :
UIComponentViewModel<SignInState, SignInAction, SignInChange>(
initialState, changesObservable, reducer
) {
class Factory(
private val initialState: SignInState,
private val changesObservable: Observable<SignInChange>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
SignInViewModel(initialState, changesObservable) as T
}
companion object {
val reducer = object : Reducer<SignInState, SignInChange> {
override fun invoke(state: SignInState, change: SignInChange): SignInState {
return when (change) {
SignInChange.SignedIn -> state.copy(signedIn = true)
SignInChange.SignedOut -> state.copy(signedIn = false)
}
}
}
}
}

View File

@ -68,7 +68,7 @@ class SelectBookmarkFolderFragment : Fragment(), CoroutineScope, AccountObserver
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_select_bookmark_folder, container, false)
signInComponent = SignInComponent(view.select_bookmark_layout, ActionBusFactory.get(this))
signInComponent = SignInComponent(view.select_bookmark_layout, this, ActionBusFactory.get(this))
return view
}

View File

@ -4,56 +4,42 @@
package org.mozilla.fenix.library.history
import android.view.ViewGroup
import org.mozilla.fenix.test.Mockable
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.Observable
import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIComponentViewModel
import org.mozilla.fenix.mvi.ViewState
import org.mozilla.fenix.test.Mockable
data class HistoryItem(val id: Int, val title: String, val url: String, val visitedAt: Long)
@Mockable
class HistoryComponent(
private val container: ViewGroup,
owner: Fragment,
bus: ActionBusFactory,
override var initialState: HistoryState = HistoryState(emptyList(), HistoryState.Mode.Normal)
) :
UIComponent<HistoryState, HistoryAction, HistoryChange>(
owner,
bus.getManagedEmitter(HistoryAction::class.java),
bus.getSafeManagedObservable(HistoryChange::class.java)
) {
override val reducer: (HistoryState, HistoryChange) -> HistoryState = { state, change ->
when (change) {
is HistoryChange.Change -> state.copy(mode = HistoryState.Mode.Normal, items = change.list)
is HistoryChange.EnterEditMode -> state.copy(mode = HistoryState.Mode.Editing(listOf(change.item)))
is HistoryChange.AddItemForRemoval -> {
val mode = state.mode
if (mode is HistoryState.Mode.Editing) {
val items = mode.selectedItems + listOf(change.item)
state.copy(mode = mode.copy(selectedItems = items))
} else {
state
}
}
is HistoryChange.RemoveItemForRemoval -> {
val mode = state.mode
if (mode is HistoryState.Mode.Editing) {
val items = mode.selectedItems.filter { it.id != change.item.id }
state.copy(mode = mode.copy(selectedItems = items))
} else {
state
}
}
is HistoryChange.ExitEditMode -> state.copy(mode = HistoryState.Mode.Normal)
}
}
override fun initView() = HistoryUIView(container, actionEmitter, changesObservable)
override fun render(): Observable<HistoryState> =
ViewModelProvider(
owner,
HistoryViewModel.Factory(initialState, changesObservable)
).get(HistoryViewModel::class.java).render(uiView)
init {
render(reducer)
render()
}
}
@ -85,3 +71,44 @@ sealed class HistoryChange : Change {
data class AddItemForRemoval(val item: HistoryItem) : HistoryChange()
data class RemoveItemForRemoval(val item: HistoryItem) : HistoryChange()
}
class HistoryViewModel(initialState: HistoryState, changesObservable: Observable<HistoryChange>) :
UIComponentViewModel<HistoryState, HistoryAction, HistoryChange>(initialState, changesObservable, reducer) {
class Factory(
private val initialState: HistoryState,
private val changesObservable: Observable<HistoryChange>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
HistoryViewModel(initialState, changesObservable) as T
}
companion object {
val reducer: (HistoryState, HistoryChange) -> HistoryState = { state, change ->
when (change) {
is HistoryChange.Change -> state.copy(mode = HistoryState.Mode.Normal, items = change.list)
is HistoryChange.EnterEditMode -> state.copy(mode = HistoryState.Mode.Editing(listOf(change.item)))
is HistoryChange.AddItemForRemoval -> {
val mode = state.mode
if (mode is HistoryState.Mode.Editing) {
val items = mode.selectedItems + listOf(change.item)
state.copy(mode = mode.copy(selectedItems = items))
} else {
state
}
}
is HistoryChange.RemoveItemForRemoval -> {
val mode = state.mode
if (mode is HistoryState.Mode.Editing) {
val items = mode.selectedItems.filter { it.id != change.item.id }
state.copy(mode = mode.copy(selectedItems = items))
} else {
state
}
}
is HistoryChange.ExitEditMode -> state.copy(mode = HistoryState.Mode.Normal)
}
}
}
}

View File

@ -49,7 +49,7 @@ class HistoryFragment : Fragment(), CoroutineScope, BackHandler {
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_history, container, false)
historyComponent = HistoryComponent(view.history_layout, ActionBusFactory.get(this))
historyComponent = HistoryComponent(view.history_layout, this, ActionBusFactory.get(this))
return view
}

View File

@ -5,16 +5,22 @@
package org.mozilla.fenix.quickactionsheet
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.Observable
import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.Reducer
import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIComponentViewModel
import org.mozilla.fenix.mvi.UIView
import org.mozilla.fenix.mvi.ViewState
class QuickActionComponent(
private val container: ViewGroup,
owner: Fragment,
bus: ActionBusFactory,
override var initialState: QuickActionState = QuickActionState(
readable = false,
@ -22,29 +28,21 @@ class QuickActionComponent(
readerActive = false
)
) : UIComponent<QuickActionState, QuickActionAction, QuickActionChange>(
owner,
bus.getManagedEmitter(QuickActionAction::class.java),
bus.getSafeManagedObservable(QuickActionChange::class.java)
) {
override val reducer: Reducer<QuickActionState, QuickActionChange> = { state, change ->
when (change) {
is QuickActionChange.BookmarkedStateChange -> {
state.copy(bookmarked = change.bookmarked)
}
is QuickActionChange.ReadableStateChange -> {
state.copy(readable = change.readable)
}
is QuickActionChange.ReaderActiveStateChange -> {
state.copy(readerActive = change.active)
}
}
}
override fun initView(): UIView<QuickActionState, QuickActionAction, QuickActionChange> =
QuickActionUIView(container, actionEmitter, changesObservable)
override fun render(): Observable<QuickActionState> =
ViewModelProvider(
owner,
QuickActionViewModel.Factory(initialState, changesObservable)
).get(QuickActionViewModel::class.java).render(uiView)
init {
render(reducer)
render()
}
}
@ -65,3 +63,36 @@ sealed class QuickActionChange : Change {
data class ReadableStateChange(val readable: Boolean) : QuickActionChange()
data class ReaderActiveStateChange(val active: Boolean) : QuickActionChange()
}
class QuickActionViewModel(initialState: QuickActionState, changesObservable: Observable<QuickActionChange>) :
UIComponentViewModel<QuickActionState, QuickActionAction, QuickActionChange>(
initialState,
changesObservable,
reducer
) {
class Factory(
private val initialState: QuickActionState,
private val changesObservable: Observable<QuickActionChange>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
QuickActionViewModel(initialState, changesObservable) as T
}
companion object {
val reducer: Reducer<QuickActionState, QuickActionChange> = { state, change ->
when (change) {
is QuickActionChange.BookmarkedStateChange -> {
state.copy(bookmarked = change.bookmarked)
}
is QuickActionChange.ReadableStateChange -> {
state.copy(readable = change.readable)
}
is QuickActionChange.ReaderActiveStateChange -> {
state.copy(readerActive = change.active)
}
}
}
}
}

View File

@ -67,6 +67,7 @@ class SearchFragment : Fragment(), BackHandler {
toolbarComponent = ToolbarComponent(
view.toolbar_component_wrapper,
this,
ActionBusFactory.get(this),
sessionId,
isPrivate,
@ -74,7 +75,7 @@ class SearchFragment : Fragment(), BackHandler {
view.search_engine_icon
)
awesomeBarComponent = AwesomeBarComponent(view.search_layout, ActionBusFactory.get(this))
awesomeBarComponent = AwesomeBarComponent(view.search_layout, this, ActionBusFactory.get(this))
ActionBusFactory.get(this).logMergedObservables()
return view
}

View File

@ -1,15 +1,23 @@
package org.mozilla.fenix.search.awesomebar
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import android.util.Log
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
import io.reactivex.Observable
import mozilla.components.browser.search.SearchEngine
import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.Reducer
import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIComponentViewModel
import org.mozilla.fenix.mvi.ViewState
data class AwesomeBarState(
@ -32,25 +40,51 @@ sealed class AwesomeBarChange : Change {
class AwesomeBarComponent(
private val container: ViewGroup,
owner: Fragment,
bus: ActionBusFactory,
override var initialState: AwesomeBarState = AwesomeBarState("", false)
) : UIComponent<AwesomeBarState, AwesomeBarAction, AwesomeBarChange>(
owner,
bus.getManagedEmitter(AwesomeBarAction::class.java),
bus.getSafeManagedObservable(AwesomeBarChange::class.java)
) {
override val reducer: Reducer<AwesomeBarState, AwesomeBarChange> = { state, change ->
when (change) {
is AwesomeBarChange.SearchShortcutEngineSelected ->
state.copy(suggestionEngine = change.engine, showShortcutEnginePicker = false)
is AwesomeBarChange.SearchShortcutEnginePicker ->
state.copy(showShortcutEnginePicker = change.show)
is AwesomeBarChange.UpdateQuery -> state.copy(query = change.query)
}
}
override fun initView() = AwesomeBarUIView(container, actionEmitter, changesObservable)
override fun render(): Observable<AwesomeBarState> =
ViewModelProviders.of(owner, AwesomeBarViewModel.Factory(initialState, changesObservable))
.get(AwesomeBarViewModel::class.java).render(uiView)
init {
render(reducer)
render()
}
}
class AwesomeBarViewModel(initialState: AwesomeBarState, changesObservable: Observable<AwesomeBarChange>) :
UIComponentViewModel<AwesomeBarState, AwesomeBarAction, AwesomeBarChange>(
initialState,
changesObservable,
reducer
) {
class Factory(
private val initialState: AwesomeBarState,
private val changesObservable: Observable<AwesomeBarChange>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
AwesomeBarViewModel(initialState, changesObservable) as T
}
companion object {
val reducer: Reducer<AwesomeBarState, AwesomeBarChange> = { state, change ->
Log.d("IN_REDUCER", change.toString())
when (change) {
is AwesomeBarChange.SearchShortcutEngineSelected ->
state.copy(suggestionEngine = change.engine, showShortcutEnginePicker = false)
is AwesomeBarChange.SearchShortcutEnginePicker ->
state.copy(showShortcutEnginePicker = change.show)
is AwesomeBarChange.UpdateQuery -> state.copy(query = change.query)
}
}
}
}

View File

@ -6,6 +6,10 @@ package org.mozilla.fenix.settings.quicksettings
import android.content.Context
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.Observable
import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.support.ktx.kotlin.toUri
import org.mozilla.fenix.ext.components
@ -13,6 +17,7 @@ import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIComponentViewModel
import org.mozilla.fenix.mvi.UIView
import org.mozilla.fenix.mvi.ViewState
import org.mozilla.fenix.settings.PhoneFeature
@ -22,48 +27,25 @@ import org.mozilla.fenix.utils.Settings
class QuickSettingsComponent(
private val container: ViewGroup,
owner: Fragment,
bus: ActionBusFactory,
override var initialState: QuickSettingsState
) : UIComponent<QuickSettingsState, QuickSettingsAction, QuickSettingsChange>(
owner,
bus.getManagedEmitter(QuickSettingsAction::class.java),
bus.getSafeManagedObservable(QuickSettingsChange::class.java)
) {
override val reducer: (QuickSettingsState, QuickSettingsChange) -> QuickSettingsState = { state, change ->
when (change) {
is QuickSettingsChange.Change -> {
state.copy(
mode = QuickSettingsState.Mode.Normal(
change.url,
change.isSecured,
change.isTrackingProtectionOn,
change.sitePermissions
)
)
}
is QuickSettingsChange.PermissionGranted -> {
state.copy(
mode = QuickSettingsState.Mode.ActionLabelUpdated(change.phoneFeature, change.sitePermissions)
)
}
is QuickSettingsChange.PromptRestarted -> {
state.copy(
mode = QuickSettingsState.Mode.CheckPendingFeatureBlockedByAndroid(change.sitePermissions)
)
}
is QuickSettingsChange.Stored -> {
state.copy(
mode = QuickSettingsState.Mode.ActionLabelUpdated(change.phoneFeature, change.sitePermissions)
)
}
}
}
override fun initView(): UIView<QuickSettingsState, QuickSettingsAction, QuickSettingsChange> {
return QuickSettingsUIView(container, actionEmitter, changesObservable, container)
}
override fun render(): Observable<QuickSettingsState> =
ViewModelProvider(owner, QuickSettingsViewModel.Factory(initialState, changesObservable)).get(
QuickSettingsViewModel::class.java
).render(uiView)
init {
render(reducer)
render()
}
fun toggleSitePermission(
@ -137,3 +119,52 @@ sealed class QuickSettingsChange : Change {
data class PromptRestarted(val sitePermissions: SitePermissions?) : QuickSettingsChange()
data class Stored(val phoneFeature: PhoneFeature, val sitePermissions: SitePermissions?) : QuickSettingsChange()
}
class QuickSettingsViewModel(initialState: QuickSettingsState, changesObservable: Observable<QuickSettingsChange>) :
UIComponentViewModel<QuickSettingsState, QuickSettingsAction, QuickSettingsChange>(
initialState,
changesObservable,
reducer
) {
class Factory(
private val initialState: QuickSettingsState,
private val changesObservable: Observable<QuickSettingsChange>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
QuickSettingsViewModel(initialState, changesObservable) as T
}
companion object {
val reducer: (QuickSettingsState, QuickSettingsChange) -> QuickSettingsState = { state, change ->
when (change) {
is QuickSettingsChange.Change -> {
state.copy(
mode = QuickSettingsState.Mode.Normal(
change.url,
change.isSecured,
change.isTrackingProtectionOn,
change.sitePermissions
)
)
}
is QuickSettingsChange.PermissionGranted -> {
state.copy(
mode = QuickSettingsState.Mode.ActionLabelUpdated(change.phoneFeature, change.sitePermissions)
)
}
is QuickSettingsChange.PromptRestarted -> {
state.copy(
mode = QuickSettingsState.Mode.CheckPendingFeatureBlockedByAndroid(change.sitePermissions)
)
}
is QuickSettingsChange.Stored -> {
state.copy(
mode = QuickSettingsState.Mode.ActionLabelUpdated(change.phoneFeature, change.sitePermissions)
)
}
}
}
}
}

View File

@ -73,7 +73,19 @@ class QuickSettingsSheetDialogFragment : AppCompatDialogFragment(), CoroutineSco
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflateRootView(container)
val rootView = inflateRootView(container)
quickSettingsComponent = QuickSettingsComponent(
rootView as NestedScrollView, this, ActionBusFactory.get(this),
QuickSettingsState(
QuickSettingsState.Mode.Normal(
url,
isSecured,
isTrackingProtectionOn,
sitePermissions
)
)
)
return rootView
}
private fun inflateRootView(container: ViewGroup? = null): View {
@ -115,21 +127,6 @@ class QuickSettingsSheetDialogFragment : AppCompatDialogFragment(), CoroutineSco
return this
}
override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
super.onViewCreated(rootView, savedInstanceState)
quickSettingsComponent = QuickSettingsComponent(
rootView as NestedScrollView, ActionBusFactory.get(this),
QuickSettingsState(
QuickSettingsState.Mode.Normal(
url,
isSecured,
isTrackingProtectionOn,
sitePermissions
)
)
)
}
companion object {
const val FRAGMENT_TAG = "QUICK_SETTINGS_FRAGMENT_TAG"

View File

@ -35,7 +35,7 @@ class BookmarkComponentTest {
TestBookmarkComponent(mockk(), TestUtils.bus),
recordPrivateCalls = true
)
bookmarkObserver = bookmarkComponent.internalRender(bookmarkComponent.reducer).test()
bookmarkObserver = bookmarkComponent.render().test()
emitter = TestUtils.owner.getManagedEmitter()
}
@ -87,7 +87,7 @@ class BookmarkComponentTest {
@Suppress("MemberVisibilityCanBePrivate")
class TestBookmarkComponent(container: ViewGroup, bus: ActionBusFactory) :
BookmarkComponent(container, bus) {
BookmarkComponent(container, mockk(relaxed = true), bus) {
override val uiView: UIView<BookmarkState, BookmarkAction, BookmarkChange>
get() = mockk(relaxed = true)

View File

@ -34,7 +34,7 @@ class HistoryComponentTest {
TestHistoryComponent(mockk(), bus),
recordPrivateCalls = true
)
historyObserver = historyComponent.internalRender(historyComponent.reducer).test()
historyObserver = historyComponent.render().test()
emitter = owner.getManagedEmitter()
}
@ -82,7 +82,7 @@ class HistoryComponentTest {
@Suppress("MemberVisibilityCanBePrivate")
class TestHistoryComponent(container: ViewGroup, bus: ActionBusFactory) :
HistoryComponent(container, bus) {
HistoryComponent(container, mockk(relaxed = true), bus) {
override val uiView: UIView<HistoryState, HistoryAction, HistoryChange>
get() = mockk(relaxed = true)

View File

@ -32,8 +32,8 @@ dependencies {
implementation Deps.kotlin_stdlib
implementation Deps.androidx_annotation
implementation Deps.androidx_lifecycle_runtime
implementation Deps.androidx_lifecycle_extensions
implementation Deps.androidx_lifecycle_viewmodel_ss
implementation Deps.mozilla_support_base
implementation Deps.rxAndroid

View File

@ -90,6 +90,7 @@ class ActionBusFactory private constructor(val owner: LifecycleOwner) {
* @param clazz is the Event Class
* @param event is the instance of the Event to be sent
*/
@Suppress("UNCHECKED_CAST")
fun <T : Action> emit(clazz: Class<T>, event: T) {
val subject = if (map[clazz] != null) map[clazz] else create(clazz)
(subject as Subject<T>).onNext(event)
@ -102,6 +103,7 @@ class ActionBusFactory private constructor(val owner: LifecycleOwner) {
*
* @param clazz is the class of the event type used by this observable
*/
@Suppress("UNCHECKED_CAST")
fun <T : Action> getSafeManagedObservable(clazz: Class<T>): Observable<T> {
return if (map[clazz] != null) map[clazz] as Observable<T> else create(clazz)
}
@ -111,6 +113,7 @@ class ActionBusFactory private constructor(val owner: LifecycleOwner) {
.`as`(autoDisposable(AndroidLifecycleScopeProvider.from(owner)))
}
@Suppress("UNCHECKED_CAST")
fun <T : Action> getManagedEmitter(clazz: Class<T>): Observer<T> {
return if (map[clazz] != null) map[clazz] as Observer<T> else create(clazz)
}
@ -132,7 +135,7 @@ class ActionBusFactory private constructor(val owner: LifecycleOwner) {
* Extension on [LifecycleOwner] used to emit an event.
*/
inline fun <reified T : Action> LifecycleOwner.emit(event: T) =
kotlin.with(ActionBusFactory.get(this)) {
with(ActionBusFactory.get(this)) {
getSafeManagedObservable(T::class.java)
emit(T::class.java, event)
}
@ -152,10 +155,10 @@ inline fun <reified T : Action> LifecycleOwner.getManagedEmitter(): Observer<T>
/**
* This method returns a destroy observable that can be passed to [org.mozilla.fenix.mvi.UIView]s as needed.
*/
inline fun LifecycleOwner?.createDestroyObservable(): Observable<Unit> {
fun LifecycleOwner?.createDestroyObservable(): Observable<Unit> {
return Observable.create { emitter ->
if (this == null || this.lifecycle.currentState == Lifecycle.State.DESTROYED) {
emitter.onNext(kotlin.Unit)
emitter.onNext(Unit)
emitter.onComplete()
return@create
}
@ -163,7 +166,7 @@ inline fun LifecycleOwner?.createDestroyObservable(): Observable<Unit> {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun emitDestroy() {
if (emitter.isDisposed) {
emitter.onNext(kotlin.Unit)
emitter.onNext(Unit)
emitter.onComplete()
}
}

View File

@ -4,6 +4,8 @@
package org.mozilla.fenix.mvi
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import io.reactivex.Observable
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
@ -11,28 +13,50 @@ import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
abstract class UIComponent<S : ViewState, A : Action, C : Change>(
protected val owner: Fragment,
protected val actionEmitter: Observer<A>,
protected val changesObservable: Observable<C>
) {
abstract var initialState: S
abstract val reducer: Reducer<S, C>
open val uiView: UIView<S, A, C> by lazy { initView() }
abstract fun initView(): UIView<S, A, C>
open fun getContainerId() = uiView.containerId
abstract fun render(): Observable<S>
}
open class UIComponentViewModel<S : ViewState, A : Action, C : Change>(
private val initialState: S,
val changesObservable: Observable<C>,
reducer: Reducer<S, C>
) : ViewModel() {
private val statesObservable: Observable<S> = internalRender(reducer)
private var statesDisposable: Disposable? = null
/**
* Render the ViewState to the View through the Reducer
*/
fun render(reducer: Reducer<S, C>): Disposable =
internalRender(reducer)
fun render(uiView: UIView<S, A, C>): Observable<S> {
statesDisposable = statesObservable
.subscribe(uiView.updateView())
return statesObservable
}
fun internalRender(reducer: Reducer<S, C>): Observable<S> =
@Suppress("MemberVisibilityCanBePrivate")
protected fun internalRender(reducer: Reducer<S, C>): Observable<S> =
changesObservable
.scan(initialState, reducer)
.distinctUntilChanged()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.replay(1)
.autoConnect(0)
override fun onCleared() {
super.onCleared()
statesDisposable?.dispose()
}
}

View File

@ -36,12 +36,16 @@ abstract class UIView<S : ViewState, A : Action, C : Change>(
/**
* Show the UIView
*/
open fun show() { view.visibility = View.VISIBLE }
open fun show() {
view.visibility = View.VISIBLE
}
/**
* Hide the UIView
*/
open fun hide() { view.visibility = View.GONE }
open fun hide() {
view.visibility = View.GONE
}
/**
* Update the view from the ViewState

View File

@ -23,6 +23,7 @@ private object Versions {
const val androidx_fragment = "1.1.0-alpha08"
const val androidx_navigation = "2.1.0-alpha03"
const val androidx_recyclerview = "1.1.0-alpha05"
const val androidx_lifecycle_savedstate = "1.0.0-alpha01"
const val androidx_testing = "1.1.0-alpha08"
const val androidx_core = "1.2.0-alpha01"
const val androidx_transition = "1.1.0-rc01"
@ -134,8 +135,10 @@ object Deps {
const val androidx_appcompat = "androidx.appcompat:appcompat:${Versions.androidx_appcompat}"
const val androidx_constraintlayout = "androidx.constraintlayout:constraintlayout:${Versions.androidx_constraint_layout}"
const val androidx_legacy = "androidx.legacy:legacy-support-v4:${Versions.androidx_legacy}"
const val androidx_lifecycle_extensions = "androidx.lifecycle:lifecycle-extensions:${Versions.androidx_lifecycle}"
const val androidx_lifecycle_viewmodel_ktx = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.androidx_lifecycle}"
const val androidx_lifecycle_runtime = "androidx.lifecycle:lifecycle-runtime:${Versions.androidx_lifecycle}"
const val androidx_lifecycle_viewmodel_ss = "androidx.lifecycle:lifecycle-viewmodel-savedstate:${Versions.androidx_lifecycle_savedstate}"
const val androidx_preference = "androidx.preference:preference-ktx:${Versions.androidx_preference}"
const val androidx_safeargs = "androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.androidx_navigation}"
const val androidx_navigation_fragment = "androidx.navigation:navigation-fragment:${Versions.androidx_navigation}"