From 6cdb15eadcb724c6bfd5c26e75b859deb3a3ad44 Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Fri, 27 Jan 2017 20:03:57 +0100 Subject: [PATCH 1/3] Add ViewRenderer --- .../thirtyinch/viewmodel/ViewRenderer.java | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 thirtyinch/src/main/java/net/grandcentrix/thirtyinch/viewmodel/ViewRenderer.java diff --git a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/viewmodel/ViewRenderer.java b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/viewmodel/ViewRenderer.java new file mode 100644 index 00000000..ac07aeb5 --- /dev/null +++ b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/viewmodel/ViewRenderer.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2017 grandcentrix GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.grandcentrix.thirtyinch.viewmodel; + + +import net.grandcentrix.thirtyinch.TiLifecycleObserver; +import net.grandcentrix.thirtyinch.TiPresenter; +import net.grandcentrix.thirtyinch.TiView; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +/** + * Automatically calls the render function when the view model changes or a new view attaches. + * Works best with immutable view models because every call to {@link #setViewModel(Object)} + * triggers rendering. When you viewModel is mutable use {@link #notifyViewModelChanged()} to + * trigger a call to the render function. + *

+ * + * @param the type of the {@link TiView} + * @param type of the view model + */ +public final class ViewRenderer implements TiLifecycleObserver { + + public interface RenderFunc { + + /** + * render the view which should represent the virtualView, will be called on the ui thread + */ + void render(@NonNull final V view, @NonNull final VM viewModel); + } + + /** + * boolean flag indicating a rendering on the main thread is pending. It's therefore not + * required to call runOnUiThread(() -> performRender(true)); again because a runnable already + * got posted + */ + private boolean mIsRenderingPending = false; + + /** + * the last view model which got rendered. Used for checks with current view model, if both + * are equal, render shouldn't be called + */ + @Nullable + private VM mLastRenderedVirtualView = null; + + @NonNull + private final TiPresenter mPresenter; + + /** + * render() function executing the rendering of the view based on the view model + */ + private final RenderFunc mRenderBlock; + + /** + * Lock synchronizing write access on {@link #mViewModel}, {@link #mIsRenderingPending} and + * {@link #mLastRenderedVirtualView} + */ + private final Object mRenderingLock = new Object(); + + /** + * The 1:1 representation of the View as pojo will be used to automatically call + * render(Object, TiView) when the setter or {@link #notifyViewModelChanged()} gets + * called. + */ + @NonNull + private VM mViewModel; + + public ViewRenderer(@NonNull final TiPresenter presenter, @NonNull final VM + initialViewModel, final RenderFunc renderBlock) { + mPresenter = presenter; + mViewModel = initialViewModel; + mRenderBlock = renderBlock; + + mPresenter.addLifecycleObserver(this); + } + + @NonNull + public VM getViewModel() { + return mViewModel; + } + + /** + * It's recommended to use a immutable view model. In case you are using a mutable view model + * you can call this method to trigger a call of render(Object, TiView) + */ + public void notifyViewModelChanged() { + dispatchRender(true); + } + + @Override + public void onChange(final TiPresenter.State state, + final boolean hasLifecycleMethodBeenCalled) { + + if (state == TiPresenter.State.VIEW_ATTACHED && hasLifecycleMethodBeenCalled) { + // after onAttachView(view) + dispatchRender(false); + } + + if (state == TiPresenter.State.VIEW_DETACHED && hasLifecycleMethodBeenCalled) { + // after onDetachView() + synchronized (mRenderingLock) { + // next attached view will be rendered again, we don't know if it is the same view + mLastRenderedVirtualView = null; + mIsRenderingPending = false; + } + } + } + + public void setViewModel(@NonNull final VM viewModel) { + synchronized (mRenderingLock) { + mViewModel = viewModel; + dispatchRender(false); + } + } + + /** + * schedules a call to {@link #performRender(boolean)} on the ui thread, skips when a call to + * render is already pending + * + * @param force when true forces a call to render() even when the view model object did not + * change + */ + private void dispatchRender(final boolean force) { + synchronized (mRenderingLock) { + if (!mIsRenderingPending || force) { + mIsRenderingPending = true; + // no render operation posted, dispatch render on the UI thread + mPresenter.runOnUiThread(new Runnable() { + @Override + public void run() { + performRender(force); + } + }); + } + } + } + + /** + * calls render(VM, V) when the view model wasn't already rendered to the view + * + * @param force forces a render(Object, TiView) call even when the view model hasn't changed + */ + private void performRender(final boolean force) { + + synchronized (mRenderingLock) { + try { + final V view = mPresenter.getView(); + if (view == null) { + // no view to render to + return; + } + + final VM newViewModel = mViewModel; + final VM lastRenderedViewModel = mLastRenderedVirtualView; + + if (!force) { + if (newViewModel.equals(lastRenderedViewModel)) { + // render not required, virtual view hasn't changed + return; + } + } + + mRenderBlock.render(view, newViewModel); + mLastRenderedVirtualView = newViewModel; + } finally { + mIsRenderingPending = false; + } + } + } +} From 340f53b50218a25e85a9afbdf8a9cae7fed6b2ce Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Mon, 6 Feb 2017 16:43:45 +0100 Subject: [PATCH 2/3] Allow extension for special Renderer with specialized render() method --- .../net/grandcentrix/thirtyinch/viewmodel/ViewRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/viewmodel/ViewRenderer.java b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/viewmodel/ViewRenderer.java index ac07aeb5..69cb5fea 100644 --- a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/viewmodel/ViewRenderer.java +++ b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/viewmodel/ViewRenderer.java @@ -33,7 +33,7 @@ * @param the type of the {@link TiView} * @param type of the view model */ -public final class ViewRenderer implements TiLifecycleObserver { +public class ViewRenderer implements TiLifecycleObserver { public interface RenderFunc { From 608f2020c763d296c5c1aeb1ff51e2c1afb6b286 Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Wed, 3 May 2017 00:34:54 +0200 Subject: [PATCH 3/3] =?UTF-8?q?Preserve=20the=20force=20attribute=20for=20?= =?UTF-8?q?the=20next=20render=20call=20when=20render=20isn=E2=80=99t=20po?= =?UTF-8?q?ssible?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thirtyinch/viewmodel/ViewRenderer.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/viewmodel/ViewRenderer.java b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/viewmodel/ViewRenderer.java index 69cb5fea..40cdb40b 100644 --- a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/viewmodel/ViewRenderer.java +++ b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/viewmodel/ViewRenderer.java @@ -43,6 +43,12 @@ public interface RenderFunc { void render(@NonNull final V view, @NonNull final VM viewModel); } + /** + * flag if the next render should be a forced render. True when rendering is not possible to + * save the force property for the next render call + */ + private boolean mForce = false; + /** * boolean flag indicating a rendering on the main thread is pending. It's therefore not * required to call runOnUiThread(() -> performRender(true)); again because a runnable already @@ -107,7 +113,7 @@ public void onChange(final TiPresenter.State state, if (state == TiPresenter.State.VIEW_ATTACHED && hasLifecycleMethodBeenCalled) { // after onAttachView(view) - dispatchRender(false); + dispatchRender(mForce); } if (state == TiPresenter.State.VIEW_DETACHED && hasLifecycleMethodBeenCalled) { @@ -138,6 +144,14 @@ private void dispatchRender(final boolean force) { synchronized (mRenderingLock) { if (!mIsRenderingPending || force) { mIsRenderingPending = true; + if (mPresenter.getView() == null) { + // can't execute it now, no executor + if (force) { + // save force rendering for next attach event + mForce = true; + } + return; + } // no render operation posted, dispatch render on the UI thread mPresenter.runOnUiThread(new Runnable() { @Override @@ -172,6 +186,8 @@ private void performRender(final boolean force) { // render not required, virtual view hasn't changed return; } + } else { + mForce = false; } mRenderBlock.render(view, newViewModel);