Giter Club home page Giter Club logo

android-architecture's Introduction

« Quand on a raison, on n'écrit pas quarante pages ! »

android-architecture's People

Contributors

andreilisun avatar dpott197 avatar efung avatar erikhellman avatar florina-muntenescu avatar goodev avatar grepx avatar h3r3x3 avatar jknair0 avatar josealcerreca avatar kioba avatar malmstein avatar murdly avatar oldergod avatar qingmei2 avatar quangson91 avatar rainer-lang avatar samiuelson avatar sfuku7 avatar steffandroid avatar thagikura avatar tomaszrykala avatar vlazzle avatar yougin avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

android-architecture's Issues

Crash on add TODO item

Easily reproduced:

  • launch app
  • add TODO item with title and description
  • clicking OK button will trigger crash
com.example.android.architecture.blueprints.todomvp.mock E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.android.architecture.blueprints.todomvp.mock, PID: 18516
    io.reactivex.exceptions.OnErrorNotImplementedException: no such table: tasks (code 1): , while compiling: INSERT OR REPLACE  INTO tasks(completed,description,title,entryid) VALUES (?,?,?,?)

How to unsubscribe a boundaryCallback?

When I put some same fragments in the viewpager, the onZeroItemsLoaded called multiple times, and the xxxtActionProcessorHolder is cause memory leak because it is referenced by boundaryCallback object.
Is there a way to unsubscribe a boundaryCallback?

.scan(initialIntentFilter)

I really like this architecture! and I'm curious about 2 things, I'm not an expert on RxJava so take these with a grain of salt:

All related to TaskViewModel.class:

1 - What's the reason for using two PublishSubjects instead of one PublishSubject and one BehaviourSubject that would automatically trigger the last TaskViewState to its subscribers?
Alternatively I believe the same behaviour can be achieved using one PublishSubject + replay(1).autoConnect() or Jake Warthon's library https://github.com/JakeWharton/RxReplayingShare

2 - On line 53 you do compose().subscribe(this.mStatesSubject);. shouldn't you store that disposable and call .dispose on ViewModel::onCleared()? I'm asking because I truly don't know if you should

Similarly, on line 58: intents.subscribe(mIntentsSubject);:
I'm guessing the intents observable has some kind of reference to the view (RxBindings), as the view gets destroyed will that translate to a memory leak? or it unsubscribes automatically for us?
If it's the latter, processIntents could return a Disposable to the view perhaps?

Time limited event management

Basically the case when one needs to display a snackbar and do not want to display it again when the latest state is reemitted (e.g. on rotation or just even onStart() after switching app).

In the blueprint they created a SingleLiveEvent but I really don't think it's a good solution. They are many pros and cons discussed in this thread about this class.

In the above thread, Hannes suggests the view would tell the viewmodel that it has shown the message once it did for the viewmodel to emit a new state containing the message triggering to an off value. I totally agree with this, UI and VM are not coupled, keeps the flow pure, etc.
The code looks like this

      if (state.getNextPageError() != null) {
        Snackbar.make(
                   binding.get().loadMoreBar,
                   state.getNextPageError(),
                   Snackbar.LENGTH_LONG
                 )
                 .show();
        searchViewModel.clearNextPageErrorMessage();
      }

So how about the MVI architecture. We have a unidirectional data flow in place, and I think it leads to two solutions (as least I could think of)

Clearing Intent

Instead of telling the viewmodel to clear the error message, we would emit a new intent for the same purpose, turning the message triggering value to null or false.

I think it's ok but

  1. Boilerplate... Having to create a new intent/action/result sounds like an overkill. Might feel better if this happens a lot and could make this logic generic.
  2. Ideally I don't want the rendering function to talk to the intents emitting one. In the architecture graph, they only communicate through the user() and I think it has meanings and respecting this barrier helps make a better app.

Conclusion: works but not fully satisfying (probably being picky)

Emitting two consecutive event

Inside the processor, when emitting a result, it could be done as not only one but two consecutive events would be emitted. The first event will trigger the message and the second event would just clear the trigger. It is basically as above but without any needs to create a new intent from the UI.

It works but

  1. The business logic is now coupled with the UI which is never good.

Conclusion: less boilerplate than above but potentially worse tech debt.

Timely marked event

No implementation but a vague idea about marking the event with a timestamp that the view would check to decide if it needs to display it or not. Something like if (event.within10Seconds() show(). Not sure is there is real value here.


Could not find aapt2-proto.jar (com.android.tools.build:aapt2-proto:0.3.1).

org.gradle.api.internal.artifacts.ivyservice.DefaultLenientConfiguration$ArtifactResolveException: Could not resolve all artifacts for configuration ':classpath'.
at org.gradle.api.internal.artifacts.configurations.DefaultConfiguration.rethrowFailure(DefaultConfiguration.java:995)
at org.gradle.api.internal.artifacts.configurations.DefaultConfiguration.access$1700(DefaultConfiguration.java:121)
at org.gradle.api.internal.artifacts.configurations.DefaultConfiguration$ConfigurationArtifactCollection.ensureResolved(DefaultConfiguration.java:1336)
at org.gradle.api.internal.artifacts.configurations.DefaultConfiguration$ConfigurationArtifactCollection.getArtifacts(DefaultConfiguration.java:1308)
at org.gradle.composite.internal.CompositeBuildClassPathInitializer.execute(CompositeBuildClassPathInitializer.java:40)
at org.gradle.composite.internal.CompositeBuildClassPathInitializer.execute(CompositeBuildClassPathInitializer.java:28)
at org.gradle.api.internal.initialization.DefaultScriptClassPathResolver.resolveClassPath(DefaultScriptClassPathResolver.java:37)
at org.gradle.api.internal.initialization.DefaultScriptHandler.getScriptClassPath(DefaultScriptHandler.java:72)
at org.gradle.plugin.use.internal.DefaultPluginRequestApplicator.defineScriptHandlerClassScope(DefaultPluginRequestApplicator.java:204)
at org.gradle.plugin.use.internal.DefaultPluginRequestApplicator.applyPlugins(DefaultPluginRequestApplicator.java:82)
at org.gradle.configuration.DefaultScriptPluginFactory$ScriptPluginImpl.apply(DefaultScriptPluginFactory.java:184)
at org.gradle.configuration.BuildOperationScriptPlugin$1.run(BuildOperationScriptPlugin.java:61)
at org.gradle.internal.operations.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:317)
at org.gradle.internal.operations.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:309)
at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:185)
at org.gradle.internal.operations.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:97)
at org.gradle.internal.operations.DelegatingBuildOperationExecutor.run(DelegatingBuildOperationExecutor.java:31)
at org.gradle.configuration.BuildOperationScriptPlugin.apply(BuildOperationScriptPlugin.java:58)
at org.gradle.configuration.project.BuildScriptProcessor.execute(BuildScriptProcessor.java:41)
at org.gradle.configuration.project.BuildScriptProcessor.execute(BuildScriptProcessor.java:26)
at org.gradle.configuration.project.ConfigureActionsProjectEvaluator.evaluate(ConfigureActionsProjectEvaluator.java:34)
at org.gradle.configuration.project.LifecycleProjectEvaluator.doConfigure(LifecycleProjectEvaluator.java:64)
at org.gradle.configuration.project.LifecycleProjectEvaluator.access$100(LifecycleProjectEvaluator.java:34)
at org.gradle.configuration.project.LifecycleProjectEvaluator$ConfigureProject.run(LifecycleProjectEvaluator.java:110)
at org.gradle.internal.operations.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:317)
at org.gradle.internal.operations.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:309)
at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:185)
at org.gradle.internal.operations.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:97)
at org.gradle.internal.operations.DelegatingBuildOperationExecutor.run(DelegatingBuildOperationExecutor.java:31)
at org.gradle.configuration.project.LifecycleProjectEvaluator.evaluate(LifecycleProjectEvaluator.java:50)
at org.gradle.api.internal.project.DefaultProject.evaluate(DefaultProject.java:667)
at org.gradle.api.internal.project.DefaultProject.evaluate(DefaultProject.java:136)
at org.gradle.execution.TaskPathProjectEvaluator.configure(TaskPathProjectEvaluator.java:35)
at org.gradle.execution.TaskPathProjectEvaluator.configureHierarchy(TaskPathProjectEvaluator.java:60)
at org.gradle.configuration.DefaultBuildConfigurer.configure(DefaultBuildConfigurer.java:38)
at org.gradle.initialization.DefaultGradleLauncher$ConfigureBuild.run(DefaultGradleLauncher.java:261)
at org.gradle.internal.operations.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:317)
at org.gradle.internal.operations.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:309)
at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:185)
at org.gradle.internal.operations.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:97)
at org.gradle.internal.operations.DelegatingBuildOperationExecutor.run(DelegatingBuildOperationExecutor.java:31)
at org.gradle.initialization.DefaultGradleLauncher.configureBuild(DefaultGradleLauncher.java:173)
at org.gradle.initialization.DefaultGradleLauncher.doBuildStages(DefaultGradleLauncher.java:132)
at org.gradle.initialization.DefaultGradleLauncher.getConfiguredBuild(DefaultGradleLauncher.java:110)
at org.gradle.internal.invocation.GradleBuildController$2.call(GradleBuildController.java:87)
at org.gradle.internal.invocation.GradleBuildController$2.call(GradleBuildController.java:84)
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:152)
at org.gradle.internal.work.StopShieldingWorkerLeaseService.withLocks(StopShieldingWorkerLeaseService.java:38)
at org.gradle.internal.invocation.GradleBuildController.doBuild(GradleBuildController.java:100)
at org.gradle.internal.invocation.GradleBuildController.configure(GradleBuildController.java:84)
at org.gradle.tooling.internal.provider.runner.ClientProvidedBuildActionRunner.run(ClientProvidedBuildActionRunner.java:64)
at org.gradle.launcher.exec.ChainingBuildActionRunner.run(ChainingBuildActionRunner.java:35)
at org.gradle.launcher.exec.ChainingBuildActionRunner.run(ChainingBuildActionRunner.java:35)
at org.gradle.tooling.internal.provider.ValidatingBuildActionRunner.run(ValidatingBuildActionRunner.java:32)
at org.gradle.launcher.exec.RunAsBuildOperationBuildActionRunner$3.run(RunAsBuildOperationBuildActionRunner.java:45)
at org.gradle.internal.operations.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:317)
at org.gradle.internal.operations.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:309)
at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:185)
at org.gradle.internal.operations.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:97)
at org.gradle.internal.operations.DelegatingBuildOperationExecutor.run(DelegatingBuildOperationExecutor.java:31)
at org.gradle.launcher.exec.RunAsBuildOperationBuildActionRunner.run(RunAsBuildOperationBuildActionRunner.java:42)
at org.gradle.tooling.internal.provider.SubscribableBuildActionRunner.run(SubscribableBuildActionRunner.java:51)
at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:47)
at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:31)
at org.gradle.launcher.exec.BuildTreeScopeBuildActionExecuter.execute(BuildTreeScopeBuildActionExecuter.java:39)
at org.gradle.launcher.exec.BuildTreeScopeBuildActionExecuter.execute(BuildTreeScopeBuildActionExecuter.java:25)
at org.gradle.tooling.internal.provider.ContinuousBuildActionExecuter.execute(ContinuousBuildActionExecuter.java:80)
at org.gradle.tooling.internal.provider.ContinuousBuildActionExecuter.execute(ContinuousBuildActionExecuter.java:53)
at org.gradle.tooling.internal.provider.ServicesSetupBuildActionExecuter.execute(ServicesSetupBuildActionExecuter.java:61)
at org.gradle.tooling.internal.provider.ServicesSetupBuildActionExecuter.execute(ServicesSetupBuildActionExecuter.java:34)
at org.gradle.tooling.internal.provider.GradleThreadBuildActionExecuter.execute(GradleThreadBuildActionExecuter.java:36)
at org.gradle.tooling.internal.provider.GradleThreadBuildActionExecuter.execute(GradleThreadBuildActionExecuter.java:25)
at org.gradle.tooling.internal.provider.ParallelismConfigurationBuildActionExecuter.execute(ParallelismConfigurationBuildActionExecuter.java:43)
at org.gradle.tooling.internal.provider.ParallelismConfigurationBuildActionExecuter.execute(ParallelismConfigurationBuildActionExecuter.java:29)
at org.gradle.tooling.internal.provider.StartParamsValidatingActionExecuter.execute(StartParamsValidatingActionExecuter.java:64)
at org.gradle.tooling.internal.provider.StartParamsValidatingActionExecuter.execute(StartParamsValidatingActionExecuter.java:29)
at org.gradle.tooling.internal.provider.SessionFailureReportingActionExecuter.execute(SessionFailureReportingActionExecuter.java:59)
at org.gradle.tooling.internal.provider.SessionFailureReportingActionExecuter.execute(SessionFailureReportingActionExecuter.java:44)
at org.gradle.tooling.internal.provider.SetupLoggingActionExecuter.execute(SetupLoggingActionExecuter.java:46)
at org.gradle.tooling.internal.provider.SetupLoggingActionExecuter.execute(SetupLoggingActionExecuter.java:30)
at org.gradle.launcher.daemon.server.exec.ExecuteBuild.doBuild(ExecuteBuild.java:67)
at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:36)
at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:122)
at org.gradle.launcher.daemon.server.exec.WatchForDisconnection.execute(WatchForDisconnection.java:37)
at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:122)
at org.gradle.launcher.daemon.server.exec.ResetDeprecationLogger.execute(ResetDeprecationLogger.java:26)
at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:122)
at org.gradle.launcher.daemon.server.exec.RequestStopIfSingleUsedDaemon.execute(RequestStopIfSingleUsedDaemon.java:34)
at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:122)
at org.gradle.launcher.daemon.server.exec.ForwardClientInput$2.call(ForwardClientInput.java:74)
at org.gradle.launcher.daemon.server.exec.ForwardClientInput$2.call(ForwardClientInput.java:72)
at org.gradle.util.Swapper.swap(Swapper.java:38)
at org.gradle.launcher.daemon.server.exec.ForwardClientInput.execute(ForwardClientInput.java:72)
at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:122)
at org.gradle.launcher.daemon.server.exec.LogAndCheckHealth.execute(LogAndCheckHealth.java:55)
at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:122)
at org.gradle.launcher.daemon.server.exec.LogToClient.doBuild(LogToClient.java:62)
at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:36)
at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:122)
at org.gradle.launcher.daemon.server.exec.EstablishBuildEnvironment.doBuild(EstablishBuildEnvironment.java:82)
at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:36)
at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:122)
at org.gradle.launcher.daemon.server.exec.StartBuildOrRespondWithBusy$1.run(StartBuildOrRespondWithBusy.java:50)
at org.gradle.launcher.daemon.server.DaemonStateCoordinator$1.run(DaemonStateCoordinator.java:295)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:63)
at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:46)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:55)
at java.lang.Thread.run(Thread.java:748)
Caused by: org.gradle.internal.resolve.ArtifactNotFoundException: Could not find aapt2-proto.jar (com.android.tools.build:aapt2-proto:0.3.1).
Searched in the following locations:
https://jcenter.bintray.com/com/android/tools/build/aapt2-proto/0.3.1/aapt2-proto-0.3.1.jar
at org.gradle.internal.resolve.result.DefaultBuildableArtifactResolveResult.notFound(DefaultBuildableArtifactResolveResult.java:27)
at org.gradle.api.internal.artifacts.ivyservice.ivyresolve.CachingModuleComponentRepository$LocateInCacheRepositoryAccess.resolveArtifactFromCache(CachingModuleComponentRepository.java:338)
at org.gradle.api.internal.artifacts.ivyservice.ivyresolve.CachingModuleComponentRepository$LocateInCacheRepositoryAccess.resolveArtifact(CachingModuleComponentRepository.java:293)
at org.gradle.api.internal.artifacts.ivyservice.ivyresolve.ErrorHandlingModuleComponentRepository$ErrorHandlingModuleComponentRepositoryAccess.resolveArtifact(ErrorHandlingModuleComponentRepository.java:181)
at org.gradle.api.internal.artifacts.ivyservice.ivyresolve.RepositoryChainArtifactResolver.resolveArtifact(RepositoryChainArtifactResolver.java:78)
at org.gradle.api.internal.artifacts.ivyservice.resolveengine.artifact.DefaultArtifactSet$LazyArtifactSource.create(DefaultArtifactSet.java:171)
at org.gradle.api.internal.artifacts.ivyservice.resolveengine.artifact.DefaultArtifactSet$LazyArtifactSource.create(DefaultArtifactSet.java:158)
at org.gradle.api.internal.artifacts.DefaultResolvedArtifact.getFile(DefaultResolvedArtifact.java:140)
at org.gradle.api.internal.artifacts.ivyservice.resolveengine.artifact.ArtifactBackedResolvedVariant$DownloadArtifactFile.run(ArtifactBackedResolvedVariant.java:136)
at org.gradle.internal.operations.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:317)
at org.gradle.internal.operations.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:309)
at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:185)
at org.gradle.internal.operations.DefaultBuildOperationExecutor.access$900(DefaultBuildOperationExecutor.java:50)
at org.gradle.internal.operations.DefaultBuildOperationExecutor$ParentPreservingQueueWorker.execute(DefaultBuildOperationExecutor.java:359)
at org.gradle.internal.operations.DefaultBuildOperationQueue$WorkerRunnable.runOperation(DefaultBuildOperationQueue.java:230)
at org.gradle.internal.operations.DefaultBuildOperationQueue$WorkerRunnable.access$600(DefaultBuildOperationQueue.java:172)
at org.gradle.internal.operations.DefaultBuildOperationQueue$WorkerRunnable$1.call(DefaultBuildOperationQueue.java:209)
at org.gradle.internal.operations.DefaultBuildOperationQueue$WorkerRunnable$1.call(DefaultBuildOperationQueue.java:203)
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:152)
at org.gradle.internal.operations.DefaultBuildOperationQueue$WorkerRunnable.runBatch(DefaultBuildOperationQueue.java:202)
at org.gradle.internal.operations.DefaultBuildOperationQueue$WorkerRunnable.run(DefaultBuildOperationQueue.java:177)
... 6 more

Filtering intents by state?

I'm implementing an MVI that draws alot of inspiration form here, great work.

In my application some actions can only be performed from certain states. (Example: The user should only be able to submit a form that has passed validation, and cant submit when a request is in flight)

I'm thinking the cleanest approach is to filter the intent stream, and log/throw/ignore when the intent cant be transformed into an action for the current state.

However, the state resides in the state reducer, it is not accessible to the intent transformer. I'm not sure how to implement the desired behavior, and would greatly appreciate your feedback.

Don't emit duplicate consecutive MviViewStates

When a reducer just emits previousState, there's no reason to call render. In fact, redrawing the UI in cases like this can cause jank (e.g. messing up snackbar animations by showing the same snackbar twice in rapid succession).

I'd fix this with a distinctUntilChanged right after scan in compose

[q] Access state in stream

Hello.
Thank you for this wonderful demo project. It does clear up things a lot.

I have the following task:
i have paginated data + search functionality that is done on backend. So every time the search text changes, i put it into the state object and all data load requests will take that search string as a parameter. My question is whether i can somehow read that search string value from current state when transforming intents into actions? What is a good way of doing it?

I am thinking of using a BehaviorSubject for state as a separate stream that the original stream will push data to from subscribe(). But it looks kinda ugly and i wanted to figure out a good way of doing it.

How to handle realtime data subscriber

Example : Chat Application that message can come anytime
I should subscribe to realtime data source on Fragment or ViewModel ?
if I subscribe on ViewModel so it ok or not to call intent inside ViewModel?

Thank yoy

Opinion on where to store/update model state

I'm looking for opinions on where to store model state that can't be extracted from the View. Keeping with the tasks example let's add a Tag object to a task. A Tag consists of an Id and a Label. The ViewState only cares about the Label so it can display it, so the Id isn't part of the state. Whose job should it be to actually update the Task object with the correct TagId until we hit save? The action processor, the viewmodel? Would updating our in memory Task just be a side effect of intents, or could we store things in our ViewState that don't necessarily display directly but are need for when the user hits save? That seems messy though because you'd have to store the current state outside of the stream.

Reduce code duplication on ViewModels

I would like to submit a PR restructuring the code to avoid the duplication of boiler plate code on MviViewModels. But I would like you get your approval before I get my hands dirty.

My approach would be converting the MviViewModel interface into an abstract class like this:

abstract public class MviViewModel<I extends MviIntent, S extends MviViewState> extends ViewModel {

    private PublishSubject<I> mIntentsSubject = PublishSubject.create();
    private Observable<S> mStatesObservable;

    protected abstract Observable<S> compose(PublishSubject<I> s);

    public void processIntents(Observable<I> intents) {
        intents.subscribe(mIntentsSubject);
    }

    public Observable<S> states() {
        if (mStatesObservable == null) mStatesObservable = compose(mIntentsSubject);
        return mStatesObservable;
    }
}

With this we can avoid repeating the following code: (which is identical for every single ViewModel)

private PublishSubject<I> mIntentsSubject;
private Observable<S> mStatesObservable;

 public CustomViewModel(@NonNull TaskDetailActionProcessorHolder actionProcessorHolder) {
     mIntentsSubject = PublishSubject.create();
     mStatesObservable = compose();
 }

@Override
 public void processIntents(Observable<TaskDetailIntent> intents) {
     intents.subscribe(mIntentsSubject);
 }

 @Override
 public Observable<TaskDetailViewState> states() {
     return mStatesObservable;
 }

Creating a new ViewModel is much more straightforward.
Can you see any disadvantages to this approach? Please let me know!

Handling intents with no mapping actions

When a dialog needs to be displayed in response to a button click, do you need an intent and a property (displayDialog: Boolean) in State data class? If yes, then how do you combine this intent->state flow with intent->action->result->state flow so that both use the latest state to produce a new one?

events
  .scan(State.default(), { state, event -> eventToState(event, state) })

events
  .map(::eventToAction)
  .compose(actionDispatcher)
  .scan(State.default(), { state, result -> resultToState(result, state) })

The two observables above produce states, but merging them is not an option because they both maintain their own states.

TasksFragment memoryleak

I added Leak Canary to the project and after rotate the screen a couple of times the TasksFragment gets leaked. I think it is because the intents are observed in the ViewModel, therefore, when the TasksFragments reaches the onDestroyView it can't actually get destroyed.

Intent subscription is never disposed

We've been using a variation of this architecture for just over a year at VanMoof, and for the most part it's been working great. However, we encountered an issue with the processIntents() function in each ViewModel:

override fun processIntents(intents: Observable<TasksIntent>) {
  intents.subscribe(intentsSubject)
}

The statesObservable is added to a CompositeDisposable in each fragment and so is disposed correctly, but this isn't propagated by intentsSubject, so a memory leak occurs here. We found this to be particularly problematic when using RxBroadcastReceiver for sending intents because the receiver would never be unregistered, so we would end up with intents being duplicated when re-opening a screen.

We solved this by adding the intents subscription to a CompositeDisposable in each ViewModel and disposing it in onCleared():

override fun processIntents(intents: Observable<TasksIntent>) {
  disposables.add(intents.subscribe(intentsSubject::onNext))
}

override fun onCleared() {
  disposables.dispose()
}

NullPointerException when reducer is extracted into a separate class

Hi, greetings from Peru.

Thank you so much for this awesome repository. It has helped me a lot to handle state in my applications in a much better way and I´ve learned a lot from your repository.

Recently, I've been having a weird issue. When I extract the reducer into another class and use it in the ViewModel, it throws a NullPointerException in the compose function, specifically in the scan part indicating that the accumulator is null.

Let's extract the reducer into its own class. I did this because my reducer started to get really big and I decided to separate it from the ViewModel.

class MonitoringStateReducer: BiFunction<MonitoringViewState, MonitoringResult, 
    MonitoringViewState> {
    override fun apply(
        previousState: MonitoringViewState,
        result: MonitoringResult
    ): MonitoringViewState {
        when (result) {
           //Returns a non-null new state
        }
    }
}

Then, in the ViewModel, I import it and use it like a normal class.

class MonitoringViewModel @Inject constructor(
    private val processor: MonitoringProcessor
) : BaseViewModel<MonitoringIntention, MonitoringViewState>() {
    private val reducer: MonitoringStateReducer = MonitoringStateReducer()
    //Similar properties like in your repository

    private fun compose(): Observable<MonitoringViewState> {
        return intentsSubject.compose(intentFilter)
            .map(actionFromIntent)
            .compose(processor)
            .scan(MonitoringViewState.init(), reducer) //Exception is here
            .distinctUntilChanged()
            .replay(1)
            .autoConnect(0)
    }

    override fun state(): Observable<MonitoringViewState> = compose()

    //Similar functions like in your repository
}

And when I run the app, NullPointerException is thrown.

2019-08-23 16:57:41.049 6925-6925/com.name.app E/AndroidRuntime: FATAL EXCEPTION: main Process: com.name.app, PID: 6925 java.lang.RuntimeException: Unable to start activity ComponentInfo{com.name.app/com.name.app.features.monitoring.presentation.MonitoringActivity}: java.lang.NullPointerException: accumulator is null at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2907) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2986) at android.app.ActivityThread.-wrap11(Unknown Source:0) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1641) at android.os.Handler.dispatchMessage(Handler.java:105) at android.os.Looper.loop(Looper.java:164) at android.app.ActivityThread.main(ActivityThread.java:6694) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:769) Caused by: java.lang.NullPointerException: accumulator is null at io.reactivex.internal.functions.ObjectHelper.requireNonNull(ObjectHelper.java:39) at io.reactivex.Observable.scanWith(Observable.java:11537) at io.reactivex.Observable.scan(Observable.java:11502) at com.name.app.features.monitoring.presentation.MonitoringViewModel.compose(MonitoringViewModel.kt:47) at com.name.app.features.monitoring.presentation.MonitoringViewModel.(MonitoringViewModel.kt:18) at com.name.app.features.monitoring.presentation.MonitoringViewModel_Factory.get(MonitoringViewModel_Factory.java:25) at com.name.app.features.monitoring.presentation.MonitoringViewModel_Factory.get(MonitoringViewModel_Factory.java:8) at dagger.internal.DoubleCheck.get(DoubleCheck.java:47) at com.name.app.di.viewmodel.ViewModelFactory.create(ViewModelFactory.kt:12) at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:164) at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:130) at com.name.app.features.monitoring.presentation.MonitoringActivity$viewModel$2.invoke(MonitoringActivity.kt:46) at com.name.app.features.monitoring.presentation.MonitoringActivity$viewModel$2.invoke(MonitoringActivity.kt:26) at kotlin.UnsafeLazyImpl.getValue(Lazy.kt:81) at com.name.app.features.monitoring.presentation.MonitoringActivity.getViewModel(Unknown Source:7) at com.name.app.features.monitoring.presentation.MonitoringActivity.bind(MonitoringActivity.kt:85) at com.name.app.features.monitoring.presentation.MonitoringActivity.onCreate(MonitoringActivity.kt:119) at android.app.Activity.performCreate(Activity.java:6984) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1235) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2860) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2986) at android.app.ActivityThread.-wrap11(Unknown Source:0) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1641) at android.os.Handler.dispatchMessage(Handler.java:105) at android.os.Looper.loop(Looper.java:164) at android.app.ActivityThread.main(ActivityThread.java:6694) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:769)

Lazy initialization doesn't work either

private val reducer by lazy(LazyThreadSafetyMode.NONE) {
    MonitoringStateReducer()
}

It only works when I use it this way

private val reducer: BiFunction<MonitoringViewState, MonitoringResult, MonitoringViewState>
    get() = MonitoringStateReducer()

Any ideas why this is happening?
I post this same issue on stackoverflow, but it hasn't received too much attention. (https://stackoverflow.com/questions/57596063/nullpointerexception-when-implementing-java-interface-from-kotlin)

English is not my mother tongue; please excuse any errors on my part.

[q] Extension instead of ObservableTransformer?

Forgive me, I'm a bit of a Kotlin & Rx noob. Is there any problem with using Kotlin extension methods instead of ObservableTransformers for the 'ActionProcessorHolder' classes? The processors become a bit more concise, and from what I've read ObservableTransformers exist to make up for the lack of extensions in Java.

private val updateTaskProcessor =
            ObservableTransformer<UpdateTaskAction, UpdateTaskResult> { actions ->
                actions.flatMap { action ->
                    tasksRepository.saveTask(
                            Task(title = action.title, description = action.description, id = action.taskId)
                    ).andThen(Observable.just(UpdateTaskResult))
                }
            }

becomes

private fun Observable<UpdateTaskAction>.updateTaskProcessor() = flatMap { action ->
        tasksRepository.saveTask(
                Task(title = action.title, description = action.description, id = action.taskId))
                .andThen(Observable.just(UpdateTaskResult))
    }

Thanks for the project, I've found it extremely helpful.

ViewModel must never reference a view

Hi,

The official Android documentation says:

Caution: A ViewModel must never reference a view, Lifecycle, or any class that may hold a reference to the activity context.

I didn't dive deeply into MVI architecture and your code yet, but looks like View Model is observing events from Activty, which means it has a reference to activty.

Is it done intentional?

Feedback

Hey, your example looks great!

I don’t have much to complain about …
Just some notes that are mostly just a matter of personal prefrences:

  • Usually you should prefer switchMap() over flatMap() when dealing with UI Events
  • Maybe you can inject the actionProcessor as constructor parameter into your MviViewModel. Or move it into its own .java file. This would reduce the number of lines of your ViewModel quite a bit.
  • Maybe observeOn(mSchedulerProvider.ui()) can be moved into View Layer (like Fragment)
  • Espresso idling resource is super ugly! I know that all examples in this repository do that, but we can do better!
  • You haven’t faced navigation yet, but maybe you will do in the future (`AddEditTaskFragment for example). Navigation is one those questions I have been asked quite often when talking about MVI. I think navigation can be seen us a "side effect" of state transitions. More information here: sockeqwe/mosby#261 (comment) . But maybe you have a better or different approach for Navigation.

Duplicate Intents triggered when onStart/onStop called multiple times

For example, on the TasksFragment if you background the app and then restore you can end up with multiple RefreshIntents being processed, adding some logs you can see this below.

2018-11-09 10:20:52.061 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksFragment.onStart
2018-11-09 10:20:52.571 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksViewModel.actionFromIntent. intent: com.example.android.architecture.blueprints.todoapp.tasks.TasksIntent$InitialIntent@c49fab6
2018-11-09 10:21:01.870 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksViewModel.actionFromIntent. intent: RefreshIntent(forceUpdate=false)
2018-11-09 10:21:09.377 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksFragment.onStop
2018-11-09 10:21:11.244 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksFragment.onStart
2018-11-09 10:21:11.258 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksViewModel.actionFromIntent. intent: RefreshIntent(forceUpdate=false)
2018-11-09 10:21:11.264 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksViewModel.actionFromIntent. intent: RefreshIntent(forceUpdate=false)
2018-11-09 10:21:14.746 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksFragment.onStop
2018-11-09 10:21:16.083 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksFragment.onStart
2018-11-09 10:21:16.128 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksViewModel.actionFromIntent. intent: RefreshIntent(forceUpdate=false)
2018-11-09 10:21:16.132 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksViewModel.actionFromIntent. intent: RefreshIntent(forceUpdate=false)
2018-11-09 10:21:16.133 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksViewModel.actionFromIntent. intent: RefreshIntent(forceUpdate=false)
2018-11-09 10:21:18.552 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksFragment.onStop
2018-11-09 10:21:20.390 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksFragment.onStart
2018-11-09 10:21:20.397 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksViewModel.actionFromIntent. intent: RefreshIntent(forceUpdate=false)
2018-11-09 10:21:20.402 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksViewModel.actionFromIntent. intent: RefreshIntent(forceUpdate=false)
2018-11-09 10:21:20.406 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksViewModel.actionFromIntent. intent: RefreshIntent(forceUpdate=false)
2018-11-09 10:21:23.480 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksFragment.onStop
2018-11-09 10:21:25.974 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksFragment.onStart
2018-11-09 10:21:25.982 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksViewModel.actionFromIntent. intent: RefreshIntent(forceUpdate=false)
2018-11-09 10:21:25.998 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksViewModel.actionFromIntent. intent: RefreshIntent(forceUpdate=false)
2018-11-09 10:21:29.020 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksFragment.onStop
2018-11-09 10:21:30.883 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksFragment.onStart
2018-11-09 10:21:30.904 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksViewModel.actionFromIntent. intent: RefreshIntent(forceUpdate=false)
2018-11-09 10:21:30.910 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksViewModel.actionFromIntent. intent: RefreshIntent(forceUpdate=false)
2018-11-09 10:21:30.912 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksViewModel.actionFromIntent. intent: RefreshIntent(forceUpdate=false)
2018-11-09 10:21:30.913 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksViewModel.actionFromIntent. intent: RefreshIntent(forceUpdate=false)
2018-11-09 10:21:30.914 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksViewModel.actionFromIntent. intent: RefreshIntent(forceUpdate=false)
2018-11-09 10:21:30.916 30351-30351/com.example.android.architecture.blueprints.todomvp.mock D/todoapp: TasksViewModel.actionFromIntent. intent: RefreshIntent(forceUpdate=false)

The issue looks to be related to the processIntents method with the ViewModel, where intents are subscribed multiple times but the old subscription never gets disposed. Its possible to ignore the duplicates by using the distinct operator but there's maybe a question as to whether you want to keep the old subscription active in the ViewModel

disposing render subscription after onDestroyView

Issue:

this might be an issue if an async event comes back and triggers a render between the onDestroyView and onDestroybecause the fragment view is already destroyed with the onDestroyView but we did not unsubscribe the render subscription.

Clearing the disposables in the fragments happening with the onDestroy event:

override fun onDestroy() { super.onDestroy() disposables.dispose() }

fragment lifecycle:
... onStop onDestroyView onDestroy

Reproducing the issue:
this could be reproduced if you call detach after binding the ViewModel and delaying the state observable.
detach: detach triggers the destruction of the view without calling onDestroy
delay: simulates the late render event

Solution suggestion:
Moving up the bind event to the onStart() and the unsubscription to the onStop() ?

Some feedback of the implementation

Hello, @oldergod thank you for the amazing MVI implementation. I have been using it for a while in my project. I like this implementation very much, but I have encountered some problems, although they have been solved, I have been confused to do so, is it right or not.

  1. I am trying to write an accounting app for your implementation. I need to display a list of all accountings on the home page. I need to add a section header before the accounting of the day. The section header needs to be displayed the sum of the day's accountings because the list needs to be paged after paging, I don't know that the last accounting adapter item displayed is the last data of the day, so the sum can only be obtained from the database through SQL:
private fun createHeader(createTime: Date): MainAccountingDetailHeader {
    val title: String = createHeaderTitle(createTime)
    // SELECT SUM(amount) FROM accounting
    val sum = getSumFromDatabase(oneDayFormat.format(createTime))
    val sumString: String = applicationContext.getString(
        R.string.main_accounting_detail_header_sum, sum
    )
    return MainAccountingDetailHeader(title, sumString)
  }

You can check out my implementation here https://github.com/littleGnAl/Accounting/blob/591a8705bffebc79690f8bb716b35f81dc8d183c/app/src/main/java/com/littlegnal/accounting/ui/main/MainActionProcessorHolder.kt#L77. My solution is to put the observeOn() method behind scan() and let the reducer switch to the main thread after it has finished executing.

  1. There is a page that requires the user to select some and fill in some information, such as username, email, address, etc. The definition of ViewState is as follows:
data class UserInfoViewState(
    val isLoading: Boolean,
    val error: Throwable? = null,
    val firstName: String? = null, 
    val lastName: String? = null, 
    val email: String? = null, 
    val phone: String? = null, 
    val address: String? = null
)

When the user clicks the confirmation button, it checks whether the information is valid. It will be saved the to the server if it is valid. I tried to save this information in ViewState, but I can only check if the information of preViewState is valid in the reducer. But in the reducer cannot call the Retrofit API to save the data, my current solution is to put these user information into the UserInfoValueHolder, and then in the corresponding processor to check whether the value of UserInfoValueHolder is valid, if valid directly call The API saves the user information to the server:

data class UserInfoValueHolder(
    val firstName: String? = null, 
    val lastName: String? = null, 
    val email: String? = null, 
    val phone: String? = null, 
    val address: String? = null
)

class UserInfoActionProcessorHolder {
    private var userInfoValueHolder: UserInfoValueHolder? = null

    ...
}

This solves the problem, but userInfoValueHolder is not immutable and can be changed anywhere, which is not what I want to see. I don't know if you can understand the problem I have expressed and whether you have encountered similar problems. I want to listen to your suggestions and look forward to your reply. Thank you.

[WIP] MVI-rxjava

  • Statistics
  • Tasks
  • TaskDetail by @malmstein
  • AddEditTask

Tests

Mostly untouched basically. Just removed obsolete view/presenter two way connections and rewrote other tests so they would pass.

View

It might be a good idea to rewrite part of them to match in a more idiomatic way the architecture works; i.e. testing 1) intents() based on UI interaction and 2) render() based on a state.

ViewModel

To which extent can we consider it a black box?
A: Would it be better to test every step of the data flow? intents → actions, actions → results, results → state
B: Or just test the input/ouput of the model? Basically intents → state.

At the moment, this is moving toward B and I don't dislike it. Although with A, we can find restrain greatly the scope of a bug. Not sure here.

SchedulerHook

Right now, we handle EspressoIdlingResource based on emitted state: it's ugly and does not support multiple intents returning only one state (Use of switch map).
How about doing some stuff with a custom scheduler that would handle this for us? Sounds great.

[UPDATE] I removed everything that dealed with IdlingResource and tests still pass... ? Otherwise I thought it would be nice to try RxIdler

initialIntentFilter

Could this be done differently? I don't see it as a hack but I am not proud of it either. I've heard stories about making the state observable a BehaviorSubject but then, that means the ViewModel is itself initializing the first load?

All those classes and all those casts

That would be the "downside" of the architecture as it might look like a lot of boilerplate. But, Kotlin could help a lot with this and since the android-architecture would propably accept a sample in Kotlin now, we should give it a try too. Also compare the java version + the kotlin version.

Navigation

I see different patterns for navigation and I'd use one of each, based on the context.

A/ A menu link has been clicked and it's a dumb, no repo side-effect navigation; e.g. statistics link in the drawer. In this case, just startActivity without entering the data flow.
B/ Navigation is the result of a side effect; e.g. AddEditTask#saveTask. In this case, I would just startActivity inside the View#render. The problem is that only works when the activity finishes as well. If this was not the case, that would be a problem for the navigating state would be repeated when coming back on the activity.

These do not seem to cover what @malmstein called a Navigator. I'd like to see how it fits the architecture and what are its responsibilities.

Interfaces

I created MviIntent, MviAction, MviViewModel, etc as a guide but I'm not sure they really need to stay on real projects. Did they help? Would it be better without them?

UI Notification

How to deal with UI notification such as SnackBar? I kind of cheated so far.
For instance, see how I handle a task marked as completed. I show a message only when the boolean says so here and I set it to true only on an IN_FLIGHT result.
I don't think it's a good use of the status here but I actually like the idea: 1/ emit an event that renders as a UI notification and 2/ cancel it so it won't be displayed an other time on config change.

But it is a way which badly ends up coupling the UI and the Processor? Don't know.

pairWithDelay causing UI rendering bug

Firstly thank you for this nice sample code.

When playing with the app I found a bug shown in this video: https://youtu.be/RG_m3_jO7SQ

It happens when you check and uncheck the 'complete' box of a task within 2 seconds. Basically what happens is when you check the box, the 'task completed' Snackbar is shown. Then within 2 seconds, you uncheck the box, the 'task completed' is shown again (which does not reflect the current state of the task - activated) and the 'task activated' snackbar is only shown after the second 'task completed' Snackbar is auto-dismissed.

This bug is caused by the way Snackbar show events are currently managed using pairWithDelay which emits a new view state after 2 seconds that clears the property (e.g., taskComplete or taskActivated) which invokes Snackbar.

What happens is:

  1. Current time - check -> taskComplete = true, taskActivated = false -> showMessage for 'task completed' called
  2. Assume 1 second later - uncheck -> taskComplete = true, taskActivated = true -> showMessage for 'task activated' called immediately followed by showMessage for 'task completed'. Thats why you'll see only 'task complete' snackbar
  3. 2 seconds later -> taskComplete = false, taskActivated = true -> showMessage for 'task activated' called
  4. 3 seconds later -> taskComplete = false, taskActivated = true -> Does nothing since the Snackbar's dismissal is handled by Snackbar itself due to Snackbar.LENGTH_LONG is used.

I believe the root cause of this issue is the fact that pairWithDelay with 2 seconds is used. I understand from the conversation in issue #6 that it was an attempt to adopt Hannes's suggestion on SingleLiveEvent alternative. IMHO the key idea from his blog post is to have the presenter/viewmodel responsible for both showing and dismissing Snackbars (by constructing view states). To do that, he set Snackbar time to Snackbar.LENGTH_INDEFINITE.

However, in this app, Snackbar showing time is set to Snackbar.LENGTH_LONG which means the dismissal of Snackbar is handled by the Snackbar itself. This raises a question as to why you need the timer of 2 seconds to update the view state and then that updated state is not used for view rendering (false values of properties like taskComplete or taskActivated are not used in render method). Also Snackbar.LENGTH_LONG is normally not 2 seconds.

IMHO there are 2 approaches to fix this issue:

  1. Remove pairWithDelay. Instead immediately emits a new state with false property.
  2. Follow exactly Hannes' suggestion by having the ViewModel to handle Snackbar's dismissal.

Would be interested hearing your thoughts.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.