3

我正在尝试将 Acticity + Fragments 应用程序转换为 Flow + Mortar + Dagger2

我想在从一个屏幕跳到另一个屏幕(至少向后)时保存和恢复屏幕状态。首选/推荐的方法是什么?

我花了很多时间查看流程和砂浆自述文件和示例,但无法弄清楚(文档和示例很少,只处理简单/静态/独特的数据)。

例如,您有一个类似浏览器的应用程序,它从一个页面移动到另一个页面,其中每个页面使用相同的 PageView 类、相同的 PagePresenter 类,但根据用户键入的 url 字符串具有不同的动态内容

使用 Dagger2(编译类型注释)通过注入来保存/恢复状态非常复杂/困难,对吧?(这将需要一个复杂的父/缓存结构)

我用谷歌搜索了一下,偶然发现: https ://github.com/lukaspili/flow-navigation

然而,这主要是一个实验......如果可能的话,我宁愿将我的生产赚钱应用程序建立在官方/可靠/测试/支持/由 square 支持的解决方案上

我还看了:

1) https://github.com/square/flow/issues/11 但是该示例在具有不同视图类的 2 个屏幕中使用 Dagger 注入数据(不是有效答案)

2)https://github.com/square/flow/issues/76(没有给出答案)

3) Mortar / Flow 在后台保存视图和演示者

我也看到了这一点:我们现在在内部通过简单地向我们的屏幕对象添加一个可变字段来执行此操作:

public void setViewState(SparseArray<Parcelable> viewState) {
this.viewState = viewState;
}

public void restoreHierarchyState(View view) {
view.restoreHierarchyState(viewState);
}

当一个 View 被换出时,我们获取它的实例状态并将其设置在屏幕对象上(它已经在 backstack 上)。在将其推广到库之前,我们将使用这种模式一段时间。

但是流动样本和砂浆样本都没有使用这个解决方案(他们使用 dagger2 来注入列表......再次不是有效的解决方案)

那么,在砂浆+流(+ dagger2)应用程序中恢复/保存屏幕状态的最新最佳/推荐方式是什么?

4

3 回答 3

14

首先是关于 Flow & Flow-path 的一些事实

  1. Flow 会保存前一个视图的视图状态,如果之前保存过,会尝试恢复新视图的状态。
    我所说的视图状态是指完全独立于 Flow 的 android 视图状态。它不会保存与前一个屏幕关联的 Mortar 范围。
    您复制粘贴的代码片段已经在 Flow 中实现,并且完全按照我上面所说的进行。

  2. 使用 Flow-path,定义如何从屏幕 A 到屏幕 B、如何动画化从 A 到 B 的视图转换以及如何设置/销毁PathContextA 和 B 的包装器的逻辑在一个PathContainer类中定义。

  3. PathContext是 Flow-path 中的一个类,它设置与 Screen 及其 View 关联的上下文(它是 的包装器android.content.Context,与 Mortar 上下文包装器的工作方式相同)。您通常还会有一个PathContextFactory由 调用的自定义,PathContainer它设置与屏幕关联的 Mortar 范围和PathContext.

  4. Flow-path 不提供PathContainer. 唯一的一个是SimplePathContainer示例项目中的 。
    如果您查看 的源代码SimplePathContainer,您会发现它破坏了与前一个屏幕关联的路径上下文。通过破坏它的上下文,它也破坏了它的 Mortar 范围,以及其中的所有内容,例如保存 ViewPresenter 实例的 Dagger2 组件。

  5. 如果你想保留前一个屏幕的 Mortar 作用域,唯一的方法是编写你自己的实现,PathContainer它不会破坏历史中以前的作用域。这基本上就是 Flow-navigation ( https://github.com/lukaspili/flow-navigation )。

  6. StateParceler用于在/从 中保存/恢复流历史堆栈Bundle。正如您所说,它的目的是使历史在配置更改和应用程序进程终止后仍然存在。
    但是,通过正确的 Mortar 配置,Mortar 范围不会在配置更改期间被破坏,因此,您不需要保存/恢复 ViewPresenter 实例,因为这些实例不会被破坏(仅视图)。不过,您仍然必须这样做才能杀死进程。

现在我的 2cents:

Flow-navigation 是历史上第一个不破坏先前屏幕的迫击炮范围的概念证明(backstack)。
从那以后,我从头开始编写了一个替代 Flow 库,它处理导航、管理 Mortar 范围的历史并以更适合我的需求的解耦方式提供视图转换:https ://github.com/lukaspili/Mortar-architect

因为您正在寻找 Square 支持和支持的解决方案,所以这对您不起作用。但是,我邀请您查看源代码,这可能会让您了解如何编写自己的代码PathContainer来保留 Flow 历史记录中的 Mortar 范围。

于 2015-07-07T21:04:44.307 回答
2

经过一番试验后,我在我的 Path 对象中添加了一些可变字段。

按照设计,一个迫击炮/Flow 应用程序使用 StateParceler 序列化/反序列化这些路径对象到/从 Bundles 和 Parcels 以保存和恢复视图状态

通过让 StateParceler 处理这些可变字段,它们能够在方向变化和反向导航中幸存下来。

通过使 StateParceler 还能够将这些可变字段序列化/反序列化到持久存储(例如 JSon 和 SharedPreferences),完整的历史记录可以在断电/不同的应用程序会话中存活

square 的示例使用 GsonParceler,它能够对大多数对象进行开箱即用的操作。您只需要编写一些代码,使其能够使用泛型/接口处理集合和复杂对象...

于 2015-06-29T18:51:46.463 回答
0

根据@lukas 的回答和他的图书馆flow-navigation,我意识到破坏的违规调用MortarScope是这一行:

oldPath.destroyNotIn(context, contextFactory);

所以我用

  public static PathContext create(PathContext previousContext, Path newPath, PathContextFactory factory) {
    if(newPath == Path.ROOT) {
      throw new IllegalArgumentException("Path is empty.");
    }
    List<Path> newPathElements = newPath.elements();
    Map<Path, Context> newContextChain = new LinkedHashMap<>();
    // We walk down the elements, reusing existing contexts for the elements we encounter.  As soon
    // as we encounter an element that doesn't already have a context, we stop.
    // Note: we will always have at least one shared element, the root.
    Context baseContext = null;
    Iterator<Path> pathIterator = newPathElements.iterator();
    Iterator<Path> basePathIterator = previousContext.path.elements().iterator();
    Log.d("PathContext", ":: Creating Context to [" + ((BasePath) newPath).getScopeName() + "]");
    while(pathIterator.hasNext() && basePathIterator.hasNext()) {
      Path element = pathIterator.next();
      Path basePathElement = basePathIterator.next();
      if(basePathElement.equals(element)) {
        if(!element.isRoot()) {
          Log.d("PathContext",
                  "Matched new Path to old Path [" + ((BasePath) element).getScopeName() + "], preserving context.");
        } else {
          Log.d("PathContext", "Matched new Path to old Path [ROOT], preserving context.");
        }

        baseContext = previousContext.contexts.get(element);
        newContextChain.put(element, baseContext);
      } else {
        if(!basePathElement.isRoot() && !element.isRoot()) {
          Log.d("PathContext",
                  "No match from [" + ((BasePath) basePathElement).getScopeName() + "] to [" + ((BasePath) element)
                          .getScopeName() + "] , creating new context.");
        } else {
          Log.d("PathContext",
                  "No match from ROOT [" + basePathElement + "] to ROOT [" + element + "] , creating new context.");
        }

        baseContext = factory.setUpContext(element, baseContext);
        newContextChain.put(element, baseContext);
        break;
      }
    }
    // Now we continue walking our new path, creating contexts as we go in case they don't exist.
    while(pathIterator.hasNext()) {
      Path element = pathIterator.next();
      if(!element.isRoot()) {
        Log.d("PathContext", "Creating new path [" + ((BasePath) element).getScopeName() + "].");
      } else {
        Log.d("PathContext", "Creating new path [ROOT].");
      }
      baseContext = factory.setUpContext(element, baseContext);
      newContextChain.put(element, baseContext);
    }
    // Finally, we can construct our new PathContext
    return new PathContext(baseContext, newPath, newContextChain);
  }

  /**
   * Finds the tail of this path which is not in the given path, and destroys it.
   */
  public void destroyNotIn(PathContext path, PathContextFactory factory) {
    Iterator<Path> aElements = this.path.elements().iterator();
    Iterator<Path> bElements = path.path.elements().iterator();
    while(aElements.hasNext() && bElements.hasNext()) {
      Path aElement = aElements.next();
      Path bElement = bElements.next();
      if(!aElement.equals(bElement)) {
        BasePath aBasePath = (BasePath) aElement;
        BasePath bBasePath = (BasePath) bElement;
        Log.d(toString(),
                "Destroying [" + aBasePath.getScopeName() + "] on matching with [" + bBasePath.getScopeName() + "]");
        factory.tearDownContext(contexts.get(aElement));
        break;
      }
    }
    while(aElements.hasNext()) {
      Path aElement = aElements.next();
      BasePath aBasePath = (BasePath) aElement;
      Log.d(toString(), "Destroying [" + aBasePath.getScopeName() + "] as it is not found in [" + path + "]");
      factory.tearDownContext(contexts.get(aElement));
    }
  } 

BasePath只是返回getScopeName()。它本质上就像BluePrintSquare 删除的那样。

还有,SimplePathContainer之前的都毁了,所以我也修改了。

/**
 * Provides basic right-to-left transitions. Saves and restores view state.
 * Uses {@link PathContext} to allow customized sub-containers.
 */
public class SimplePathContainer
        extends PathContainer {
    private static final String TAG = "SimplePathContainer";

    private final PathContextFactory contextFactory;

    public SimplePathContainer(int tagKey, PathContextFactory contextFactory) {
        super(tagKey);
        this.contextFactory = contextFactory;
    }

    @Override
    protected void performTraversal(final ViewGroup containerView, Flow.Traversal traversal, final PathContainer.TraversalState traversalState, final Flow.Direction direction, final Flow.TraversalCallback callback) {
        final PathContext oldPathContext;
        final PathContext newPathContext;
        if(containerView.getChildCount() > 0) {
            Log.d(TAG,
                    "Container View Child count was > 0, using view context of path [" + PathContext.get(containerView.getChildAt(
                            0).getContext()).path + "]");
            oldPathContext = PathContext.get(containerView.getChildAt(0).getContext());
        } else {
            Log.d(TAG, "Container View Child Count was == 0, using root context");
            oldPathContext = PathContext.root(containerView.getContext());
        }
        Log.d(TAG, "Previous Path [" + oldPathContext.path + "]");
        final Path to = traversalState.toPath();
        Log.d(TAG, "New Path [" + to + "]");

        View newView;
        newPathContext = PathContext.create(oldPathContext, to, contextFactory);

        int layout = ((BasePath) to).getLayout(); //removed annotation
        newView = LayoutInflater.from(newPathContext.getApplicationContext()) //fixed first path error
                .cloneInContext(newPathContext)
                .inflate(layout, containerView, false);
        View fromView = null;
        if(traversalState.fromPath() != null) {
            fromView = containerView.getChildAt(0);
            traversalState.saveViewState(fromView);
        }
        traversalState.restoreViewState(newView);

        if(fromView == null || direction == REPLACE) {
            containerView.removeAllViews();
            containerView.addView(newView);
            oldPathContext.destroyNotIn(newPathContext, contextFactory);
            callback.onTraversalCompleted();
        } else {
            final View finalFromView = fromView;
            if(direction == Flow.Direction.BACKWARD) {
                containerView.removeView(fromView);
                containerView.addView(newView);
                containerView.addView(finalFromView);
            } else {
                containerView.addView(newView);
            }
            ViewUtils.waitForMeasure(newView, new ViewUtils.OnMeasuredCallback() {
                @Override
                public void onMeasured(View view, int width, int height) {
                    runAnimation(containerView, finalFromView, view, direction, new Flow.TraversalCallback() {
                        @Override
                        public void onTraversalCompleted() {
                            containerView.removeView(finalFromView);
                            oldPathContext.destroyNotIn(newPathContext, contextFactory);
                            callback.onTraversalCompleted();
                        }
                    }, (BasePath) Path.get(oldPathContext), (BasePath) to);
                }
            });
        }
    }

    private void runAnimation(final ViewGroup container, final View from, final View to, Flow.Direction direction, final Flow.TraversalCallback callback, BasePath fromPath, BasePath toPath) {
        Animator animator = createSegue(from, to, direction);

        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                container.removeView(from);
                callback.onTraversalCompleted();
            }
        });
        animator.start();
    }

    private Animator createSegue(View from, View to, Flow.Direction direction) {
        boolean backward = direction == Flow.Direction.BACKWARD;
        int fromTranslation = backward ? from.getWidth() : -from.getWidth();
        int toTranslation = backward ? -to.getWidth() : to.getWidth();

        AnimatorSet set = new AnimatorSet();

        set.play(ObjectAnimator.ofFloat(from, View.TRANSLATION_X, fromTranslation));
        set.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, toTranslation, 0));

        return set;
    }
}
于 2015-11-03T15:54:01.980 回答