Resolved: Cross platform way to clear history #283

This commit is contained in:
Rossen Hristov
2015-09-25 11:34:17 +03:00
parent 84b3eb545d
commit f75922102a
7 changed files with 220 additions and 43 deletions

View File

@@ -1,5 +1,4 @@
import application = require("application");
import frameModule = require("ui/frame");
import navPageModule = require("../nav-page");
import trace = require("trace");
@@ -7,14 +6,13 @@ trace.enable();
trace.setCategories(trace.categories.concat(
trace.categories.NativeLifecycle
, trace.categories.Navigation
, trace.categories.ViewHierarchy
, trace.categories.VisualTreeEvents
));
));
application.onLaunch = function (context) {
var frame = new frameModule.Frame();
var pageFactory = function () {
application.mainEntry = {
create: function () {
return new navPageModule.NavPage(0);
};
frame.navigate(pageFactory);
}
}
//backstackVisible: false,
//clearHistory: true
};
application.start();

View File

@@ -6,11 +6,14 @@ import labelModule = require("ui/label");
import buttonModule = require("ui/button");
import textFieldModule = require("ui/text-field");
import enums = require("ui/enums");
import switchModule = require("ui/switch");
export class NavPage extends pagesModule.Page implements definition.ControlsPage {
constructor(id: number) {
super();
this.id = "NavPage " + id;
var stackLayout = new stackLayoutModule.StackLayout();
stackLayout.orientation = enums.Orientation.vertical;
@@ -21,6 +24,11 @@ export class NavPage extends pagesModule.Page implements definition.ControlsPage
});
stackLayout.addChild(goBackButton);
this.on(pagesModule.Page.navigatedToEvent, function () {
//console.log("Navigated to NavPage " + id + "; backStack.length: " + frameModule.topmost().backStack.length);
goBackButton.isEnabled = frameModule.topmost().canGoBack();
});
var stateLabel = new labelModule.Label();
stateLabel.text = "NavPage " + id;
stackLayout.addChild(stateLabel);
@@ -39,13 +47,37 @@ export class NavPage extends pagesModule.Page implements definition.ControlsPage
});
stackLayout.addChild(changeStateButton);
var optionsLayout = new stackLayoutModule.StackLayout();
var addToBackStackLabel = new labelModule.Label();
addToBackStackLabel.text = "backStackVisible";
optionsLayout.addChild(addToBackStackLabel);
var addToBackStackSwitch = new switchModule.Switch();
addToBackStackSwitch.checked = true;
optionsLayout.addChild(addToBackStackSwitch);
var clearHistoryLabel = new labelModule.Label();
clearHistoryLabel.text = "clearHistory";
optionsLayout.addChild(clearHistoryLabel);
var clearHistorySwitch = new switchModule.Switch();
clearHistorySwitch.checked = false;
optionsLayout.addChild(clearHistorySwitch);
stackLayout.addChild(optionsLayout);
var forwardButton = new buttonModule.Button();
forwardButton.text = "->";
forwardButton.on(buttonModule.Button.tapEvent, function () {
var pageFactory = function () {
return new NavPage(id + 1);
};
frameModule.topmost().navigate(pageFactory);
frameModule.topmost().navigate({
create: pageFactory,
backstackVisible: addToBackStackSwitch.checked,
clearHistory: clearHistorySwitch.checked
});
});
stackLayout.addChild(forwardButton);

View File

@@ -29,4 +29,37 @@ export var test_backstackVisible = function() {
frame.topmost().goBack();
TKUnit.waitUntilReady(() => { return frame.topmost().currentPage === mainTestPage; });
}
}
// Clearing the history messes up the tests app.
//export var test_ClearHistory = function () {
// var pageFactory = function (): pageModule.Page {
// return new pageModule.Page();
// };
// var mainTestPage = frame.topmost().currentPage;
// var currentPage: pageModule.Page;
// currentPage = frame.topmost().currentPage;
// frame.topmost().navigate({ create: pageFactory });
// TKUnit.waitUntilReady(() => { return frame.topmost().currentPage !== currentPage; });
// currentPage = frame.topmost().currentPage;
// frame.topmost().navigate({ create: pageFactory });
// TKUnit.waitUntilReady(() => { return frame.topmost().currentPage !== currentPage; });
// currentPage = frame.topmost().currentPage;
// frame.topmost().navigate({ create: pageFactory });
// TKUnit.waitUntilReady(() => { return frame.topmost().currentPage !== currentPage; });
// TKUnit.assert(frame.topmost().canGoBack(), "Frame should be able to go back.");
// TKUnit.assert(frame.topmost().backStack.length === 3, "Back stack should have 3 entries.");
// // Navigate with clear history.
// currentPage = frame.topmost().currentPage;
// frame.topmost().navigate({ create: pageFactory, clearHistory: true });
// TKUnit.waitUntilReady(() => { return frame.topmost().currentPage !== currentPage; });
// TKUnit.assert(!frame.topmost().canGoBack(), "Frame should NOT be able to go back.");
// TKUnit.assert(frame.topmost().backStack.length === 0, "Back stack should have 0 entries.");
//}

View File

@@ -184,7 +184,8 @@ export class Frame extends view.CustomLayoutView implements definition.Frame {
var entry = this._navigationQueue[0].entry;
var currentNavigationPage = entry.resolvedPage;
if (page !== currentNavigationPage) {
throw new Error("Corrupted navigation stack.");
console.trace();
throw new Error(`Corrupted navigation stack; page: ${page.id}; currentNavigationPage: ${currentNavigationPage.id}`);
}
// remove completed operation.
@@ -224,7 +225,10 @@ export class Frame extends view.CustomLayoutView implements definition.Frame {
var navContext = navigationContext.entry;
this._onNavigatingTo(navContext);
if (this._isEntryBackstackVisible(this._currentEntry)) {
if (navigationContext.entry.entry.clearHistory) {
this._backStack.length = 0;
}
else if (this._isEntryBackstackVisible(this._currentEntry)) {
this._backStack.push(this._currentEntry);
}

View File

@@ -15,6 +15,7 @@ var INTENT_EXTRA = "com.tns.activity";
var ANDROID_FRAME = "android_frame";
var BACKSTACK_TAG = "_backstackTag";
var NAV_DEPTH = "_navDepth";
var CLEARING_HISTORY = "_clearingHistory";
var navDepth = -1;
@@ -32,17 +33,23 @@ class PageFragmentBody extends android.app.Fragment {
onAttach(activity: android.app.Activity) {
super.onAttach(activity);
trace.write(this.getTag() + ".onAttach();", trace.categories.NativeLifecycle);
trace.write(this.toString() + ".onAttach();", trace.categories.NativeLifecycle);
}
onCreate(savedInstanceState: android.os.Bundle) {
super.onCreate(savedInstanceState);
trace.write(this.getTag() + ".onCreate(); savedInstanceState: " + savedInstanceState, trace.categories.NativeLifecycle);
trace.write(this.toString() + ".onCreate(); savedInstanceState: " + savedInstanceState, trace.categories.NativeLifecycle);
super.setHasOptionsMenu(true);
}
onCreateView(inflater: android.view.LayoutInflater, container: android.view.ViewGroup, savedInstanceState: android.os.Bundle): android.view.View {
trace.write(this.getTag() + ".onCreateView(); container: " + container + "; savedInstanceState: " + savedInstanceState, trace.categories.NativeLifecycle);
trace.write(this.toString() + ".onCreateView(); container: " + container + "; savedInstanceState: " + savedInstanceState, trace.categories.NativeLifecycle);
if (this[CLEARING_HISTORY]) {
trace.write(`${this.toString() } wants to create a view, but we are currently clearing history. Returning null.`, trace.categories.NativeLifecycle);
return null;
}
var entry: definition.BackstackEntry = this.entry;
var page: pages.Page = entry.resolvedPage;
@@ -58,13 +65,13 @@ class PageFragmentBody extends android.app.Fragment {
onFragmentShown(this);
}
trace.write(this.getTag() + ".onCreateView(); nativeView: " + page._nativeView, trace.categories.NativeLifecycle);
trace.write(this.toString() + ".onCreateView(); nativeView: " + page._nativeView, trace.categories.NativeLifecycle);
return page._nativeView;
}
onHiddenChanged(hidden: boolean) {
super.onHiddenChanged(hidden);
trace.write(this.getTag() + ".onHiddenChanged(); hidden: " + hidden, trace.categories.NativeLifecycle);
trace.write(this.toString() + ".onHiddenChanged(); hidden: " + hidden, trace.categories.NativeLifecycle);
if (hidden) {
onFragmentHidden(this);
@@ -76,12 +83,12 @@ class PageFragmentBody extends android.app.Fragment {
onActivityCreated(savedInstanceState: android.os.Bundle) {
super.onActivityCreated(savedInstanceState);
trace.write(this.getTag() + ".onActivityCreated(); savedInstanceState: " + savedInstanceState, trace.categories.NativeLifecycle);
trace.write(this.toString() + ".onActivityCreated(); savedInstanceState: " + savedInstanceState, trace.categories.NativeLifecycle);
}
onSaveInstanceState(outState: android.os.Bundle) {
super.onSaveInstanceState(outState);
trace.write(this.getTag() + ".onSaveInstanceState();", trace.categories.NativeLifecycle);
trace.write(this.toString() + ".onSaveInstanceState();", trace.categories.NativeLifecycle);
if (this.isHidden()) {
outState.putBoolean(HIDDEN, true);
@@ -90,39 +97,39 @@ class PageFragmentBody extends android.app.Fragment {
onViewStateRestored(savedInstanceState: android.os.Bundle) {
super.onViewStateRestored(savedInstanceState);
trace.write(this.getTag() + ".onViewStateRestored(); savedInstanceState: " + savedInstanceState, trace.categories.NativeLifecycle);
trace.write(this.toString() + ".onViewStateRestored(); savedInstanceState: " + savedInstanceState, trace.categories.NativeLifecycle);
}
onStart() {
super.onStart();
trace.write(this.getTag() + ".onStart();", trace.categories.NativeLifecycle);
trace.write(this.toString() + ".onStart();", trace.categories.NativeLifecycle);
}
onResume() {
super.onResume();
trace.write(this.getTag() + ".onResume();", trace.categories.NativeLifecycle);
trace.write(this.toString() + ".onResume();", trace.categories.NativeLifecycle);
}
onPause() {
super.onPause();
trace.write(this.getTag() + ".onPause();", trace.categories.NativeLifecycle);
trace.write(this.toString() + ".onPause();", trace.categories.NativeLifecycle);
}
onStop() {
super.onStop();
trace.write(this.getTag() + ".onStop();", trace.categories.NativeLifecycle);
trace.write(this.toString() + ".onStop();", trace.categories.NativeLifecycle);
}
onDestroyView() {
super.onDestroyView();
trace.write(this.getTag() + ".onDestroyView();", trace.categories.NativeLifecycle);
trace.write(this.toString() + ".onDestroyView();", trace.categories.NativeLifecycle);
onFragmentHidden(this);
}
onDestroy() {
super.onDestroy();
trace.write(this.getTag() + ".onDestroy();", trace.categories.NativeLifecycle);
trace.write(this.toString() + ".onDestroy();", trace.categories.NativeLifecycle);
// Explicitly free resources to allow Java Garbage collector to release resources associated with JavaScript implementations - e.g. large image files.
// Although we hint the V8 with the externally allocated memory, synchronization between the two GCs is not deterministic without an explicit call.
@@ -132,11 +139,20 @@ class PageFragmentBody extends android.app.Fragment {
onDetach() {
super.onDetach();
trace.write(this.getTag() + ".onDetach();", trace.categories.NativeLifecycle);
trace.write(this.toString() + ".onDetach();", trace.categories.NativeLifecycle);
}
public toString() {
return `${this.getTag() }[${this.entry.resolvedPage.id}]`;
}
}
function onFragmentShown(fragment: PageFragmentBody) {
if (fragment[CLEARING_HISTORY]) {
trace.write(`${fragment.toString() } has been shown, but we are currently clearing history. Returning.`, trace.categories.NativeLifecycle);
return null;
}
// TODO: consider putting entry and page in queue so we can safely extract them here. Pass the index of current navigation and extract it from here.
// After extracting navigation info - remove this index from navigation stack.
var frame = fragment.frame;
@@ -152,6 +168,11 @@ function onFragmentShown(fragment: PageFragmentBody) {
}
function onFragmentHidden(fragment: PageFragmentBody) {
if (fragment[CLEARING_HISTORY]) {
trace.write(`${fragment.toString() } has been hidden, but we are currently clearing history. Returning.`, trace.categories.NativeLifecycle);
return null;
}
var entry: definition.BackstackEntry = fragment.entry;
var page: pages.Page = entry.resolvedPage;
// This might be a second call if the fragment is hidden and then destroyed.
@@ -193,6 +214,11 @@ export class Frame extends frameCommon.Frame {
}
public _navigateCore(backstackEntry: definition.BackstackEntry) {
trace.write(`_navigateCore; id: ${backstackEntry.resolvedPage.id}; backstackVisible: ${this._isEntryBackstackVisible(backstackEntry)}; clearHistory: ${backstackEntry.entry.clearHistory};`, trace.categories.Navigation);
//this._printFrameBackStack();
//this._printNativeBackStack();
var activity = this._android.activity;
if (!activity) {
// We do not have an Activity yet associated. In this case we have two execution paths:
@@ -207,9 +233,36 @@ export class Frame extends frameCommon.Frame {
return;
}
var manager = activity.getFragmentManager();
// Clear history
if (backstackEntry.entry.clearHistory && !this._isFirstNavigation) {
var i = manager.getBackStackEntryCount() - 1;
var fragment: android.app.Fragment;
while (i >= 0) {
fragment = manager.findFragmentByTag(manager.getBackStackEntryAt(i--).getName());
trace.write(`${fragment.toString()}[CLEARING_HISTORY] = true;`, trace.categories.NativeLifecycle);
fragment[CLEARING_HISTORY] = true;
}
// Remember that the current fragment has never been added to the backStack, so mark it as well.
if (this.currentPage) {
fragment = manager.findFragmentByTag(this.currentPage[TAG]);
if (fragment) {
fragment[CLEARING_HISTORY] = true;
trace.write(`${fragment.toString() }[CLEARING_HISTORY] = true;`, trace.categories.NativeLifecycle);
}
}
var firstEntryName = manager.getBackStackEntryAt(0).getName();
trace.write(`manager.popBackStack(${firstEntryName}, android.app.FragmentManager.POP_BACK_STACK_INCLUSIVE);`, trace.categories.NativeLifecycle);
manager.popBackStack(firstEntryName, android.app.FragmentManager.POP_BACK_STACK_INCLUSIVE);
this._currentEntry = null;
navDepth = -1;
}
navDepth++;
var manager = activity.getFragmentManager();
var fragmentTransaction = manager.beginTransaction();
var newFragmentTag = "fragment" + navDepth;
@@ -227,7 +280,7 @@ export class Frame extends frameCommon.Frame {
trace.write("fragmentTransaction.add(" + this.containerViewId + ", " + newFragment + ", " + newFragmentTag + ");", trace.categories.NativeLifecycle);
}
else {
if (this.android.cachePagesOnNavigate) {
if (this.android.cachePagesOnNavigate && !backstackEntry.entry.clearHistory) {
var currentFragmentTag = this.currentPage[TAG];
var currentFragment = manager.findFragmentByTag(currentFragmentTag);
if (currentFragment) {
@@ -246,7 +299,10 @@ export class Frame extends frameCommon.Frame {
trace.write("fragmentTransaction.replace(" + this.containerViewId + ", " + newFragment + ", " + newFragmentTag + ");", trace.categories.NativeLifecycle);
}
if (this.backStack.length > 0) {
// Add to backStack if needed.
if (this.backStack.length > 0 &&
this._currentEntry &&
this._isEntryBackstackVisible(this._currentEntry)) {
// We add each entry in the backstack to avoid the "Stack corrupted" mismatch
var backstackTag = this._currentEntry[BACKSTACK_TAG];
fragmentTransaction.addToBackStack(backstackTag);
@@ -271,6 +327,11 @@ export class Frame extends frameCommon.Frame {
fragmentTransaction.commit();
trace.write("fragmentTransaction.commit();", trace.categories.NativeLifecycle);
//setTimeout(() => {
// this._printFrameBackStack();
// this._printNativeBackStack();
//}, 100);
}
public _goBackCore(backstackEntry: definition.BackstackEntry) {
@@ -323,6 +384,32 @@ export class Frame extends frameCommon.Frame {
public _clearAndroidReference() {
// we should keep the reference to underlying native object, since frame can contain many pages.
}
public _printNativeBackStack() {
if (!this._android.activity) {
return;
}
var manager = this._android.activity.getFragmentManager();
var length = manager.getBackStackEntryCount();
var i = length - 1;
console.log("---------------------------");
console.log("Fragment Manager Back Stack (" + length + ")");
while (i >= 0) {
var fragment = <PageFragmentBody>manager.findFragmentByTag(manager.getBackStackEntryAt(i--).getName());
console.log("[ " + fragment.toString() + " ]");
}
}
public _printFrameBackStack() {
var length = this.backStack.length;
var i = length - 1;
console.log("---------------------------");
console.log("Frame Back Stack (" + length + ")");
while (i >= 0) {
var backstackEntry = <definition.BackstackEntry>this.backStack[i--];
console.log("[ " + backstackEntry.resolvedPage.id + " ]");
}
}
}
var NativeActivity = {

5
ui/frame/frame.d.ts vendored
View File

@@ -151,6 +151,11 @@ declare module "ui/frame" {
* If the parameter is set to false then the Page will be displayed but once navigated from it will not be able to be navigated back to.
*/
backstackVisible?: boolean;
/**
* True to clear the navigation history, false otherwise. Very useful when navigating away from login pages.
*/
clearHistory?: boolean;
}
/**

View File

@@ -45,7 +45,7 @@ export class Frame extends frameCommon.Frame {
}
public _navigateCore(backstackEntry: definition.BackstackEntry) {
var viewController = backstackEntry.resolvedPage.ios;
var viewController: UIViewController = backstackEntry.resolvedPage.ios;
if (!viewController) {
throw new Error("Required page does have an viewController created.");
}
@@ -63,30 +63,48 @@ export class Frame extends frameCommon.Frame {
this._updateActionBar(backstackEntry.resolvedPage);
if (this._currentEntry && !this._isEntryBackstackVisible(this._currentEntry)) {
// First navigation.
if (!this._currentEntry) {
this._ios.controller.pushViewControllerAnimated(viewController, animated);
trace.write("Frame<" + this._domId + ">.pushViewControllerAnimated(newController) depth = " + navDepth, trace.categories.Navigation);
return;
}
// We should clear the entire history.
if (backstackEntry.entry.clearHistory) {
viewController.navigationItem.hidesBackButton = true;
var newControllers = NSMutableArray.alloc().initWithCapacity(1);
newControllers.addObject(viewController);
this._ios.controller.setViewControllersAnimated(newControllers, animated);
trace.write("Frame<" + this._domId + ">.setViewControllersAnimated([newController]) depth = " + navDepth, trace.categories.Navigation);
return;
}
// We should hide the current entry from the back stack.
if (!this._isEntryBackstackVisible(this._currentEntry)) {
var newControllers = NSMutableArray.alloc().initWithArray(this._ios.controller.viewControllers);
if (newControllers.count === 0) {
throw new Error("Wrong controllers count.");
}
var newController: UIViewController = backstackEntry.resolvedPage.ios;
// the code below fixes a phantom animation that appears on the Back button in this case
// TODO: investigate why the animation happens at first place before working around it
newController.navigationItem.hidesBackButton = this.backStack.length === 0;
viewController.navigationItem.hidesBackButton = this.backStack.length === 0;
// swap the top entry with the new one
newControllers.removeLastObject();
newControllers.addObject(newController);
newControllers.addObject(viewController);
// replace the controllers instead of pushing directly
this._ios.controller.setViewControllersAnimated(newControllers, animated);
trace.write("Frame<" + this._domId + ">.setViewControllersAnimated([originalControllers - lastController + newController]) depth = " + navDepth, trace.categories.Navigation);
return;
}
else {
this._ios.controller.pushViewControllerAnimated(viewController, animated);
}
trace.write("Frame<" + this._domId + ">.pushViewControllerAnimated depth = " + navDepth, trace.categories.Navigation);
// General case.
this._ios.controller.pushViewControllerAnimated(viewController, animated);
trace.write("Frame<" + this._domId + ">.pushViewControllerAnimated(newController) depth = " + navDepth, trace.categories.Navigation);
}
public _goBackCore(backstackEntry: definition.BackstackEntry) {