Bug 1839984 - Update architecture example to use lib-state
parent
a3df7acfda
commit
1062129831
@ -1,24 +0,0 @@
|
|||||||
/* 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/. */
|
|
||||||
|
|
||||||
// This is example code for the 'Simplified Example' section of
|
|
||||||
// /docs/architecture-overview.md
|
|
||||||
class ContactsController(
|
|
||||||
private val store: ContactsStore,
|
|
||||||
private val navController: NavController,
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun contactRenamed(contactId: Int, newName: String) {
|
|
||||||
store.dispatch(ContactsAction.ContactRenamed(contactId = contactId, newName = newName))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun chatSelected(contactId: Int) {
|
|
||||||
// This is how we pass arguments between fragments using Google's navigation library.
|
|
||||||
// See https://developer.android.com/guide/navigation/navigation-getting-started
|
|
||||||
val directions = ContactsFragment.actionContactsFragmentToChatFragment(
|
|
||||||
contactId = contactId,
|
|
||||||
)
|
|
||||||
navController.nav(R.id.contactFragment, directions)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
/* 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/. */
|
|
||||||
|
|
||||||
|
|
||||||
// This is example code for the 'Simplified Example' section of
|
|
||||||
// /docs/architecture-overview.md
|
|
||||||
class ContactsFragment : Fragment() {
|
|
||||||
|
|
||||||
lateinit var contactsStore: ContactsStore
|
|
||||||
lateinit var contactsView: ContactsView
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
||||||
val view = inflater.inflate(R.layout.fragment_contacts, container, false)
|
|
||||||
|
|
||||||
// Create the various components and hook them up to each other
|
|
||||||
val initialState = ContactsState(
|
|
||||||
contacts = emptyList(),
|
|
||||||
theme = Theme.ORANGE
|
|
||||||
)
|
|
||||||
|
|
||||||
contactsStore = ContactsStore(initialState = initialState)
|
|
||||||
|
|
||||||
val contactsController = ContactsController(
|
|
||||||
store = store,
|
|
||||||
navController = findNavController()
|
|
||||||
)
|
|
||||||
|
|
||||||
val themeController = ThemeController(
|
|
||||||
store = store
|
|
||||||
)
|
|
||||||
|
|
||||||
val interactor = ContactsInteractor(
|
|
||||||
contactsController = contactsController,
|
|
||||||
themeController = themeController
|
|
||||||
)
|
|
||||||
|
|
||||||
contactsView = ContactsView(view.contains_container, interactor)
|
|
||||||
}
|
|
||||||
|
|
||||||
override onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
// Whenever State is updated, pass it to the View
|
|
||||||
consumeFrom(contactsStore) { state ->
|
|
||||||
contactsView.update(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
/* 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/. */
|
|
||||||
|
|
||||||
// This is example code for the 'Simplified Example' section of
|
|
||||||
// /docs/architecture-overview.md
|
|
||||||
class ContactsInteractor(
|
|
||||||
private val contactsController: ContactsController,
|
|
||||||
private val themeController: ThemeController,
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun onThemeSelected(theme: Theme) {
|
|
||||||
themeController.themeSelected(theme)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onContactRenamed(contactId: Int, newName: String) {
|
|
||||||
contactsController.contactRenamed(contactId, newName)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onChatSelected(contactId: Int) {
|
|
||||||
contactsController.chatSelected(contactId)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
/* 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/. */
|
|
||||||
|
|
||||||
// This is example code for the 'Simplified Example' section of
|
|
||||||
// /docs/architecture-overview.md
|
|
||||||
class ContactsStore(
|
|
||||||
private val initialState: ContactsState,
|
|
||||||
) : Store<ContactsState, Reducer<ContactState, ContactsAction>>(initialState, ::reducer)
|
|
||||||
|
|
||||||
sealed class ContactsAction {
|
|
||||||
data class ContactRenamed(val contactId: Int, val newName: String) : ContactsAction
|
|
||||||
data class ThemeChanged(val newTheme: Theme) : ContactsAction
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ContactsState(
|
|
||||||
val contacts: List<Contact>,
|
|
||||||
val theme: Theme,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Contact(
|
|
||||||
val name: String,
|
|
||||||
val id: Int,
|
|
||||||
val imageUrl: Uri,
|
|
||||||
)
|
|
||||||
|
|
||||||
enum class Theme {
|
|
||||||
ORANGE, DARK
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reducer(oldState: ContactsState, action: ContactsAction): ContactsState = when (action) {
|
|
||||||
is ContactsAction.ThemeChanged -> oldState.copy(theme = action.newTheme)
|
|
||||||
is ContactsAction.ContactRenamed -> {
|
|
||||||
val newContacts = oldState.contacts.map { contact ->
|
|
||||||
// If this is the contact we want to change...
|
|
||||||
if (contact.id == action.contactId) {
|
|
||||||
// Update its name, but keep other values the same
|
|
||||||
contact.copy(name = newName)
|
|
||||||
} else {
|
|
||||||
// Otherwise return the original contact
|
|
||||||
return@map contact
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return oldState.copy(contacts = newContacts)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
/* 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/. */
|
|
||||||
|
|
||||||
// This is example code for the 'Simplified Example' section of
|
|
||||||
// /docs/architecture-overview.md
|
|
||||||
class ContactsView(
|
|
||||||
private val container: ViewGroup,
|
|
||||||
private val interactor: ContactsInteractor,
|
|
||||||
) {
|
|
||||||
|
|
||||||
val view: View = LayoutInflater.from(container.context)
|
|
||||||
.inflate(R.layout.contact_list, container, true)
|
|
||||||
|
|
||||||
private val contactAdapter: ContactAdapter
|
|
||||||
|
|
||||||
init {
|
|
||||||
// Setup view constraints and anything else that will not change as data updates
|
|
||||||
view.select_theme_orange.setOnClickListener {
|
|
||||||
interactor.onThemeSelected(Theme.ORANGE)
|
|
||||||
}
|
|
||||||
view.select_theme_dark.setOnClickListner {
|
|
||||||
interactor.onThemeSelected(Theme.DARK)
|
|
||||||
}
|
|
||||||
// The RecyclerView.Adapter is passed the interactor, and will call it from its own listeners
|
|
||||||
contactAdapter = ContactAdapter(view.contactRoot, interactor)
|
|
||||||
view.contact_recycler.apply {
|
|
||||||
adapter = contactAdapter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun update(state: ContactsState) {
|
|
||||||
view.toolbar.setColor(ContextCompat.getColor(this, R.color.state.toolbarColor))
|
|
||||||
contactAdapter.update(state)
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,60 @@
|
|||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
// This is example code for the 'Simplified Example' section of
|
||||||
|
// /docs/architecture-overview.md
|
||||||
|
class HistoryFragment : Fragment() {
|
||||||
|
|
||||||
|
private val store by lazy {
|
||||||
|
StoreProvider.get(this) {
|
||||||
|
HistoryStore(
|
||||||
|
initialState = HistoryState.initial,
|
||||||
|
middleware = listOf(
|
||||||
|
HistoryNavigationMiddleware(findNavController())
|
||||||
|
HistoryStorageMiddleware(HistoryStorage()),
|
||||||
|
HistoryTelemetryMiddleware(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
return ComposeView(requireContext()).apply {
|
||||||
|
setContent {
|
||||||
|
HistoryScreen(store)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HistoryScreen(store: HistoryStore) {
|
||||||
|
val state = store.observeAsState(initialValue = HistoryState.initial) { state -> state }
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
LazyColumn(listState) {
|
||||||
|
if (state.selectedItems.isNotEmpty()) {
|
||||||
|
HistoryMultiSelectHeader(
|
||||||
|
onDeleteSelectedClick = {
|
||||||
|
store.dispatch(HistoryAction.DeleteItems(state.selectedItems))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
HistoryHeader(
|
||||||
|
onDeleteAllClick = { store.dispatch(HistoryAction.DeleteItems(state.items)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(items = state.displayItems, key = { item -> item.id } ) { item ->
|
||||||
|
val isSelected = state.selectedItems.find { selectedItem ->
|
||||||
|
selectdItem == item
|
||||||
|
}
|
||||||
|
HistoryItem(
|
||||||
|
item = item,
|
||||||
|
isSelected = isSelected,
|
||||||
|
onClick = { store.dispatch(HistoryAction.OpenItem(item)) },
|
||||||
|
onLongClick = { store.dispatch(HistoryAction.ToggleItemSelection(item)) },
|
||||||
|
onDeleteClick = { store.dispatch(HistoryAction.DeleteItems(listOf(item))) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
// This is example code for the 'Simplified Example' section of
|
||||||
|
// /docs/architecture-overview.md
|
||||||
|
class HistoryNavigationMiddleware(
|
||||||
|
private val navController: NavController,
|
||||||
|
) : Middleware<HistoryState, HistoryAction> {
|
||||||
|
override fun invoke(
|
||||||
|
context: MiddlewareContext<HistoryState, HistoryAction>,
|
||||||
|
next: (HistoryAction) -> Unit,
|
||||||
|
action: HistoryAction,
|
||||||
|
) {
|
||||||
|
// This middleware won't need to manipulate the action, so the action can be passed through
|
||||||
|
// the middleware chain before the side-effects are initiated
|
||||||
|
next(action)
|
||||||
|
when(action) {
|
||||||
|
is HistoryAction.OpenItem -> {
|
||||||
|
navController.openToBrowserAndLoad(
|
||||||
|
searchTermOrURL = item.url,
|
||||||
|
newTab = true,
|
||||||
|
from = BrowserDirection.FromHistory,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
// This is example code for the 'Simplified Example' section of
|
||||||
|
// /docs/architecture-overview.md
|
||||||
|
class HistoryStorageMiddleware(
|
||||||
|
private val storage: HistoryStorage
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
) : Middleware<HistoryState, HistoryAction> {
|
||||||
|
override fun invoke(
|
||||||
|
context: MiddlewareContext<HistoryState, HistoryAction>,
|
||||||
|
next: (HistoryAction) -> Unit,
|
||||||
|
action: HistoryAction,
|
||||||
|
) {
|
||||||
|
// This middleware won't need to manipulate the action, so the action can be passed through
|
||||||
|
// the middleware chain before the side-effects are initiated
|
||||||
|
next(action)
|
||||||
|
when(action) {
|
||||||
|
is HistoryAction.Init -> {
|
||||||
|
scope.launch {
|
||||||
|
val history = storage.load()
|
||||||
|
context.store.dispatch(HistoryAction.ItemsChanged(history))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is HistoryAction.DeleteItems -> {
|
||||||
|
scope.launch {
|
||||||
|
val currentItems = context.state.items
|
||||||
|
if (storage.delete(action.items) is HistoryStorage.Success) {
|
||||||
|
context.store.dispatch(
|
||||||
|
HistoryAction.DeleteFinished()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
// This is example code for the 'Simplified Example' section of
|
||||||
|
// /docs/architecture-overview.md
|
||||||
|
class HistoryStore(
|
||||||
|
private val initialState: HistoryState,
|
||||||
|
private val middleware: List<Middleware<HistoryState, HistoryAction>>
|
||||||
|
) : Store<HistoryState, Reducer<HistoryState, HistoryAction>>(initialState, middleware, ::reducer) {
|
||||||
|
init {
|
||||||
|
// This will ensure that middlewares can take any actions they need to during initialization
|
||||||
|
dispatch(HistoryAction.Init)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class HistoryAction {
|
||||||
|
object Init : HistoryAction()
|
||||||
|
data class ItemsChanged(val items: List<History>) : HistoryAction()
|
||||||
|
data class DeleteItems(val items: List<History>) : HistoryAction()
|
||||||
|
data class DeleteFinished() : HistoryAction()
|
||||||
|
data class ToggleItemSelection(val item: History) : HistoryAction()
|
||||||
|
data class OpenItem(val item: History) : HistoryAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class HistoryState(
|
||||||
|
val items: List<History>,
|
||||||
|
val selectedItems: List<History>,
|
||||||
|
val itemsBeingDeleted: List<History>,
|
||||||
|
companion object {
|
||||||
|
val initial = HistoryState(
|
||||||
|
items = listOf(),
|
||||||
|
selectedItems = listOf(),
|
||||||
|
itemsBeingDeleted = listOf(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
val displayItems = items.filter { item ->
|
||||||
|
item !in itemsBeingDeleted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reducer(oldState: HistoryState, action: HistoryAction): HistoryState = when (action) {
|
||||||
|
is HistoryAction.ItemsChanged -> oldState.copy(items = action.items)
|
||||||
|
is HistoryAction.DeleteItems -> oldState.copy(itemsBeingDeleted = action.items)
|
||||||
|
is HistoryAction.DeleteFinished -> oldState.copy(
|
||||||
|
items = oldState.items - oldState.itemsBeingDeleted,
|
||||||
|
itemsBeingDeleted = listOf(),
|
||||||
|
)
|
||||||
|
is HistoryAction.ToggleItemSelection -> {
|
||||||
|
if (oldState.selectedItems.contains(action.item)) {
|
||||||
|
oldState.copy(selectedItems = oldState.selectedItems - action.item)
|
||||||
|
} else {
|
||||||
|
oldState.copy(selectedItems = oldState.selectedItems + action.item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
data class History(
|
||||||
|
val id: Int,
|
||||||
|
val title: String,
|
||||||
|
val url: Uri,
|
||||||
|
)
|
@ -0,0 +1,22 @@
|
|||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
// This is example code for the 'Simplified Example' section of
|
||||||
|
// /docs/architecture-overview.md
|
||||||
|
class HistoryTelemetryMiddleware : Middleware<HistoryState, HistoryAction> {
|
||||||
|
override fun invoke(
|
||||||
|
context: MiddlewareContext<HistoryState, HistoryAction>,
|
||||||
|
next: (HistoryAction) -> Unit,
|
||||||
|
action: HistoryAction,
|
||||||
|
) {
|
||||||
|
// This middleware won't need to manipulate the action, so the action can be passed through
|
||||||
|
// the middleware chain before the side-effects are initiated
|
||||||
|
next(action)
|
||||||
|
when(action) {
|
||||||
|
is HistoryAction.DeleteItems -> History.itemsDeleted.record()
|
||||||
|
is HistoryAction.OpenItem -> History.itemOpened.record()
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +0,0 @@
|
|||||||
/* 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/. */
|
|
||||||
|
|
||||||
|
|
||||||
// This is example code for the 'Simplified Example' section of
|
|
||||||
// /docs/architecture-overview.md
|
|
||||||
class ThemeController(
|
|
||||||
private val ContactsStore
|
|
||||||
) {
|
|
||||||
fun themeSelected(newTheme: Theme) {
|
|
||||||
store.dispatch(ContactsAction.ThemeChanged(newTheme = newTheme))
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 20 KiB |
Loading…
Reference in New Issue