From b4d35c1c12120503e74d7ae99edd94302673acf6 Mon Sep 17 00:00:00 2001 From: Guillaume Jacquart Date: Thu, 28 Jul 2022 06:04:22 +0000 Subject: #5444 Fix CPU consumption - remove flow-mvi dependency --- DEVELOPMENT.md | 184 +-------------------------------------------------------- 1 file changed, 3 insertions(+), 181 deletions(-) (limited to 'DEVELOPMENT.md') diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 75e1535..2743aac 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -29,11 +29,9 @@ In this app, we have implemented MVI using [Kotlin Flow](https://kotlinlang.org/ Elements of a feature: -1. **Actor**: It is just a function that takes current state, user action as input and produces an effect (result) as output. This function generally makes the call to external APIs and usecases. -2. **Reducer**: It is also a very simple function whose inputs are current state, effect from the actor and it returns new state. -3. **State**: Simple POJO (kotlin data class) representing various UI states of the application. -4. **Effect**: A POJO (kotlin data class) which is returned from the actor function. -5. **SingleEventProducer**: This is a function which is invoked by the reducer to publish single events (that can/should only be consumed once like displaying toast, snackbar message or sending an analytics event). This function takes action, effect, current state as input and it returns a `SingleEvent`. By default this function is null for any Feature. +1. **Action**: The exhaustive list of user actions for a feature. +2. **State**: Simple POJO (kotlin data class) representing various UI states of the application. +3. **SingleEventProducer**: This is a function which is invoked by the reducer to publish single events (that can/should only be consumed once like displaying toast, snackbar message or sending an analytics event). This function takes action, effect, current state as input and it returns a `SingleEvent`. By default this function is null for any Feature. ### Architecture Overview of PrivacyCentral App @@ -50,179 +48,6 @@ Looking at the diagram from right to left: 8. **ViewModel**: arch-component lifecycle aware viewmodel. 9. **Views**: Android high level components like activities, fragments, etc. -## How to implement a new feature -Imaging you have to implement a fake location feature. -1. Create a new package under `features` called `fakelocation` -2. Create a new feature class called `FakeLocationFeature` and make it extend the BaseFeature class as below: -```kotlin -class FakeLocationFeature( - initialState: State, - coroutineScope: CoroutineScope, - reducer: Reducer, - actor: Actor, - singleEventProducer: SingleEventProducer -) : BaseFeature( - initialState, - actor, - reducer, - coroutineScope, - { message -> Log.d("FakeLocationFeature", message) }, - singleEventProducer -) { - // Other elements goes here. -} -``` - -3. Define various elements for the feature in the above class -```kotlin -// State to be reflected in the UI -data class State(val location: Location) - -// User triggered actions -sealed class Action { - data class UpdateLocationAction(val latLng: LatLng) : Action() - object UseRealLocationAction : Action() - object UseSpecificLocationAction : Action() - data class SetFakeLocationAction(val latitude: Double, val longitude: Double) : Action() -} - -// Output from the actor after processing an action -sealed class Effect { - data class LocationUpdatedEffect(val latitude: Double, val longitude: Double) : Effect() - object RealLocationSelectedEffect : Effect() - ... - ... - data class ErrorEffect(val message: String) : Effect() -} -``` - -4. Create a static `create` function in feature which returns the feature instance: -```kotlin -companion object { - fun create( - initialState: State = - coroutineScope: CoroutineScope - ) = FakeLocationFeature( - initialState, coroutineScope, - reducer = { state, effect -> - when (effect) { - Effect.RealLocationSelectedEffect -> state.copy( - location = state.location.copy( - mode = LocationMode.REAL_LOCATION - ) - ) - is Effect.ErrorEffect, Effect.SpecificLocationSavedEffect -> state - is Effect.LocationUpdatedEffect -> state.copy( - location = state.location.copy( - latitude = effect.latitude, - longitude = effect.longitude - ) - ) - } - }, - actor = { _, action -> - when (action) { - is Action.UpdateLocationAction -> flowOf( - Effect.LocationUpdatedEffect( - action.latLng.latitude, - action.latLng.longitude - ) - ) - is Action.SetFakeLocationAction -> { - val location = Location( - LocationMode.CUSTOM_LOCATION, - action.latitude, - action.longitude - ) - // TODO: Call fake location api with specific coordinates here. - val success = DummyDataSource.setLocationMode( - LocationMode.CUSTOM_LOCATION, - location - ) - if (success) { - flowOf( - Effect.SpecificLocationSavedEffect - ) - } else { - flowOf( - Effect.ErrorEffect("Couldn't select location") - ) - } - } - Action.UseRealLocationAction -> { - // TODO: Call turn off fake location api here. - val success = DummyDataSource.setLocationMode(LocationMode.REAL_LOCATION) - if (success) { - flowOf( - Effect.RealLocationSelectedEffect - ) - } else { - flowOf( - Effect.ErrorEffect("Couldn't select location") - ) - } - } - Action.UseSpecificLocationAction -> { - flowOf(Effect.SpecificLocationSelectedEffect) - } - } - }, - singleEventProducer = { _, _, effect -> - when (effect) { - Effect.SpecificLocationSavedEffect -> SingleEvent.SpecificLocationSavedEvent - Effect.RealLocationSelectedEffect -> SingleEvent.RealLocationSelectedEvent - is Effect.ErrorEffect -> SingleEvent.ErrorEvent(effect.message) - else -> null - } - } - ) - } -``` - -5. Create a `viewmodel` like below: -```kotlin -class FakeLocationViewModel : ViewModel() { - - private val _actions = MutableSharedFlow() - val actions = _actions.asSharedFlow() - - val fakeLocationFeature: FakeLocationFeature by lazy { - FakeLocationFeature.create(coroutineScope = viewModelScope) - } - - fun submitAction(action: FakeLocationFeature.Action) { - viewModelScope.launch { - _actions.emit(action) - } - } -} -``` - -6. Create a `fragment` for your feature and make sure it implements `MVIView<>` interface -7. Initialize (or retrieve the existing) instance of viewmodel in your `fragment` class by using extension function. -```kotlin -private val viewModel: FakeLocationViewModel by viewModels() -``` - -8. In `onCreate` method of fragment, launch a coroutine to bind the view to feature and to listen single events. -```kotlin -override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - lifecycleScope.launchWhenStarted { - viewModel.fakeLocationFeature.takeView(this, this@FakeLocationFragment) - } - lifecycleScope.launchWhenStarted { - viewModel.fakeLocationFeature.singleEvents.collect { event -> - // Do something with event - } - } -} -``` - -9. To render the state in UI, override the `render` function of MVIView. -10. For publishing ui actions, use `viewModel.submitAction(action)`. - -Everything is lifecycle aware so we don't need to anything manually here. ## Code Quality and Style This project integrates a combination of unit tests, functional test and code styling tools. To run **unit** tests on your machine: @@ -240,13 +65,10 @@ To run code style check and formatting tool: The project currently doesn't have exactly the same mentioned structure as it is just a POC and will be improved. ### Todo/Improvements -- [ ] Add domain layer with usecases. -- [ ] Add data layer with repository implementation. - [ ] Add unit tests and code coverage. - [ ] Implement Hilt DI. # References 1. [Kotlin Flow](https://kotlinlang.org/docs/flow.html) 2. [MVI](https://hannesdorfmann.com/android/mosby3-mvi-1/) -3. [Redux](https://redux.js.org/) 4. [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) \ No newline at end of file -- cgit v1.2.1