Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SpeziAccount & SpeziFirebase #107

Draft
wants to merge 73 commits into
base: main
Choose a base branch
from
Draft

Conversation

pauljohanneskraft
Copy link
Contributor

@pauljohanneskraft pauljohanneskraft commented Sep 20, 2024

SpeziAccount & SpeziFirebase

♻️ Current situation & Problem

This PR will add SpeziAccount functionality to Spezi. As long as it is still a draft PR, we may assume it not to be "finished" yet.

⚙️ Release Notes

Add a bullet point list summary of the feature and possible migration guides if this is a breaking change so this section can be added to the release notes.
Include code snippets that provide examples of the feature implemented or links to the documentation if it appends or changes the public interface.

📚 Documentation

Please ensure that you properly document any additions in conformance to Spezi Documentation Guide.
You can use this section to describe your solution, but we encourage contributors to document your reasoning and changes using in-line documentation.

✅ Testing

Please ensure that the PR meets the testing requirements set by CodeCov and that new functionality is appropriately tested.
This section describes important information about the tests and why some elements might not be testable.

📝 Code of Conduct & Contributing Guidelines

By submitting creating this pull request, you agree to follow our Code of Conduct and Contributing Guidelines:

Copy link

codecov bot commented Sep 20, 2024

Codecov Report

Attention: Patch coverage is 0% with 76 lines in your changes missing coverage. Please review.

Project coverage is 39.03%. Comparing base (09d65ba) to head (be77221).
Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...i/module/account/account/ExternalAccountStorage.kt 0.00% 28 Missing ⚠️
...ezi/module/account/account/AccountDetails+Flags.kt 0.00% 12 Missing ⚠️
...spezi/module/account/account/AccountKeyCategory.kt 0.00% 6 Missing ⚠️
.../module/account/account/AccountKeyConfiguration.kt 0.00% 4 Missing ⚠️
.../spezi/module/account/account/AccountDetails+Id.kt 0.00% 3 Missing ⚠️
...ezi/module/account/account/AccountModifications.kt 0.00% 3 Missing ⚠️
...i/module/account/account/AccountStorageProvider.kt 0.00% 3 Missing ⚠️
...d/spezi/module/account/account/FollowUpBehavior.kt 0.00% 3 Missing ⚠️
...ord/spezi/module/account/account/AccountDetails.kt 0.00% 2 Missing ⚠️
...nford/spezi/module/account/account/InitialValue.kt 0.00% 2 Missing ⚠️
... and 9 more
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##               main     #107      +/-   ##
============================================
+ Coverage     38.64%   39.03%   +0.40%     
- Complexity      656      676      +20     
============================================
  Files           223      240      +17     
  Lines          8034     8156     +122     
  Branches       1140     1155      +15     
============================================
+ Hits           3104     3183      +79     
- Misses         4638     4676      +38     
- Partials        292      297       +5     
Flag Coverage Δ
uitests 30.37% <ø> (-0.16%) ⬇️
unittests 32.69% <0.00%> (+0.50%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...zi/module/account/account/AccountKeyRequirement.kt 0.00% <0.00%> (ø)
...ord/spezi/module/account/account/AccountService.kt 0.00% <0.00%> (ø)
...ule/account/account/AccountServiceConfiguration.kt 0.00% <0.00%> (ø)
...ount/account/AccountServiceConfigurationStorage.kt 0.00% <0.00%> (ø)
...dule/account/foundation/SendableValueRepository.kt 0.00% <0.00%> (ø)
...anford/spezi/module/account/spezi/Configuration.kt 0.00% <0.00%> (ø)
.../edu/stanford/spezi/module/account/spezi/Module.kt 0.00% <0.00%> (ø)
...du/stanford/spezi/module/account/spezi/Standard.kt 0.00% <0.00%> (ø)
...ord/spezi/module/account/account/AccountDetails.kt 0.00% <0.00%> (ø)
...nford/spezi/module/account/account/InitialValue.kt 0.00% <0.00%> (ø)
... and 9 more

... and 15 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 09d65ba...be77221. Read the comment docs.

@eldcn eldcn self-assigned this Oct 20, 2024
Copy link
Contributor

@eldcn eldcn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't manage to review all the changes today as I checked out the iOS implementation as well - (I know that the current state is a draft only). Some general notes so far, which we can then discuss next:

  1. I notice a tendency to replicate exactly the iOS constructs. Here I would highlight two perspectives: 1. Developers of Spezi frameworks and 2. Users (developers) of Spezi Framework. Both actors are mainly used to develop natively in the respective platform, following the guidelines of the respective platforms - hence trying to replicate constructs of one platform into the other, might probably cause some confusion in regard to both maintaining the frameworks and also including and using the frameworks into their apps.

  2. iOS is built keeping SwiftUI in mind and makes strongly use of Environment and property wrappers. While the equivalent construct in Compose is CompositionLocal I want to share some thoughts why it might not be the appropriate way to implement some of the features. Both components belong to the UI layer, and should ideally provide ui related data, such as theming for instance. By design, ui layer in Android is very fragile in terms of preserving state. The view / composables can get recreated multiple times during the lifecycle of the screen, e.g. during device configuration changes (dark / light mode for instance), hence it is not advisable to preserve any kind of state in UI layer. The appropriate handling of in Android should happen in ViewModels - those components preserve state even in case of view / composable recreations. It is not a nice architectural pattern to choose, but a MUST, especially when dealing with complex screens. Views should be treated and kept as dumb as possible, meaning that they should ideally just receive a reactive ui state injected and render themselves based on the state (e.g. compose state). Their job is also to notify user interactions (such as click actions). You can notice this in iOS as well from the strong usage of @MainActor. The main logic should then happen in the view model, which can process those and emit a new state again. I see this approach also applicable in SpeziAccount in kotlin.

  3. Before going into implementation, I would suggest us to start with the public API of SpeziAccount in iOS - which is what the users of Spezi will then consume in Android, a good start for that can simply be the documentation. I would then continue with identifying what is really needed to be public, e.g. service, configurations, models, views / composables. Next, we should identify what SpeziAccount needs to be provided from the users (e.g. configurations or extensions). Then we should proceed with the actual building of components. For composables, I see the concept of Slots API in compose which enables customization of the views that we will provide. In general, it is only the public components that we should align to provide similarly as in iOS (naming, configuration options, similar constructs), for internal constructs we shouldn't restrict ourselves to replicate iOS concepts, e.g using Self.Type for map keys - these are implementation details to achieve what the requirements of the public API


private val standard: Standard = TODO()
private val storage: ExternalAccountStorage = TODO()
private val collectors = mutableMapOf<UUID, FlowCollector<Event>>()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: we can use a concurrent hash map and can avoid the need for the mutex here

private val collectors = ConcurrentHashMap<UUID, FlowCollector<Event>>()

private val collectors = mutableMapOf<UUID, FlowCollector<Event>>()
private val mutex = Mutex()

val events: Flow<Event> get() = newSubscription()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to keep a reference of the collectors in the map above? Currently we are manually setting entries there in newSubscription() and from the API I don't see a need to assign collectors from outside. This is handled by default from kotlin flow and coroutines api when consumers will start collecting the public events property . You can create a single private mutable flow here and expose via this public events. Also there is no need to handle onCompletion as you are doing below. Consumers of these events will launch their own coroutine scope to listen for changes, e.g. a viewModelScope. When the view model will get destroyed, the scope will get cancelled automatically -> and also will stop listening to these changes. By design flow apis should honor consumers request to listen to the changes, and we should not manage this internally via the map. Responsibility of this component should be to notify the events, and not manage it's listeners.

TLDR: I know this ways done to match the internal implementation in iOS, but the equivalent in android can be achieved as follows:

private val _events = MutableSharedFlow<Event>()
val events: Flow<Event> = _events.asSharedFlow()

// emitting e.g. in respond to event
_events.emit(event) // this will trigger a new update (emission) in public events flow

This approach is also safer in regard to wrong usages from clients, because they might access the events property multiple times from their components, which would result into creating new instances every time - while with the proposal we are returning the same instance (single source of truth) on every access

The only difference is that the shared flow is hot - technical meaning is that it will emit always even though it might not have any active subscribers / collectors. However, flow api is pretty efficient and we can really ignore the fact the we might emit even though no one is listening

import kotlinx.coroutines.sync.withLock
import java.util.UUID

class AccountNotifications {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to align on the following pattern: Every public component will be defined as an interface, e.g. interface AccountNotifications, and the actual implementation as an internal class AccountNotificationsImpl : AccountNotifications. This approach will give the consumers of the library to use test doubles of their choice for testing, e.g. mocking or fakes.


Also, I would rethink the public API of this component, it should indeed not offer the respond to event method publicly as it allows mutation from outside, while we only want to mutate internally in Account class

data class DisassociatingAccount(val details: AccountDetails): Event
}

private val standard: Standard = TODO()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we will define our own type right? as this is coming from android.text.style.TabStopSpan.Standard

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is just a placeholder for now

Base automatically changed from feature/spezi-views to main November 24, 2024 13:28
@eldcn eldcn changed the base branch from main to feature/spezi-bluetooth November 24, 2024 17:17
@eldcn eldcn changed the base branch from feature/spezi-bluetooth to main November 24, 2024 17:17
@pauljohanneskraft pauljohanneskraft changed the title SpeziAccount SpeziAccount & SpeziFirebase Nov 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants