diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/404.html b/404.html new file mode 100644 index 00000000..3b0c254f --- /dev/null +++ b/404.html @@ -0,0 +1 @@ + Compose Navigation Reimagined
\ No newline at end of file diff --git a/animations/index.html b/animations/index.html new file mode 100644 index 00000000..80cb335f --- /dev/null +++ b/animations/index.html @@ -0,0 +1,15 @@ + Animations - Compose Navigation Reimagined
Skip to content

Animations

If you want to show animated transitions between destinations use AnimatedNavHost. The default transition is a simple crossfade, but you can granularly customize every transition with your own NavTransitionSpec implementation.

Here is one possible implementation of NavTransitionSpec:

val CustomTransitionSpec = NavTransitionSpec<Any?> { action, _, _ ->
+    val direction = if (action == NavAction.Pop) {
+        AnimatedContentTransitionScope.SlideDirection.End
+    } else {
+        AnimatedContentTransitionScope.SlideDirection.Start
+    }
+    slideIntoContainer(direction) togetherWith slideOutOfContainer(direction)
+}
+

Set it into AnimatedNavHost:

AnimatedNavHost(
+    controller = navController,
+    transitionSpec = CustomTransitionSpec
+) { destination ->
+    // ...
+}
+

and it'll end up looking like this:

In NavTransitionSpec you get the parameters:

  • action - a hint about the last NavController method that changed the backstack
  • from - a previous visible destination
  • to - a target visible destination

This information is plenty enough to choose a transition for every possible combination of destinations and navigation actions.

Tip

You can add more enter/exit animations to the composables through the AnimatedNavHostScope receiver of the contentSelector parameter. AnimatedNavHostScope gives you access to the current transition and to animateEnterExit modifier.

There are four default NavAction types:

  • Pop, Replace and Navigate - objects that correspond to pop…, replace…, navigate methods of NavController
  • Idle - the default action of a newly created NavController. You don't need to handle it in NavTransitionSpec.

You can also create new action types by implementing NavAction interface. Pass any object of the new type into setNewBackstack method of NavController and handle it in NavTransitionSpec.

The last action can also be accessed through action property of NavBackstack.

\ No newline at end of file diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 00000000..9b57f3e4 Binary files /dev/null and b/assets/images/favicon.png differ diff --git a/assets/images/logo.svg b/assets/images/logo.svg new file mode 100644 index 00000000..31857e9d --- /dev/null +++ b/assets/images/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/javascripts/bundle.220ee61c.min.js b/assets/javascripts/bundle.220ee61c.min.js new file mode 100644 index 00000000..116072a1 --- /dev/null +++ b/assets/javascripts/bundle.220ee61c.min.js @@ -0,0 +1,29 @@ +"use strict";(()=>{var Ci=Object.create;var gr=Object.defineProperty;var Ri=Object.getOwnPropertyDescriptor;var ki=Object.getOwnPropertyNames,Ht=Object.getOwnPropertySymbols,Hi=Object.getPrototypeOf,yr=Object.prototype.hasOwnProperty,nn=Object.prototype.propertyIsEnumerable;var rn=(e,t,r)=>t in e?gr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,P=(e,t)=>{for(var r in t||(t={}))yr.call(t,r)&&rn(e,r,t[r]);if(Ht)for(var r of Ht(t))nn.call(t,r)&&rn(e,r,t[r]);return e};var on=(e,t)=>{var r={};for(var n in e)yr.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&Ht)for(var n of Ht(e))t.indexOf(n)<0&&nn.call(e,n)&&(r[n]=e[n]);return r};var Pt=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Pi=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ki(t))!yr.call(e,o)&&o!==r&&gr(e,o,{get:()=>t[o],enumerable:!(n=Ri(t,o))||n.enumerable});return e};var yt=(e,t,r)=>(r=e!=null?Ci(Hi(e)):{},Pi(t||!e||!e.__esModule?gr(r,"default",{value:e,enumerable:!0}):r,e));var sn=Pt((xr,an)=>{(function(e,t){typeof xr=="object"&&typeof an!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(xr,function(){"use strict";function e(r){var n=!0,o=!1,i=null,s={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function a(O){return!!(O&&O!==document&&O.nodeName!=="HTML"&&O.nodeName!=="BODY"&&"classList"in O&&"contains"in O.classList)}function f(O){var Qe=O.type,De=O.tagName;return!!(De==="INPUT"&&s[Qe]&&!O.readOnly||De==="TEXTAREA"&&!O.readOnly||O.isContentEditable)}function c(O){O.classList.contains("focus-visible")||(O.classList.add("focus-visible"),O.setAttribute("data-focus-visible-added",""))}function u(O){O.hasAttribute("data-focus-visible-added")&&(O.classList.remove("focus-visible"),O.removeAttribute("data-focus-visible-added"))}function p(O){O.metaKey||O.altKey||O.ctrlKey||(a(r.activeElement)&&c(r.activeElement),n=!0)}function m(O){n=!1}function d(O){a(O.target)&&(n||f(O.target))&&c(O.target)}function h(O){a(O.target)&&(O.target.classList.contains("focus-visible")||O.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(O.target))}function v(O){document.visibilityState==="hidden"&&(o&&(n=!0),Y())}function Y(){document.addEventListener("mousemove",N),document.addEventListener("mousedown",N),document.addEventListener("mouseup",N),document.addEventListener("pointermove",N),document.addEventListener("pointerdown",N),document.addEventListener("pointerup",N),document.addEventListener("touchmove",N),document.addEventListener("touchstart",N),document.addEventListener("touchend",N)}function B(){document.removeEventListener("mousemove",N),document.removeEventListener("mousedown",N),document.removeEventListener("mouseup",N),document.removeEventListener("pointermove",N),document.removeEventListener("pointerdown",N),document.removeEventListener("pointerup",N),document.removeEventListener("touchmove",N),document.removeEventListener("touchstart",N),document.removeEventListener("touchend",N)}function N(O){O.target.nodeName&&O.target.nodeName.toLowerCase()==="html"||(n=!1,B())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",m,!0),document.addEventListener("pointerdown",m,!0),document.addEventListener("touchstart",m,!0),document.addEventListener("visibilitychange",v,!0),Y(),r.addEventListener("focus",d,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var cn=Pt(Er=>{(function(e){var t=function(){try{return!!Symbol.iterator}catch(c){return!1}},r=t(),n=function(c){var u={next:function(){var p=c.shift();return{done:p===void 0,value:p}}};return r&&(u[Symbol.iterator]=function(){return u}),u},o=function(c){return encodeURIComponent(c).replace(/%20/g,"+")},i=function(c){return decodeURIComponent(String(c).replace(/\+/g," "))},s=function(){var c=function(p){Object.defineProperty(this,"_entries",{writable:!0,value:{}});var m=typeof p;if(m!=="undefined")if(m==="string")p!==""&&this._fromString(p);else if(p instanceof c){var d=this;p.forEach(function(B,N){d.append(N,B)})}else if(p!==null&&m==="object")if(Object.prototype.toString.call(p)==="[object Array]")for(var h=0;hd[0]?1:0}),c._entries&&(c._entries={});for(var p=0;p1?i(d[1]):"")}})})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Er);(function(e){var t=function(){try{var o=new e.URL("b","http://a");return o.pathname="c d",o.href==="http://a/c%20d"&&o.searchParams}catch(i){return!1}},r=function(){var o=e.URL,i=function(f,c){typeof f!="string"&&(f=String(f)),c&&typeof c!="string"&&(c=String(c));var u=document,p;if(c&&(e.location===void 0||c!==e.location.href)){c=c.toLowerCase(),u=document.implementation.createHTMLDocument(""),p=u.createElement("base"),p.href=c,u.head.appendChild(p);try{if(p.href.indexOf(c)!==0)throw new Error(p.href)}catch(O){throw new Error("URL unable to set base "+c+" due to "+O)}}var m=u.createElement("a");m.href=f,p&&(u.body.appendChild(m),m.href=m.href);var d=u.createElement("input");if(d.type="url",d.value=f,m.protocol===":"||!/:/.test(m.href)||!d.checkValidity()&&!c)throw new TypeError("Invalid URL");Object.defineProperty(this,"_anchorElement",{value:m});var h=new e.URLSearchParams(this.search),v=!0,Y=!0,B=this;["append","delete","set"].forEach(function(O){var Qe=h[O];h[O]=function(){Qe.apply(h,arguments),v&&(Y=!1,B.search=h.toString(),Y=!0)}}),Object.defineProperty(this,"searchParams",{value:h,enumerable:!0});var N=void 0;Object.defineProperty(this,"_updateSearchParams",{enumerable:!1,configurable:!1,writable:!1,value:function(){this.search!==N&&(N=this.search,Y&&(v=!1,this.searchParams._fromString(this.search),v=!0))}})},s=i.prototype,a=function(f){Object.defineProperty(s,f,{get:function(){return this._anchorElement[f]},set:function(c){this._anchorElement[f]=c},enumerable:!0})};["hash","host","hostname","port","protocol"].forEach(function(f){a(f)}),Object.defineProperty(s,"search",{get:function(){return this._anchorElement.search},set:function(f){this._anchorElement.search=f,this._updateSearchParams()},enumerable:!0}),Object.defineProperties(s,{toString:{get:function(){var f=this;return function(){return f.href}}},href:{get:function(){return this._anchorElement.href.replace(/\?$/,"")},set:function(f){this._anchorElement.href=f,this._updateSearchParams()},enumerable:!0},pathname:{get:function(){return this._anchorElement.pathname.replace(/(^\/?)/,"/")},set:function(f){this._anchorElement.pathname=f},enumerable:!0},origin:{get:function(){var f={"http:":80,"https:":443,"ftp:":21}[this._anchorElement.protocol],c=this._anchorElement.port!=f&&this._anchorElement.port!=="";return this._anchorElement.protocol+"//"+this._anchorElement.hostname+(c?":"+this._anchorElement.port:"")},enumerable:!0},password:{get:function(){return""},set:function(f){},enumerable:!0},username:{get:function(){return""},set:function(f){},enumerable:!0}}),i.createObjectURL=function(f){return o.createObjectURL.apply(o,arguments)},i.revokeObjectURL=function(f){return o.revokeObjectURL.apply(o,arguments)},e.URL=i};if(t()||r(),e.location!==void 0&&!("origin"in e.location)){var n=function(){return e.location.protocol+"//"+e.location.hostname+(e.location.port?":"+e.location.port:"")};try{Object.defineProperty(e.location,"origin",{get:n,enumerable:!0})}catch(o){setInterval(function(){e.location.origin=n()},100)}}})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Er)});var qr=Pt((Mt,Nr)=>{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Mt=="object"&&typeof Nr=="object"?Nr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Mt=="object"?Mt.ClipboardJS=r():t.ClipboardJS=r()})(Mt,function(){return function(){var e={686:function(n,o,i){"use strict";i.d(o,{default:function(){return Ai}});var s=i(279),a=i.n(s),f=i(370),c=i.n(f),u=i(817),p=i.n(u);function m(j){try{return document.execCommand(j)}catch(T){return!1}}var d=function(T){var E=p()(T);return m("cut"),E},h=d;function v(j){var T=document.documentElement.getAttribute("dir")==="rtl",E=document.createElement("textarea");E.style.fontSize="12pt",E.style.border="0",E.style.padding="0",E.style.margin="0",E.style.position="absolute",E.style[T?"right":"left"]="-9999px";var H=window.pageYOffset||document.documentElement.scrollTop;return E.style.top="".concat(H,"px"),E.setAttribute("readonly",""),E.value=j,E}var Y=function(T,E){var H=v(T);E.container.appendChild(H);var I=p()(H);return m("copy"),H.remove(),I},B=function(T){var E=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},H="";return typeof T=="string"?H=Y(T,E):T instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(T==null?void 0:T.type)?H=Y(T.value,E):(H=p()(T),m("copy")),H},N=B;function O(j){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?O=function(E){return typeof E}:O=function(E){return E&&typeof Symbol=="function"&&E.constructor===Symbol&&E!==Symbol.prototype?"symbol":typeof E},O(j)}var Qe=function(){var T=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},E=T.action,H=E===void 0?"copy":E,I=T.container,q=T.target,Me=T.text;if(H!=="copy"&&H!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(q!==void 0)if(q&&O(q)==="object"&&q.nodeType===1){if(H==="copy"&&q.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(H==="cut"&&(q.hasAttribute("readonly")||q.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(Me)return N(Me,{container:I});if(q)return H==="cut"?h(q):N(q,{container:I})},De=Qe;function $e(j){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?$e=function(E){return typeof E}:$e=function(E){return E&&typeof Symbol=="function"&&E.constructor===Symbol&&E!==Symbol.prototype?"symbol":typeof E},$e(j)}function Ei(j,T){if(!(j instanceof T))throw new TypeError("Cannot call a class as a function")}function tn(j,T){for(var E=0;E0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof I.action=="function"?I.action:this.defaultAction,this.target=typeof I.target=="function"?I.target:this.defaultTarget,this.text=typeof I.text=="function"?I.text:this.defaultText,this.container=$e(I.container)==="object"?I.container:document.body}},{key:"listenClick",value:function(I){var q=this;this.listener=c()(I,"click",function(Me){return q.onClick(Me)})}},{key:"onClick",value:function(I){var q=I.delegateTarget||I.currentTarget,Me=this.action(q)||"copy",kt=De({action:Me,container:this.container,target:this.target(q),text:this.text(q)});this.emit(kt?"success":"error",{action:Me,text:kt,trigger:q,clearSelection:function(){q&&q.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(I){return vr("action",I)}},{key:"defaultTarget",value:function(I){var q=vr("target",I);if(q)return document.querySelector(q)}},{key:"defaultText",value:function(I){return vr("text",I)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(I){var q=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return N(I,q)}},{key:"cut",value:function(I){return h(I)}},{key:"isSupported",value:function(){var I=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],q=typeof I=="string"?[I]:I,Me=!!document.queryCommandSupported;return q.forEach(function(kt){Me=Me&&!!document.queryCommandSupported(kt)}),Me}}]),E}(a()),Ai=Li},828:function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function s(a,f){for(;a&&a.nodeType!==o;){if(typeof a.matches=="function"&&a.matches(f))return a;a=a.parentNode}}n.exports=s},438:function(n,o,i){var s=i(828);function a(u,p,m,d,h){var v=c.apply(this,arguments);return u.addEventListener(m,v,h),{destroy:function(){u.removeEventListener(m,v,h)}}}function f(u,p,m,d,h){return typeof u.addEventListener=="function"?a.apply(null,arguments):typeof m=="function"?a.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return a(v,p,m,d,h)}))}function c(u,p,m,d){return function(h){h.delegateTarget=s(h.target,p),h.delegateTarget&&d.call(u,h)}}n.exports=f},879:function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var s=Object.prototype.toString.call(i);return i!==void 0&&(s==="[object NodeList]"||s==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var s=Object.prototype.toString.call(i);return s==="[object Function]"}},370:function(n,o,i){var s=i(879),a=i(438);function f(m,d,h){if(!m&&!d&&!h)throw new Error("Missing required arguments");if(!s.string(d))throw new TypeError("Second argument must be a String");if(!s.fn(h))throw new TypeError("Third argument must be a Function");if(s.node(m))return c(m,d,h);if(s.nodeList(m))return u(m,d,h);if(s.string(m))return p(m,d,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(m,d,h){return m.addEventListener(d,h),{destroy:function(){m.removeEventListener(d,h)}}}function u(m,d,h){return Array.prototype.forEach.call(m,function(v){v.addEventListener(d,h)}),{destroy:function(){Array.prototype.forEach.call(m,function(v){v.removeEventListener(d,h)})}}}function p(m,d,h){return a(document.body,m,d,h)}n.exports=f},817:function(n){function o(i){var s;if(i.nodeName==="SELECT")i.focus(),s=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var a=i.hasAttribute("readonly");a||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),a||i.removeAttribute("readonly"),s=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var f=window.getSelection(),c=document.createRange();c.selectNodeContents(i),f.removeAllRanges(),f.addRange(c),s=f.toString()}return s}n.exports=o},279:function(n){function o(){}o.prototype={on:function(i,s,a){var f=this.e||(this.e={});return(f[i]||(f[i]=[])).push({fn:s,ctx:a}),this},once:function(i,s,a){var f=this;function c(){f.off(i,c),s.apply(a,arguments)}return c._=s,this.on(i,c,a)},emit:function(i){var s=[].slice.call(arguments,1),a=((this.e||(this.e={}))[i]||[]).slice(),f=0,c=a.length;for(f;f{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var rs=/["'&<>]/;Yo.exports=ns;function ns(e){var t=""+e,r=rs.exec(t);if(!r)return t;var n,o="",i=0,s=0;for(i=r.index;i0&&i[i.length-1])&&(c[0]===6||c[0]===2)){r=0;continue}if(c[0]===3&&(!i||c[1]>i[0]&&c[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function W(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var n=r.call(e),o,i=[],s;try{for(;(t===void 0||t-- >0)&&!(o=n.next()).done;)i.push(o.value)}catch(a){s={error:a}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(s)throw s.error}}return i}function D(e,t,r){if(r||arguments.length===2)for(var n=0,o=t.length,i;n1||a(m,d)})})}function a(m,d){try{f(n[m](d))}catch(h){p(i[0][3],h)}}function f(m){m.value instanceof et?Promise.resolve(m.value.v).then(c,u):p(i[0][2],m)}function c(m){a("next",m)}function u(m){a("throw",m)}function p(m,d){m(d),i.shift(),i.length&&a(i[0][0],i[0][1])}}function pn(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof Ee=="function"?Ee(e):e[Symbol.iterator](),r={},n("next"),n("throw"),n("return"),r[Symbol.asyncIterator]=function(){return this},r);function n(i){r[i]=e[i]&&function(s){return new Promise(function(a,f){s=e[i](s),o(a,f,s.done,s.value)})}}function o(i,s,a,f){Promise.resolve(f).then(function(c){i({value:c,done:a})},s)}}function C(e){return typeof e=="function"}function at(e){var t=function(n){Error.call(n),n.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var It=at(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(n,o){return o+1+") "+n.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function Ve(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Ie=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,n,o,i;if(!this.closed){this.closed=!0;var s=this._parentage;if(s)if(this._parentage=null,Array.isArray(s))try{for(var a=Ee(s),f=a.next();!f.done;f=a.next()){var c=f.value;c.remove(this)}}catch(v){t={error:v}}finally{try{f&&!f.done&&(r=a.return)&&r.call(a)}finally{if(t)throw t.error}}else s.remove(this);var u=this.initialTeardown;if(C(u))try{u()}catch(v){i=v instanceof It?v.errors:[v]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var m=Ee(p),d=m.next();!d.done;d=m.next()){var h=d.value;try{ln(h)}catch(v){i=i!=null?i:[],v instanceof It?i=D(D([],W(i)),W(v.errors)):i.push(v)}}}catch(v){n={error:v}}finally{try{d&&!d.done&&(o=m.return)&&o.call(m)}finally{if(n)throw n.error}}}if(i)throw new It(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)ln(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&Ve(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&Ve(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Sr=Ie.EMPTY;function jt(e){return e instanceof Ie||e&&"closed"in e&&C(e.remove)&&C(e.add)&&C(e.unsubscribe)}function ln(e){C(e)?e():e.unsubscribe()}var Le={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var st={setTimeout:function(e,t){for(var r=[],n=2;n0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,s=o.isStopped,a=o.observers;return i||s?Sr:(this.currentObservers=null,a.push(r),new Ie(function(){n.currentObservers=null,Ve(a,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,s=n.isStopped;o?r.error(i):s&&r.complete()},t.prototype.asObservable=function(){var r=new F;return r.source=this,r},t.create=function(r,n){return new xn(r,n)},t}(F);var xn=function(e){ie(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Sr},t}(x);var Et={now:function(){return(Et.delegate||Date).now()},delegate:void 0};var wt=function(e){ie(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=Et);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,s=n._infiniteTimeWindow,a=n._timestampProvider,f=n._windowTime;o||(i.push(r),!s&&i.push(a.now()+f)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,s=o._buffer,a=s.slice(),f=0;f0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=ut.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var s=r.actions;n!=null&&((i=s[s.length-1])===null||i===void 0?void 0:i.id)!==n&&(ut.cancelAnimationFrame(n),r._scheduled=void 0)},t}(Wt);var Sn=function(e){ie(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n=this._scheduled;this._scheduled=void 0;var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t}(Dt);var Oe=new Sn(wn);var M=new F(function(e){return e.complete()});function Vt(e){return e&&C(e.schedule)}function Cr(e){return e[e.length-1]}function Ye(e){return C(Cr(e))?e.pop():void 0}function Te(e){return Vt(Cr(e))?e.pop():void 0}function zt(e,t){return typeof Cr(e)=="number"?e.pop():t}var pt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Nt(e){return C(e==null?void 0:e.then)}function qt(e){return C(e[ft])}function Kt(e){return Symbol.asyncIterator&&C(e==null?void 0:e[Symbol.asyncIterator])}function Qt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function zi(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Yt=zi();function Gt(e){return C(e==null?void 0:e[Yt])}function Bt(e){return un(this,arguments,function(){var r,n,o,i;return $t(this,function(s){switch(s.label){case 0:r=e.getReader(),s.label=1;case 1:s.trys.push([1,,9,10]),s.label=2;case 2:return[4,et(r.read())];case 3:return n=s.sent(),o=n.value,i=n.done,i?[4,et(void 0)]:[3,5];case 4:return[2,s.sent()];case 5:return[4,et(o)];case 6:return[4,s.sent()];case 7:return s.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function Jt(e){return C(e==null?void 0:e.getReader)}function U(e){if(e instanceof F)return e;if(e!=null){if(qt(e))return Ni(e);if(pt(e))return qi(e);if(Nt(e))return Ki(e);if(Kt(e))return On(e);if(Gt(e))return Qi(e);if(Jt(e))return Yi(e)}throw Qt(e)}function Ni(e){return new F(function(t){var r=e[ft]();if(C(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function qi(e){return new F(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?A(function(o,i){return e(o,i,n)}):de,ge(1),r?He(t):Dn(function(){return new Zt}))}}function Vn(){for(var e=[],t=0;t=2,!0))}function pe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new x}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,s=i===void 0?!0:i,a=e.resetOnRefCountZero,f=a===void 0?!0:a;return function(c){var u,p,m,d=0,h=!1,v=!1,Y=function(){p==null||p.unsubscribe(),p=void 0},B=function(){Y(),u=m=void 0,h=v=!1},N=function(){var O=u;B(),O==null||O.unsubscribe()};return y(function(O,Qe){d++,!v&&!h&&Y();var De=m=m!=null?m:r();Qe.add(function(){d--,d===0&&!v&&!h&&(p=$r(N,f))}),De.subscribe(Qe),!u&&d>0&&(u=new rt({next:function($e){return De.next($e)},error:function($e){v=!0,Y(),p=$r(B,o,$e),De.error($e)},complete:function(){h=!0,Y(),p=$r(B,s),De.complete()}}),U(O).subscribe(u))})(c)}}function $r(e,t){for(var r=[],n=2;ne.next(document)),e}function K(e,t=document){return Array.from(t.querySelectorAll(e))}function z(e,t=document){let r=ce(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function ce(e,t=document){return t.querySelector(e)||void 0}function _e(){return document.activeElement instanceof HTMLElement&&document.activeElement||void 0}function tr(e){return L(b(document.body,"focusin"),b(document.body,"focusout")).pipe(ke(1),l(()=>{let t=_e();return typeof t!="undefined"?e.contains(t):!1}),V(e===_e()),J())}function Xe(e){return{x:e.offsetLeft,y:e.offsetTop}}function Kn(e){return L(b(window,"load"),b(window,"resize")).pipe(Ce(0,Oe),l(()=>Xe(e)),V(Xe(e)))}function rr(e){return{x:e.scrollLeft,y:e.scrollTop}}function dt(e){return L(b(e,"scroll"),b(window,"resize")).pipe(Ce(0,Oe),l(()=>rr(e)),V(rr(e)))}var Yn=function(){if(typeof Map!="undefined")return Map;function e(t,r){var n=-1;return t.some(function(o,i){return o[0]===r?(n=i,!0):!1}),n}return function(){function t(){this.__entries__=[]}return Object.defineProperty(t.prototype,"size",{get:function(){return this.__entries__.length},enumerable:!0,configurable:!0}),t.prototype.get=function(r){var n=e(this.__entries__,r),o=this.__entries__[n];return o&&o[1]},t.prototype.set=function(r,n){var o=e(this.__entries__,r);~o?this.__entries__[o][1]=n:this.__entries__.push([r,n])},t.prototype.delete=function(r){var n=this.__entries__,o=e(n,r);~o&&n.splice(o,1)},t.prototype.has=function(r){return!!~e(this.__entries__,r)},t.prototype.clear=function(){this.__entries__.splice(0)},t.prototype.forEach=function(r,n){n===void 0&&(n=null);for(var o=0,i=this.__entries__;o0},e.prototype.connect_=function(){!Wr||this.connected_||(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),va?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){!Wr||!this.connected_||(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(t){var r=t.propertyName,n=r===void 0?"":r,o=ba.some(function(i){return!!~n.indexOf(i)});o&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),Gn=function(e,t){for(var r=0,n=Object.keys(t);r0},e}(),Jn=typeof WeakMap!="undefined"?new WeakMap:new Yn,Xn=function(){function e(t){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var r=ga.getInstance(),n=new La(t,r,this);Jn.set(this,n)}return e}();["observe","unobserve","disconnect"].forEach(function(e){Xn.prototype[e]=function(){var t;return(t=Jn.get(this))[e].apply(t,arguments)}});var Aa=function(){return typeof nr.ResizeObserver!="undefined"?nr.ResizeObserver:Xn}(),Zn=Aa;var eo=new x,Ca=$(()=>k(new Zn(e=>{for(let t of e)eo.next(t)}))).pipe(g(e=>L(ze,k(e)).pipe(R(()=>e.disconnect()))),X(1));function he(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ye(e){return Ca.pipe(S(t=>t.observe(e)),g(t=>eo.pipe(A(({target:r})=>r===e),R(()=>t.unobserve(e)),l(()=>he(e)))),V(he(e)))}function bt(e){return{width:e.scrollWidth,height:e.scrollHeight}}function ar(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}var to=new x,Ra=$(()=>k(new IntersectionObserver(e=>{for(let t of e)to.next(t)},{threshold:0}))).pipe(g(e=>L(ze,k(e)).pipe(R(()=>e.disconnect()))),X(1));function sr(e){return Ra.pipe(S(t=>t.observe(e)),g(t=>to.pipe(A(({target:r})=>r===e),R(()=>t.unobserve(e)),l(({isIntersecting:r})=>r))))}function ro(e,t=16){return dt(e).pipe(l(({y:r})=>{let n=he(e),o=bt(e);return r>=o.height-n.height-t}),J())}var cr={drawer:z("[data-md-toggle=drawer]"),search:z("[data-md-toggle=search]")};function no(e){return cr[e].checked}function Ke(e,t){cr[e].checked!==t&&cr[e].click()}function Ue(e){let t=cr[e];return b(t,"change").pipe(l(()=>t.checked),V(t.checked))}function ka(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Ha(){return L(b(window,"compositionstart").pipe(l(()=>!0)),b(window,"compositionend").pipe(l(()=>!1))).pipe(V(!1))}function oo(){let e=b(window,"keydown").pipe(A(t=>!(t.metaKey||t.ctrlKey)),l(t=>({mode:no("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),A(({mode:t,type:r})=>{if(t==="global"){let n=_e();if(typeof n!="undefined")return!ka(n,r)}return!0}),pe());return Ha().pipe(g(t=>t?M:e))}function le(){return new URL(location.href)}function ot(e){location.href=e.href}function io(){return new x}function ao(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)ao(e,r)}function _(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)ao(n,o);return n}function fr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function so(){return location.hash.substring(1)}function Dr(e){let t=_("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Pa(e){return L(b(window,"hashchange"),e).pipe(l(so),V(so()),A(t=>t.length>0),X(1))}function co(e){return Pa(e).pipe(l(t=>ce(`[id="${t}"]`)),A(t=>typeof t!="undefined"))}function Vr(e){let t=matchMedia(e);return er(r=>t.addListener(()=>r(t.matches))).pipe(V(t.matches))}function fo(){let e=matchMedia("print");return L(b(window,"beforeprint").pipe(l(()=>!0)),b(window,"afterprint").pipe(l(()=>!1))).pipe(V(e.matches))}function zr(e,t){return e.pipe(g(r=>r?t():M))}function ur(e,t={credentials:"same-origin"}){return ue(fetch(`${e}`,t)).pipe(fe(()=>M),g(r=>r.status!==200?Ot(()=>new Error(r.statusText)):k(r)))}function We(e,t){return ur(e,t).pipe(g(r=>r.json()),X(1))}function uo(e,t){let r=new DOMParser;return ur(e,t).pipe(g(n=>n.text()),l(n=>r.parseFromString(n,"text/xml")),X(1))}function pr(e){let t=_("script",{src:e});return $(()=>(document.head.appendChild(t),L(b(t,"load"),b(t,"error").pipe(g(()=>Ot(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(l(()=>{}),R(()=>document.head.removeChild(t)),ge(1))))}function po(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function lo(){return L(b(window,"scroll",{passive:!0}),b(window,"resize",{passive:!0})).pipe(l(po),V(po()))}function mo(){return{width:innerWidth,height:innerHeight}}function ho(){return b(window,"resize",{passive:!0}).pipe(l(mo),V(mo()))}function bo(){return G([lo(),ho()]).pipe(l(([e,t])=>({offset:e,size:t})),X(1))}function lr(e,{viewport$:t,header$:r}){let n=t.pipe(ee("size")),o=G([n,r]).pipe(l(()=>Xe(e)));return G([r,t,o]).pipe(l(([{height:i},{offset:s,size:a},{x:f,y:c}])=>({offset:{x:s.x-f,y:s.y-c+i},size:a})))}(()=>{function e(n,o){parent.postMessage(n,o||"*")}function t(...n){return n.reduce((o,i)=>o.then(()=>new Promise(s=>{let a=document.createElement("script");a.src=i,a.onload=s,document.body.appendChild(a)})),Promise.resolve())}var r=class extends EventTarget{constructor(n){super(),this.url=n,this.m=i=>{i.source===this.w&&(this.dispatchEvent(new MessageEvent("message",{data:i.data})),this.onmessage&&this.onmessage(i))},this.e=(i,s,a,f,c)=>{if(s===`${this.url}`){let u=new ErrorEvent("error",{message:i,filename:s,lineno:a,colno:f,error:c});this.dispatchEvent(u),this.onerror&&this.onerror(u)}};let o=document.createElement("iframe");o.hidden=!0,document.body.appendChild(this.iframe=o),this.w.document.open(),this.w.document.write(`

Back handling

Back handling in the library is opt-in, rather than opt-out. By itself, neither NavController nor NavHost handles the back button press. You can add NavBackHandler or usual BackHandler in order to react to back presses where you need to.

NavBackHandler is the most basic implementation of BackHandler - it calls pop only when there are more than one item in the backstack. When there is only one backstack item left, NavBackHandler is disabled, and any upper-level BackHandler may take its turn to react to back button presses.

If you want to specify your own backstack logic, use BackHandler directly. For example, this is how back navigation is handled for BottomNavigation in the sample:

@Composable
+private fun BottomNavigationBackHandler(
+    navController: NavController<BottomNavigationDestination>
+) {
+    BackHandler(enabled = navController.backstack.entries.size > 1) {
+        val lastEntry = navController.backstack.entries.last()
+        if (lastEntry.destination == BottomNavigationDestination.values()[0]) {
+            // The start destination should always be the last to pop. We move
+            // it to the start to preserve its saved state and view models.
+            navController.moveLastEntryToStart()
+        } else {
+            navController.pop()
+        }
+    }
+}
+

Bug

Always place your NavBackHandler/BackHandler before the corresponding NavHost.

As both BackHandler and NavHost use Lifecycle under the hood, there is a case when the order of back handling may be restored incorrectly after process/activity recreation. This is how the framework works and there is nothing to do about it. Simple placement of BackHandler before NavHost guarantees no issues in this part.

\ No newline at end of file diff --git a/bottom-sheets/index.html b/bottom-sheets/index.html new file mode 100644 index 00000000..d12a0d4c --- /dev/null +++ b/bottom-sheets/index.html @@ -0,0 +1,43 @@ + Bottom sheets - Compose Navigation Reimagined

Bottom sheets

Similar to dialogs, you may use BottomSheetNavHost to handle a backstack of bottom sheets alongside the backstack of screens.

To use it, you need to add the dependency:

// if you are using Material
+implementation("dev.olshevski.navigation:reimagined-material:<latest-version>")
+
+// if you are using Material 3
+implementation("dev.olshevski.navigation:reimagined-material3:<latest-version>")
+

The usage would look like this:

@Composable
+fun NavHostScreen() {
+    val navController = rememberNavController<ScreenDestination>(
+        startDestination = ScreenDestination.First,
+    )
+
+    val sheetController = rememberNavController<SheetDestination>(
+        initialBackstack = emptyList()
+    )
+
+    NavBackHandler(navController)
+
+    NavHost(navController) { destination ->
+        when (destination) {
+            ScreenDestination.First -> { /* ... */ }
+            ScreenDestination.Second -> { /* ... */ }
+        }
+    }
+
+    BackHandler(enabled = sheetController.backstack.entries.isNotEmpty()) {
+        sheetController.pop()
+    }
+
+    BottomSheetNavHost(
+        controller = sheetController,
+        onDismissRequest = { sheetController.pop() }
+    ) { destination ->
+        Surface(
+            elevation = ModalBottomSheetDefaults.Elevation
+        ) {
+            when (destination) {
+                SheetDestination.First -> { /* ... */ }
+                SheetDestination.Second -> { /* ... */ }
+            }
+        }
+    }
+}
+

BottomSheetNavHost is based on the source code of ModalBottomSheetLayout, but with some improvements for switching between multiple bottom sheets. The API also has some similarities.

Tip

You can access current sheetState through the BottomSheetNavHostScope receiver of the contentSelector parameter.

\ No newline at end of file diff --git a/dialogs/index.html b/dialogs/index.html new file mode 100644 index 00000000..5cc6ab1f --- /dev/null +++ b/dialogs/index.html @@ -0,0 +1,29 @@ + Dialogs - Compose Navigation Reimagined

Dialogs

If you need to handle a backstack of dialogs in your application, simply add DialogNavHost to the same composition layer where your regular NavHost lives. This way you may show and control the backstack of regular screen destinations, as well as a second backstack of dialogs:

@Composable
+fun NavHostScreen() {
+    val navController = rememberNavController<ScreenDestination>(
+        startDestination = ScreenDestination.First,
+    )
+
+    val dialogController = rememberNavController<DialogDestination>(
+        initialBackstack = emptyList()
+    )
+
+    NavBackHandler(navController)
+
+    NavHost(navController) { destination ->
+        when (destination) {
+            ScreenDestination.First -> { /* ... */ }
+            ScreenDestination.Second -> { /* ... */ }
+        }
+    }
+
+    DialogNavHost(dialogController) { destination ->
+        Dialog(onDismissRequest = { dialogController.pop() }) {
+            when (destination) {
+                DialogDestination.First -> { /* ... */ }
+                DialogDestination.Second -> { /* ... */ }
+            }
+        }
+    }
+}
+

DialogNavHost is an alternative version of NavHost that is better suited for showing dialogs. It is based on AnimatedNavHost and provides smoother transition between dialogs without scrim flickering:

And this is how it looks in the regular NavHost:

Note

DialogNavHost doesn't wrap your composables into a Dialog. You need to use either Dialog or AlertDialog composable inside contentSelector yourself.

\ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 00000000..39caf293 --- /dev/null +++ b/index.html @@ -0,0 +1,48 @@ + Compose Navigation Reimagined

Overview

A small and simple, yet fully fledged and customizable navigation library for Jetpack Compose:

  • Full type-safety
  • Built-in state restoration
  • Nested navigation with independent backstacks
  • Own Lifecycle, ViewModelStore and SavedStateRegistry for every backstack entry
  • Animated transitions
  • Dialog and bottom sheet navigation
  • Scopes for easier ViewModel sharing
  • No builders, no obligatory superclasses for your composables

Quick start

Add a single dependency to your project:

implementation("dev.olshevski.navigation:reimagined:1.4.0")
+

Define a set of destinations. It is convenient to use a sealed class for this:

sealed class Destination : Parcelable {
+
+    @Parcelize
+    data object First : Destination()
+
+    @Parcelize
+    data class Second(val id: Int) : Destination()
+
+    @Parcelize
+    data class Third(val text: String) : Destination()
+
+}
+

Create a composable with NavController, NavBackHandler and NavHost:

@Composable
+fun NavHostScreen() {
+    val navController = rememberNavController<Destination>(
+        startDestination = Destination.First
+    )
+
+    NavBackHandler(navController)
+
+    NavHost(navController) { destination ->
+        when (destination) {
+            is Destination.First -> Column {
+                Text("First destination")
+                Button(onClick = {
+                    navController.navigate(Destination.Second(id = 42))
+                }) {
+                    Text("Open Second destination")
+                }
+            }
+
+            is Destination.Second -> Column {
+                Text("Second destination: ${destination.id}")
+                Button(onClick = {
+                    navController.navigate(Destination.Third(text = "Hello"))
+                }) {
+                    Text("Open Third destination")
+                }
+            }
+
+            is Destination.Third -> {
+                Text("Third destination: ${destination.text}")
+            }
+        }
+    }
+}
+

As you can see, NavController is used for switching between destinations, NavBackHandler handles back presses and NavHost provides a composable corresponding to the last destination in the backstack. As simple as that.

\ No newline at end of file diff --git a/nav-controller/index.html b/nav-controller/index.html new file mode 100644 index 00000000..1acfa100 --- /dev/null +++ b/nav-controller/index.html @@ -0,0 +1,34 @@ + NavController - Compose Navigation Reimagined

NavController

This is the main control point of navigation. It keeps record of all current backstack entries and preserves them on activity/process recreation.

NavController may be created with rememberNavController method in a composable function or with navController outside of composition. The latter may be used for storing NavController in a ViewModel. As it implements Parcelable interface, it could be stored in a SavedStateHandle.

Both rememberNavController and navController methods accept startDestination as a parameter:

val navController = rememberNavController<Destination>(
+    startDestination = Destination.First
+)
+

If you want to create NavController with an arbitrary number of backstack items, you may use initialBackstack parameter instead:

val navController = rememberNavController<Destination>(
+    initialBackstack = listOf(Destination.First, Destination.Second, Destination.Third)
+)
+

Destination.Third will become the currently displayed item. Destination.First and Destination.Second will be stored in the backstack.

If you want to store NavController in a ViewModel use saveable delegate for SavedStateHandle:

class NavigationViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
+
+    val navController by savedStateHandle.saveable<NavController<Destination>> {
+        navController(startDestination = Destination.First)
+    }
+
+}
+

Destinations

NavController accepts all types that meet the requirements as destinations:

  1. The type must be writable to Parcel - it could be Parcelable, Serializable, string/primitive, or other supported type.

  2. The type must be Stable, Immutable, or string/primitive type.

Other than that, you are not limited to any particular type.

Tip

It is very convenient to define your set of destinations as a sealed class or enum. This way you will always be notified by the compiler that you have a non-exhaustive when statement if you add a new destination.

Tip

You may also define your own base interface for destinations, for example:

interface Destination : Parcelable {
+
+    @Composable
+    fun Content()
+
+}
+

This way you may handle each destinations without checking its instance:

NavHost(navController) { it.Content() }
+

In order to be passed into NavController, each destination should be wrapped into NavEntry. It contains a unique identifier which is used to properly preserve saved state and manage Android architecture components (Lifecycle, ViewModelStore and SavedStateRegistry) for each such entry inside NavHost.

Saved state and view models of each entry are guaranteed to be preserved for as long as the associated entry is present in the backstack.

Note

If you add two equal destinations to the backstack, wrapped into two different entries, they will get their own separate identities, saved states and components. However, it is possible to put same exact entry instance into the backstack and it will be correctly treated as the same entry.

There is a handful of pre-defined methods suitable for basic app navigation: navigate, moveToTop, pop, popUpTo, popAll, replaceLast, replaceUpTo, replaceAll. They all are pretty much self-explanatory, except maybe moveToTop.

moveToTop method searches for some particular destination in the backstack and moves it to the top, effectively making it the currently displayed destination. This is particularly useful for integration with BottomNavigation/TabRow, when you want to always keep a single instance of every destination in the backstack.

The method is expected to be used in pair with navigate:

if (!navController.moveToTop { it is SomeDestination }) {
+    // navigate to a new destination if there is no existing one
+    navController.navigate(SomeDestination())
+}
+

You may see how it is used for BottomNavigation in the sample.

Methods with a search predicate

moveToTop, popUpTo, replaceUpTo methods require the predicate parameter to be specified. It provides a selection condition for a destination to search for.

In case multiple destinations match the predicate, you may specify the match parameter. Match.Last is the default value and in this case the last matching item from the start of the backstack will be selected. Alternatively, you may use Match.First.

New custom methods

If your use-case calls for some advanced backstack manipulations, you may use setNewBackstack method. It is in fact the only public method defined in NavController, all other methods are provided as extensions and use setNewBackstack under the hood. Here is how a new extension method moveLastEntryToStart is implemented in the sample:

fun NavController<BottomNavigationDestination>.moveLastEntryToStart() {
+    setNewBackstack(
+        entries = backstack.entries.toMutableList().also {
+            val entry = it.removeLast()
+            it.add(0, entry)
+        },
+        action = NavAction.Pop
+    )
+}
+

You may access current backstack entries and the last NavAction through backstack property of NavController. This property is backed up by MutableState and any changes to it will notify composition.

\ No newline at end of file diff --git a/nav-host/index.html b/nav-host/index.html new file mode 100644 index 00000000..1000c529 --- /dev/null +++ b/nav-host/index.html @@ -0,0 +1,12 @@ + Basics - Compose Navigation Reimagined

NavHost

NavHost is a composable that shows the last entry of a backstack, manages saved state and provides all Android architecture components associated with the entry: Lifecycle, ViewModelStore and SavedStateRegistry. All these components are provided through CompositionLocalProvider within their corresponding owners: LocalLifecycleOwner, LocalViewModelStoreOwner and LocalSavedStateRegistryOwner.

The components are kept around until its associated entry is removed from the backstack (or until the parent entry containing the current child NavHost is removed).

The default NavHost implementation by itself doesn't provide any animated transitions, it simply jump-cuts to the next destination:

Each NavEntry from the backstack is mapped to NavHostEntry within NavHost. NavHostEntry is what actually implements LifecycleOwner, SavedStateRegistryOwner and ViewModelStoreOwner interfaces.

Usually, you don't need to interact with NavHostEntries directly, everything just works out of the box. But if you have a situation when you need to access all NavHostEntries from the current backstack, e.g. trying to access a ViewModel of neighbour entry, you could do it through the NavHostScope receiver of the contentSelector parameter.

NavHostState is a state holder of NavHost that stores and manages saved state and all Android architecture components for each entry. By default, it is automatically created by NavHost, but it is possible to create and set it into NavHost manually.

Note that you most probably don't need to use the state holder directly unless you are conditionally adding/removing NavHost to/from composition:

val state = rememberNavHostState(backstack)
+if (visible) {
+     NavHost(state) {
+         // ...
+     }
+}
+

In this example, the state of NavHost will be properly preserved, as it is placed outside of condition.

If you do want to clear the state when NavHost is removed by condition, use NavHostVisibility/NavHostAnimatedVisibility. These composables properly clear the internal state of NavHost when the visible parameter is set to false:

NavHostVisibility(visible) {
+    NavHost(backstack) {
+        // ...
+    }
+}
+

You can explore the sample of NavHostVisibility usage here.

\ No newline at end of file diff --git a/nested-navigation/index.html b/nested-navigation/index.html new file mode 100644 index 00000000..dc2f0617 --- /dev/null +++ b/nested-navigation/index.html @@ -0,0 +1 @@ + Nested navigation - Compose Navigation Reimagined

Nested navigation

Nested navigation is actually quite simple. You just need to place NavHost (let's call it a child) into any entry of the other NavHost (a parent). You may want to add decoration around your child NavHost or leave it within the same viewport of the parent NavHost.

There may be different reasons for nesting your NavHosts:

  • It may be useful when you need to have several backstacks at once, as in case of BottomNavigation, TabRow, or similar, where each item has it's own inner independent layer of navigation.

  • You want to contain some particular flow of destinations within a single composable function. This flow may also contain some shared static layout elements.

  • You want to share a ViewModel between several destinations that logically and visually may be grouped into a single flow.

Note

There is no depth limit for nesting NavHosts. In fact, each NavHost is completely oblivious to its placement in the hierarchy.

\ No newline at end of file diff --git a/return-results/index.html b/return-results/index.html new file mode 100644 index 00000000..deea6092 --- /dev/null +++ b/return-results/index.html @@ -0,0 +1,23 @@ + Return results - Compose Navigation Reimagined

Return results

As destination types are not strictly required to be Immutable, you may change them while they are in the backstack. This may be used for returning values from other destinations. Just make a mutable property backed up by mutableStateOf and change it when required.

For example, we want to return a string from the Second screen. Here is how destinations may be defined:

interface AcceptsResultFromSecond {
+    val resultFromSecond: MutableState<String?>
+}
+
+@Stable
+sealed class Destination : Parcelable {
+
+    @Parcelize
+    data class First(
+        override val resultFromSecond: @RawValue MutableState<String?> = mutableStateOf(null)
+    ) : Destination(), AcceptsResultFromSecond
+
+    @Parcelize
+    data object Second : Destination()
+
+}
+

And to actually set the result from the Second screen you do:

val previousDestination = navController.backstack.entries.let {
+    it[it.lastIndex - 1].destination
+}
+check(previousDestination is AcceptsResultFromSecond)
+previousDestination.resultFromSecond.value = text
+navController.pop()
+

You may see how it is implemented in the sample here.

Warning

In general, returning values to previous destinations makes the navigation logic more complicated. Also, this approach doesn't guarantee full compile time type-safety. Use it with caution and when you are sure what you are doing. Sometimes it may be easier to use a shared state holder.

\ No newline at end of file diff --git a/search/search_index.json b/search/search_index.json new file mode 100644 index 00000000..571c7fd1 --- /dev/null +++ b/search/search_index.json @@ -0,0 +1 @@ +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Overview","text":"

A small and simple, yet fully fledged and customizable navigation library for Jetpack Compose:

  • Full type-safety
  • Built-in state restoration
  • Nested navigation with independent backstacks
  • Own Lifecycle, ViewModelStore and SavedStateRegistry for every backstack entry
  • Animated transitions
  • Dialog and bottom sheet navigation
  • Scopes for easier ViewModel sharing
  • No builders, no obligatory superclasses for your composables
"},{"location":"#quick-start","title":"Quick start","text":"

Add a single dependency to your project:

implementation(\"dev.olshevski.navigation:reimagined:1.4.0\")\n

Define a set of destinations. It is convenient to use a sealed class for this:

sealed class Destination : Parcelable {\n\n@Parcelize\ndata object First : Destination()\n\n@Parcelize\ndata class Second(val id: Int) : Destination()\n\n@Parcelize\ndata class Third(val text: String) : Destination()\n\n}\n

Create a composable with NavController, NavBackHandler and NavHost:

@Composable\nfun NavHostScreen() {\nval navController = rememberNavController<Destination>(\nstartDestination = Destination.First\n)\n\nNavBackHandler(navController)\n\nNavHost(navController) { destination ->\nwhen (destination) {\nis Destination.First -> Column {\nText(\"First destination\")\nButton(onClick = {\nnavController.navigate(Destination.Second(id = 42))\n}) {\nText(\"Open Second destination\")\n}\n}\n\nis Destination.Second -> Column {\nText(\"Second destination: ${destination.id}\")\nButton(onClick = {\nnavController.navigate(Destination.Third(text = \"Hello\"))\n}) {\nText(\"Open Third destination\")\n}\n}\n\nis Destination.Third -> {\nText(\"Third destination: ${destination.text}\")\n}\n}\n}\n}\n

As you can see, NavController is used for switching between destinations, NavBackHandler handles back presses and NavHost provides a composable corresponding to the last destination in the backstack. As simple as that.

"},{"location":"animations/","title":"Animations","text":"

If you want to show animated transitions between destinations use AnimatedNavHost. The default transition is a simple crossfade, but you can granularly customize every transition with your own NavTransitionSpec implementation.

Here is one possible implementation of NavTransitionSpec:

val CustomTransitionSpec = NavTransitionSpec<Any?> { action, _, _ ->\nval direction = if (action == NavAction.Pop) {\nAnimatedContentTransitionScope.SlideDirection.End\n} else {\nAnimatedContentTransitionScope.SlideDirection.Start\n}\nslideIntoContainer(direction) togetherWith slideOutOfContainer(direction)\n}\n

Set it into AnimatedNavHost:

AnimatedNavHost(\ncontroller = navController,\ntransitionSpec = CustomTransitionSpec\n) { destination ->\n// ...\n}\n

and it'll end up looking like this:

In NavTransitionSpec you get the parameters:

  • action - a hint about the last NavController method that changed the backstack
  • from - a previous visible destination
  • to - a target visible destination

This information is plenty enough to choose a transition for every possible combination of destinations and navigation actions.

Tip

You can add more enter/exit animations to the composables through the AnimatedNavHostScope receiver of the contentSelector parameter. AnimatedNavHostScope gives you access to the current transition and to animateEnterExit modifier.

"},{"location":"animations/#navaction","title":"NavAction","text":"

There are four default NavAction types:

  • Pop, Replace and Navigate - objects that correspond to pop\u2026, replace\u2026, navigate methods of NavController
  • Idle - the default action of a newly created NavController. You don't need to handle it in NavTransitionSpec.

You can also create new action types by implementing NavAction interface. Pass any object of the new type into setNewBackstack method of NavController and handle it in NavTransitionSpec.

The last action can also be accessed through action property of NavBackstack.

"},{"location":"back-handling/","title":"Back handling","text":"

Back handling in the library is opt-in, rather than opt-out. By itself, neither NavController nor NavHost handles the back button press. You can add NavBackHandler or usual BackHandler in order to react to back presses where you need to.

NavBackHandler is the most basic implementation of BackHandler - it calls pop only when there are more than one item in the backstack. When there is only one backstack item left, NavBackHandler is disabled, and any upper-level BackHandler may take its turn to react to back button presses.

If you want to specify your own backstack logic, use BackHandler directly. For example, this is how back navigation is handled for BottomNavigation in the sample:

@Composable\nprivate fun BottomNavigationBackHandler(\nnavController: NavController<BottomNavigationDestination>\n) {\nBackHandler(enabled = navController.backstack.entries.size > 1) {\nval lastEntry = navController.backstack.entries.last()\nif (lastEntry.destination == BottomNavigationDestination.values()[0]) {\n// The start destination should always be the last to pop. We move\n// it to the start to preserve its saved state and view models.\nnavController.moveLastEntryToStart()\n} else {\nnavController.pop()\n}\n}\n}\n

Bug

Always place your NavBackHandler/BackHandler before the corresponding NavHost.

As both BackHandler and NavHost use Lifecycle under the hood, there is a case when the order of back handling may be restored incorrectly after process/activity recreation. This is how the framework works and there is nothing to do about it. Simple placement of BackHandler before NavHost guarantees no issues in this part.

"},{"location":"bottom-sheets/","title":"Bottom sheets","text":"

Similar to dialogs, you may use BottomSheetNavHost to handle a backstack of bottom sheets alongside the backstack of screens.

To use it, you need to add the dependency:

// if you are using Material\nimplementation(\"dev.olshevski.navigation:reimagined-material:<latest-version>\")\n\n// if you are using Material 3\nimplementation(\"dev.olshevski.navigation:reimagined-material3:<latest-version>\")\n

The usage would look like this:

@Composable\nfun NavHostScreen() {\nval navController = rememberNavController<ScreenDestination>(\nstartDestination = ScreenDestination.First,\n)\n\nval sheetController = rememberNavController<SheetDestination>(\ninitialBackstack = emptyList()\n)\n\nNavBackHandler(navController)\n\nNavHost(navController) { destination ->\nwhen (destination) {\nScreenDestination.First -> { /* ... */ }\nScreenDestination.Second -> { /* ... */ }\n}\n}\n\nBackHandler(enabled = sheetController.backstack.entries.isNotEmpty()) {\nsheetController.pop()\n}\n\nBottomSheetNavHost(\ncontroller = sheetController,\nonDismissRequest = { sheetController.pop() }\n) { destination ->\nSurface(\nelevation = ModalBottomSheetDefaults.Elevation\n) {\nwhen (destination) {\nSheetDestination.First -> { /* ... */ }\nSheetDestination.Second -> { /* ... */ }\n}\n}\n}\n}\n

BottomSheetNavHost is based on the source code of ModalBottomSheetLayout, but with some improvements for switching between multiple bottom sheets. The API also has some similarities.

Tip

You can access current sheetState through the BottomSheetNavHostScope receiver of the contentSelector parameter.

"},{"location":"dialogs/","title":"Dialogs","text":"

If you need to handle a backstack of dialogs in your application, simply add DialogNavHost to the same composition layer where your regular NavHost lives. This way you may show and control the backstack of regular screen destinations, as well as a second backstack of dialogs:

@Composable\nfun NavHostScreen() {\nval navController = rememberNavController<ScreenDestination>(\nstartDestination = ScreenDestination.First,\n)\n\nval dialogController = rememberNavController<DialogDestination>(\ninitialBackstack = emptyList()\n)\n\nNavBackHandler(navController)\n\nNavHost(navController) { destination ->\nwhen (destination) {\nScreenDestination.First -> { /* ... */ }\nScreenDestination.Second -> { /* ... */ }\n}\n}\n\nDialogNavHost(dialogController) { destination ->\nDialog(onDismissRequest = { dialogController.pop() }) {\nwhen (destination) {\nDialogDestination.First -> { /* ... */ }\nDialogDestination.Second -> { /* ... */ }\n}\n}\n}\n}\n

DialogNavHost is an alternative version of NavHost that is better suited for showing dialogs. It is based on AnimatedNavHost and provides smoother transition between dialogs without scrim flickering:

And this is how it looks in the regular NavHost:

Note

DialogNavHost doesn't wrap your composables into a Dialog. You need to use either Dialog or AlertDialog composable inside contentSelector yourself.

"},{"location":"nav-controller/","title":"NavController","text":"

This is the main control point of navigation. It keeps record of all current backstack entries and preserves them on activity/process recreation.

NavController may be created with rememberNavController method in a composable function or with navController outside of composition. The latter may be used for storing NavController in a ViewModel. As it implements Parcelable interface, it could be stored in a SavedStateHandle.

Both rememberNavController and navController methods accept startDestination as a parameter:

val navController = rememberNavController<Destination>(\nstartDestination = Destination.First\n)\n

If you want to create NavController with an arbitrary number of backstack items, you may use initialBackstack parameter instead:

val navController = rememberNavController<Destination>(\ninitialBackstack = listOf(Destination.First, Destination.Second, Destination.Third)\n)\n

Destination.Third will become the currently displayed item. Destination.First and Destination.Second will be stored in the backstack.

If you want to store NavController in a ViewModel use saveable delegate for SavedStateHandle:

class NavigationViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {\n\nval navController by savedStateHandle.saveable<NavController<Destination>> {\nnavController(startDestination = Destination.First)\n}\n\n}\n
"},{"location":"nav-controller/#destinations","title":"Destinations","text":"

NavController accepts all types that meet the requirements as destinations:

  1. The type must be writable to Parcel - it could be Parcelable, Serializable, string/primitive, or other supported type.

  2. The type must be Stable, Immutable, or string/primitive type.

Other than that, you are not limited to any particular type.

Tip

It is very convenient to define your set of destinations as a sealed class or enum. This way you will always be notified by the compiler that you have a non-exhaustive when statement if you add a new destination.

Tip

You may also define your own base interface for destinations, for example:

interface Destination : Parcelable {\n\n@Composable\nfun Content()\n\n}\n

This way you may handle each destinations without checking its instance:

NavHost(navController) { it.Content() }\n
"},{"location":"nav-controller/#naventry","title":"NavEntry","text":"

In order to be passed into NavController, each destination should be wrapped into NavEntry. It contains a unique identifier which is used to properly preserve saved state and manage Android architecture components (Lifecycle, ViewModelStore and SavedStateRegistry) for each such entry inside NavHost.

Saved state and view models of each entry are guaranteed to be preserved for as long as the associated entry is present in the backstack.

Note

If you add two equal destinations to the backstack, wrapped into two different entries, they will get their own separate identities, saved states and components. However, it is possible to put same exact entry instance into the backstack and it will be correctly treated as the same entry.

"},{"location":"nav-controller/#navigation-methods","title":"Navigation methods","text":"

There is a handful of pre-defined methods suitable for basic app navigation: navigate, moveToTop, pop, popUpTo, popAll, replaceLast, replaceUpTo, replaceAll. They all are pretty much self-explanatory, except maybe moveToTop.

moveToTop method searches for some particular destination in the backstack and moves it to the top, effectively making it the currently displayed destination. This is particularly useful for integration with BottomNavigation/TabRow, when you want to always keep a single instance of every destination in the backstack.

The method is expected to be used in pair with navigate:

if (!navController.moveToTop { it is SomeDestination }) {\n// navigate to a new destination if there is no existing one\nnavController.navigate(SomeDestination())\n}\n

You may see how it is used for BottomNavigation in the sample.

"},{"location":"nav-controller/#methods-with-a-search-predicate","title":"Methods with a search predicate","text":"

moveToTop, popUpTo, replaceUpTo methods require the predicate parameter to be specified. It provides a selection condition for a destination to search for.

In case multiple destinations match the predicate, you may specify the match parameter. Match.Last is the default value and in this case the last matching item from the start of the backstack will be selected. Alternatively, you may use Match.First.

"},{"location":"nav-controller/#new-custom-methods","title":"New custom methods","text":"

If your use-case calls for some advanced backstack manipulations, you may use setNewBackstack method. It is in fact the only public method defined in NavController, all other methods are provided as extensions and use setNewBackstack under the hood. Here is how a new extension method moveLastEntryToStart is implemented in the sample:

fun NavController<BottomNavigationDestination>.moveLastEntryToStart() {\nsetNewBackstack(\nentries = backstack.entries.toMutableList().also {\nval entry = it.removeLast()\nit.add(0, entry)\n},\naction = NavAction.Pop\n)\n}\n
"},{"location":"nav-controller/#navbackstack","title":"NavBackstack","text":"

You may access current backstack entries and the last NavAction through backstack property of NavController. This property is backed up by MutableState and any changes to it will notify composition.

"},{"location":"nav-host/","title":"NavHost","text":"

NavHost is a composable that shows the last entry of a backstack, manages saved state and provides all Android architecture components associated with the entry: Lifecycle, ViewModelStore and SavedStateRegistry. All these components are provided through CompositionLocalProvider within their corresponding owners: LocalLifecycleOwner, LocalViewModelStoreOwner and LocalSavedStateRegistryOwner.

The components are kept around until its associated entry is removed from the backstack (or until the parent entry containing the current child NavHost is removed).

The default NavHost implementation by itself doesn't provide any animated transitions, it simply jump-cuts to the next destination:

"},{"location":"nav-host/#navhostentry-and-navhostscope","title":"NavHostEntry and NavHostScope","text":"

Each NavEntry from the backstack is mapped to NavHostEntry within NavHost. NavHostEntry is what actually implements LifecycleOwner, SavedStateRegistryOwner and ViewModelStoreOwner interfaces.

Usually, you don't need to interact with NavHostEntries directly, everything just works out of the box. But if you have a situation when you need to access all NavHostEntries from the current backstack, e.g. trying to access a ViewModel of neighbour entry, you could do it through the NavHostScope receiver of the contentSelector parameter.

"},{"location":"nav-host/#navhoststate","title":"NavHostState","text":"

NavHostState is a state holder of NavHost that stores and manages saved state and all Android architecture components for each entry. By default, it is automatically created by NavHost, but it is possible to create and set it into NavHost manually.

Note that you most probably don't need to use the state holder directly unless you are conditionally adding/removing NavHost to/from composition:

val state = rememberNavHostState(backstack)\nif (visible) {\nNavHost(state) {\n// ...\n}\n}\n

In this example, the state of NavHost will be properly preserved, as it is placed outside of condition.

If you do want to clear the state when NavHost is removed by condition, use NavHostVisibility/NavHostAnimatedVisibility. These composables properly clear the internal state of NavHost when the visible parameter is set to false:

NavHostVisibility(visible) {\nNavHost(backstack) {\n// ...\n}\n}\n

You can explore the sample of NavHostVisibility usage here.

"},{"location":"nested-navigation/","title":"Nested navigation","text":"

Nested navigation is actually quite simple. You just need to place NavHost (let's call it a child) into any entry of the other NavHost (a parent). You may want to add decoration around your child NavHost or leave it within the same viewport of the parent NavHost.

There may be different reasons for nesting your NavHosts:

  • It may be useful when you need to have several backstacks at once, as in case of BottomNavigation, TabRow, or similar, where each item has it's own inner independent layer of navigation.

  • You want to contain some particular flow of destinations within a single composable function. This flow may also contain some shared static layout elements.

  • You want to share a ViewModel between several destinations that logically and visually may be grouped into a single flow.

Note

There is no depth limit for nesting NavHosts. In fact, each NavHost is completely oblivious to its placement in the hierarchy.

"},{"location":"return-results/","title":"Return results","text":"

As destination types are not strictly required to be Immutable, you may change them while they are in the backstack. This may be used for returning values from other destinations. Just make a mutable property backed up by mutableStateOf and change it when required.

For example, we want to return a string from the Second screen. Here is how destinations may be defined:

interface AcceptsResultFromSecond {\nval resultFromSecond: MutableState<String?>\n}\n\n@Stable\nsealed class Destination : Parcelable {\n\n@Parcelize\ndata class First(\noverride val resultFromSecond: @RawValue MutableState<String?> = mutableStateOf(null)\n) : Destination(), AcceptsResultFromSecond\n\n@Parcelize\ndata object Second : Destination()\n\n}\n

And to actually set the result from the Second screen you do:

val previousDestination = navController.backstack.entries.let {\nit[it.lastIndex - 1].destination\n}\ncheck(previousDestination is AcceptsResultFromSecond)\npreviousDestination.resultFromSecond.value = text\nnavController.pop()\n

You may see how it is implemented in the sample here.

Warning

In general, returning values to previous destinations makes the navigation logic more complicated. Also, this approach doesn't guarantee full compile time type-safety. Use it with caution and when you are sure what you are doing. Sometimes it may be easier to use a shared state holder.

"},{"location":"shared-view-models/","title":"Shared ViewModels","text":"

Sometimes you need to access the same ViewModel instance from several destinations. The library provides multiple ways to achieve this.

"},{"location":"shared-view-models/#nested-navigation","title":"Nested navigation","text":"

The easiest way to share a ViewModel between several destinations is to use a nested NavHost. Simply collect all required destinations into a separate nested NavHost, and pass a ViewModel of the parent entry to each destination.

However, it may not work for all scenarios. Sometimes it is not desirable or possible to group destinations into a single nested NavHost. For such cases, it would be more convenient to use scoping NavHosts.

"},{"location":"shared-view-models/#scoping-navhosts","title":"Scoping NavHosts","text":"

Each NavHost in the library has its own Scoping counterpart. For NavHost it is ScopingNavHost, for AnimatedNavHost it is ScopingAnimatedNavHost, and so on.

Every scoping NavHost gives you the ability to assign scopes to destinations and access scoped ViewModelStoreOwners bound to each of the defined scope. Such scoped ViewModelStoreOwner is created when there is at least one backstack entry marked with the corresponding scope, and removed when there are none of the entries marked with it.

For example, the ViewModelStoreOwner for Scope X will exist only when at least one of the destinations B or C is in the backstack (their positions don't matter). Both B and C can access the same ViewModelStoreOwner instance. A cannot access it as it is not marked with Scope X.

When both B and C are popped off the backstack and there is only A left, the ViewModelStoreOwner for Scope X will be cleared and removed.

Note that if you replace B and C with a new destination D that is also marked with Scope X, the ViewModelStoreOwner will not be recreated, but left as is.

In order to use scoping NavHost, you need to implement NavScopeSpec and pass it as the scopeSpec parameter. NavScopeSpec requests a set of scopes for each destination in the backstack:

@Parcelize\ndata object ScopeX : Parcelable\n\nval DestinationScopeSpec = NavScopeSpec<Destination, ScopeX> { destination ->  when (destination) {\nDestination.B, Destination.C, Destination.D -> setOf(ScopeX)\nelse -> emptySet()\n}\n}\n

Note that a destination may belong to several scopes at once, that's why NavScopeSpec requires you to return a Set.

Scoped ViewModelStoreOwner is implemented by ScopedNavHostEntry class. You can acquire all scoped entries associated with the current destination through the ScopingNavHostScope receiver of the contentSelector parameter:

ScopingNavHost(\ncontroller = navController,\nscopeSpec = DestinationScopeSpec\n) { destination ->\nwhen (destination) {\nDestination.A -> { /* ... */ }\nDestination.B -> {\nval sharedViewModel = viewModel<SharedViewModel>(\nviewModelStoreOwner = scopedHostEntries[ScopeX]!!\n)\n}\nDestination.C -> { /* same code as for B */ }\nDestination.D -> { /* same code as for B */ }\n}\n}\n

You just have to pass this ScopedNavHostEntry as the viewModelStoreOwner parameter of the viewModel method.

Alternatively, you can access scoped ViewModelStoreOwners through the LocalScopedViewModelStoreOwners composition local.

"},{"location":"shared-view-models/#access-viewmodels-of-backstack-entries","title":"Access ViewModels of backstack entries","text":"

If the two previous solutions are not suitable for your case, you may always access ViewModels of neighbour entries directly. Read more about it here.

"},{"location":"view-models/","title":"Entry-scoped ViewModels","text":"

Every unique NavEntry in NavHost provides its own ViewModelStore. Every such ViewModelStore is guaranteed to exist for as long as the associated NavEntry is present in the backstack.

As soon as NavEntry is removed from the backstack, its ViewModelStore with all ViewModels is cleared.

You can get ViewModels as you do it usually, by using composable viewModel from androidx.lifecycle:lifecycle-viewmodel-compose artifact, for example:

@Composable\nfun SomeScreen() {\nval someViewModel = viewModel<SomeViewModel>()\n// ...\n}\n
"},{"location":"view-models/#accessing-viewmodels-of-backstack-entries","title":"Accessing ViewModels of backstack entries","text":"

It is possible to access ViewModelStoreOwner of any entry that is currently present on the backstack. It is done through the the NavHostScope receiver of the contentSelector parameter of NavHost:

@Composable\nfun NavHostScope<Destination>.SomeScreen() {\nval previousViewModel = viewModel<PreviousViewModel>(\nviewModelStoreOwner = hostEntries.find {\nit.destination is Destination.Previous\n}!!\n)\n// ...\n}\n
"},{"location":"view-models/#passing-parameters-into-a-viewmodel","title":"Passing parameters into a ViewModel","text":"

There is no such thing as a Bundle of arguments for navigation entries in this library. This means that there is literally nothing to pass into SavedStateHandle of your ViewModel as the default arguments.

I personally recommend passing all parameters into a ViewModel constructor directly. This keeps everything clean and type-safe.

If you use dependency injections in your project, explore the samples that show how to pass parameters into a ViewModel and inject all other dependencies:

  • Dagger/Anvil/Hilt use @AssistedInject

  • Koin supports ViewModel parameters out of the box and does it charmingly simple

"},{"location":"view-models/#hiltviewmodel","title":"hiltViewModel()","text":"

If Hilt is the DI library of your choice and you want to use hiltViewModel() method that you may already be familiar with from the official Navigation Component, you can add the dependency:

implementation(\"dev.olshevski.navigation:reimagined-hilt:<latest-version>\")\n

It provides a similar hiltViewModel() method that works with the Reimagined library. The only catch is that by default it doesn't know how to pass arguments to the SavedStateHandle of your ViewModel. For this you can use an additional defaultArguments parameter:

val viewModel = hiltViewModel<SomeViewModel>(\ndefaultArguments = bundleOf(\"id\" to id)\n)\n

And in ViewModel you can read this argument as such:

@HiltViewModel\nclass SomeViewModel @Inject constructor(\nsavedStateHandle: SavedStateHandle\n) : LoggingViewModel() {\n\nprivate val id: Int = savedStateHandle[\"id\"]!!\n\n}\n

Tip

Don't forget to annotate your view models with @HiltViewModel annotation.

Warning

Do not pass mutable data structures as defaultArguments and expect the external changes to be reflected through the SavedStateHandle inside a ViewModel, e.g. when trying to return results as described here.

As soon as SavedStateHandle parcelize/unparcelize data once, it becomes the only source of truth for the data it holds.

If you still need to pass mutable data structure into your ViewModel, it would be more reliable to pass it directly as a constructor parameter).

"}]} \ No newline at end of file diff --git a/shared-view-models/index.html b/shared-view-models/index.html new file mode 100644 index 00000000..facac44d --- /dev/null +++ b/shared-view-models/index.html @@ -0,0 +1,25 @@ + Shared - Compose Navigation Reimagined

Shared ViewModels

Sometimes you need to access the same ViewModel instance from several destinations. The library provides multiple ways to achieve this.

Nested navigation

The easiest way to share a ViewModel between several destinations is to use a nested NavHost. Simply collect all required destinations into a separate nested NavHost, and pass a ViewModel of the parent entry to each destination.

However, it may not work for all scenarios. Sometimes it is not desirable or possible to group destinations into a single nested NavHost. For such cases, it would be more convenient to use scoping NavHosts.

Scoping NavHosts

Each NavHost in the library has its own Scoping counterpart. For NavHost it is ScopingNavHost, for AnimatedNavHost it is ScopingAnimatedNavHost, and so on.

Every scoping NavHost gives you the ability to assign scopes to destinations and access scoped ViewModelStoreOwners bound to each of the defined scope. Such scoped ViewModelStoreOwner is created when there is at least one backstack entry marked with the corresponding scope, and removed when there are none of the entries marked with it.

For example, the ViewModelStoreOwner for Scope X will exist only when at least one of the destinations B or C is in the backstack (their positions don't matter). Both B and C can access the same ViewModelStoreOwner instance. A cannot access it as it is not marked with Scope X.

When both B and C are popped off the backstack and there is only A left, the ViewModelStoreOwner for Scope X will be cleared and removed.

Note that if you replace B and C with a new destination D that is also marked with Scope X, the ViewModelStoreOwner will not be recreated, but left as is.

In order to use scoping NavHost, you need to implement NavScopeSpec and pass it as the scopeSpec parameter. NavScopeSpec requests a set of scopes for each destination in the backstack:

@Parcelize
+data object ScopeX : Parcelable
+
+val DestinationScopeSpec = NavScopeSpec<Destination, ScopeX> { destination ->  
+    when (destination) {
+        Destination.B, Destination.C, Destination.D -> setOf(ScopeX)
+        else -> emptySet()
+    }
+}
+

Note that a destination may belong to several scopes at once, that's why NavScopeSpec requires you to return a Set.

Scoped ViewModelStoreOwner is implemented by ScopedNavHostEntry class. You can acquire all scoped entries associated with the current destination through the ScopingNavHostScope receiver of the contentSelector parameter:

ScopingNavHost(
+    controller = navController,
+    scopeSpec = DestinationScopeSpec
+) { destination ->
+    when (destination) {
+        Destination.A -> { /* ... */ }
+        Destination.B -> {
+            val sharedViewModel = viewModel<SharedViewModel>(
+                viewModelStoreOwner = scopedHostEntries[ScopeX]!!
+            )
+        }
+        Destination.C -> { /* same code as for B */ }
+        Destination.D -> { /* same code as for B */ }
+    }
+}
+

You just have to pass this ScopedNavHostEntry as the viewModelStoreOwner parameter of the viewModel method.

Alternatively, you can access scoped ViewModelStoreOwners through the LocalScopedViewModelStoreOwners composition local.

Access ViewModels of backstack entries

If the two previous solutions are not suitable for your case, you may always access ViewModels of neighbour entries directly. Read more about it here.

\ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 00000000..5b05496c --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,58 @@ + + + + https://olshevski.github.io/compose-navigation-reimagined/ + 2023-07-26 + daily + + + https://olshevski.github.io/compose-navigation-reimagined/animations/ + 2023-07-26 + daily + + + https://olshevski.github.io/compose-navigation-reimagined/back-handling/ + 2023-07-26 + daily + + + https://olshevski.github.io/compose-navigation-reimagined/bottom-sheets/ + 2023-07-26 + daily + + + https://olshevski.github.io/compose-navigation-reimagined/dialogs/ + 2023-07-26 + daily + + + https://olshevski.github.io/compose-navigation-reimagined/nav-controller/ + 2023-07-26 + daily + + + https://olshevski.github.io/compose-navigation-reimagined/nav-host/ + 2023-07-26 + daily + + + https://olshevski.github.io/compose-navigation-reimagined/nested-navigation/ + 2023-07-26 + daily + + + https://olshevski.github.io/compose-navigation-reimagined/return-results/ + 2023-07-26 + daily + + + https://olshevski.github.io/compose-navigation-reimagined/shared-view-models/ + 2023-07-26 + daily + + + https://olshevski.github.io/compose-navigation-reimagined/view-models/ + 2023-07-26 + daily + + \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz new file mode 100644 index 00000000..66382f41 Binary files /dev/null and b/sitemap.xml.gz differ diff --git a/view-models/index.html b/view-models/index.html new file mode 100644 index 00000000..a8120f2a --- /dev/null +++ b/view-models/index.html @@ -0,0 +1,27 @@ + Entry-scoped - Compose Navigation Reimagined

Entry-scoped ViewModels

Every unique NavEntry in NavHost provides its own ViewModelStore. Every such ViewModelStore is guaranteed to exist for as long as the associated NavEntry is present in the backstack.

As soon as NavEntry is removed from the backstack, its ViewModelStore with all ViewModels is cleared.

You can get ViewModels as you do it usually, by using composable viewModel from androidx.lifecycle:lifecycle-viewmodel-compose artifact, for example:

@Composable
+fun SomeScreen() {
+    val someViewModel = viewModel<SomeViewModel>()
+    // ...
+}
+

Accessing ViewModels of backstack entries

It is possible to access ViewModelStoreOwner of any entry that is currently present on the backstack. It is done through the the NavHostScope receiver of the contentSelector parameter of NavHost:

@Composable
+fun NavHostScope<Destination>.SomeScreen() {
+    val previousViewModel = viewModel<PreviousViewModel>(
+        viewModelStoreOwner = hostEntries.find {
+            it.destination is Destination.Previous
+        }!!
+    )
+    // ...
+}
+

Passing parameters into a ViewModel

There is no such thing as a Bundle of arguments for navigation entries in this library. This means that there is literally nothing to pass into SavedStateHandle of your ViewModel as the default arguments.

I personally recommend passing all parameters into a ViewModel constructor directly. This keeps everything clean and type-safe.

If you use dependency injections in your project, explore the samples that show how to pass parameters into a ViewModel and inject all other dependencies:

  • Dagger/Anvil/Hilt use @AssistedInject

  • Koin supports ViewModel parameters out of the box and does it charmingly simple

hiltViewModel()

If Hilt is the DI library of your choice and you want to use hiltViewModel() method that you may already be familiar with from the official Navigation Component, you can add the dependency:

implementation("dev.olshevski.navigation:reimagined-hilt:<latest-version>")
+

It provides a similar hiltViewModel() method that works with the Reimagined library. The only catch is that by default it doesn't know how to pass arguments to the SavedStateHandle of your ViewModel. For this you can use an additional defaultArguments parameter:

val viewModel = hiltViewModel<SomeViewModel>(
+    defaultArguments = bundleOf("id" to id)
+)
+

And in ViewModel you can read this argument as such:

@HiltViewModel
+class SomeViewModel @Inject constructor(
+    savedStateHandle: SavedStateHandle
+) : LoggingViewModel() {
+
+    private val id: Int = savedStateHandle["id"]!!
+
+}
+

Tip

Don't forget to annotate your view models with @HiltViewModel annotation.

Warning

Do not pass mutable data structures as defaultArguments and expect the external changes to be reflected through the SavedStateHandle inside a ViewModel, e.g. when trying to return results as described here.

As soon as SavedStateHandle parcelize/unparcelize data once, it becomes the only source of truth for the data it holds.

If you still need to pass mutable data structure into your ViewModel, it would be more reliable to pass it directly as a constructor parameter).

\ No newline at end of file