Bug 1839984 - Update architecture example to use lib-state

fenix/117.0
MatthewTighe 10 months ago committed by mergify[bot]
parent a3df7acfda
commit 1062129831

@ -111,21 +111,17 @@ In some cases, it can be appropriate to initiate side-effects from the view when
-------
## Simplified Example
When reading through live code trying to understand an architecture, it can be difficult to find canonical examples, and often hard to locate the most important aspects. This is a simplified example using a hypothetical app that should help clarify the above patterns.
When reading through live code trying to understand an architecture, it can be difficult to find canonical examples, and often hard to locate the most important aspects. This is a simplified example of a basic history screen that includes a list of history items and which can be opened, multi-selected, and deleted.
![example app wireframe](./architectureexample/example-app-wireframe.png?raw=true)
The following are links to the example versions of the architectural components listed above.
This app currently has three (wonderful) features.
- Clicking on one of the colored circles will update the toolbar color
- Clicking on 'Rename', typing a new name, and selecting return will update the name of the contact
- Clicking anywhere else on a contact will navigate to a text message fragment
These link to the architectural code that accomplishes those features:
- [ContactsView](./architectureexample/ContactsView.kt)
- [ContactsStore](./architectureexample/ContactsStore.kt)
- [ContactsState](./architectureexample/ContactsStore.kt)
- [ContactsReducer](./architectureexample/ContactsStore.kt)
- [ContactsFragment](./architectureexample/ContactsFragment.kt)
- [HistoryFragment](./architectureexample/HistoryFragmentExample.kt)
- [HistoryStore](./architectureexample/HistoryStoreExample.kt)
- [HistoryState](./architectureexample/HistoryStoreExample.kt)
- [HistoryReducer](./architectureexample/HistoryStoreExample.kt)
- [HistoryNavigationMiddleware](./architectureexample/HistoryNavigationMiddlewareExample.kt)
- [HistoryStorageMiddleware](./architectureexample/HistoryStorageMiddlewareExample.kt)
- [HistoryTelemetryMiddleware](./architectureexample/HistoryTelemetryMiddlewareExample.kt)
-------

@ -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…
Cancel
Save