-
Notifications
You must be signed in to change notification settings - Fork 0
Meteor on Mobile
This is my attempt to gather in one place the various issues for fully supporting Meteor on mobile devices. See also Improve mobile browser experience on the Meteor Roadmap.
I am at this point most interested in identifying tradeoffs. For example, it might not be possible to avoid having a bad user experience on iOS devices when tabs are unloaded to save memory, or it might be an unrealistic amount of work to "fully" support offline data... but I'd like to know the options.
I'm focusing only on web applications here. Some of these issues don't matter for native apps (e.g. running Meteor inside of PhoneGap), and native apps might have other needs not covered here.
See Meteor issue #734: Android artifacts.
When a "click" handler is attached to any element on the page, all elements on the page become clickable. The Android browser highlights "clickable" elements when they are tapped.
Meteor's universal-events document event handler doesn't itself handle the event, but it temporarily attaches a second event handler to the target of the event which does handle the event. (events-w3c.js:117) Apparently attaching a "click" event handler in this way triggers the Android "clickable" handling.
Open questions:
-
Why does universal-events attach an event handler to the target of the event?
-
What functionality would be lost if this wasn't done? (And maybe that functionality, whatever it is, isn't needed on mobile devices...?)
-
Is there some way to turn off Android's default "clickable" handling? (Would
preventDefault
do the trick? But what would it affect if we usedpreventDefault
all the time?)
jquery-events is an experimental stab at a jQuery implementation of universal-events for investigation. While it's not a package I'd recommend using at this point (I haven't investigated what Meteor features might be broken by using it), it does seem to be able to run the "todos" example.
See Meteor issue #691: Touch events.
Adding mobile touch and gesture events to Meteor's event maps.
I haven't worked on this myself, but it looks like it could be an easy project.
This one is not an issue as far as I know, but worth being aware of.
iOS holds off delivering setTimeout and setInterval events in inactive tabs until the tab becomes active again.
Some blog posts say that inactive tabs are "suspended", but this isn't true. The tab will still respond to AJAX and storage events (and other events, as far as I know, if there's a way to deliver them to the tab). Thus for example, an inactive tab can make a long poll request to a server, handle the return result, and make another long poll request... and continue indefinitely as long as the server does. (But if it loses the connection it has no way to set a timeout and try again).
Meteor uses setTimeout(fn, 0)
to run things in the next tick of the event loop, and in an inactive tab the call to fn
won't happen until the tab becomes active again. As far as I know this isn't much of a problem... it does prevent updating the UI in the background, but Meteor is pretty fast anyway.
By "window state" I mean state which is unique to a particular browser window or tab.
Window state is more of an issue on mobile devices compared to the desktop because mobile browsers will unload inactive tabs to save memory. Thus if the application doesn't save the window state, the user will lose state merely by switching between tabs. (This happens more often on older devices with less memory or when the user is loading large pages in other tabs).
The window state is closely related to Meteor's Session
, because Session
is associated with the browser tab (each tab has its own Session
), and Session
is persisted across hot code reloads. However, not everything in Session
is necessarily unique to the tab.
For example, in the "todos" example, the currently selected list id is stored in the URL and reflected in the Session list_id
variable. This state is not "window state" by my definition (it is URL state instead): if the URL is opened in a new browser, it is the URL which determines the list id, not the (currently empty) window state.
As another example of a distinction, suppose the user is typing in some text, and the developer wants to ensure that the text isn't lost just because the user switches between tabs. One mechanism might be to store the draft text in the window state (assuming the window state is preserved). Or, the developer might like to save the draft to the server so that the user still has the draft when switching devices.
Other kinds of state is state associated with the browser across all tabs (e.g. is the user logged in or not) and state associated with a user across all devices a user is logged into (e.g. their username). "Is it possible to support window state" is one question, and "do we want to associate this particular piece of state with the window, URL, browser, or user" is another question.
A question is why not make all state URL state? That is, store all state in the URL (or a hash of the state in the URL), and all state becomes tied to the URL instead of to the particular window tab. This has an appealing simplicity, but leads to some odd behavior. For example, in the "todos" application whether or not I'm editing a todo title is part of the state. If that state were URL state, then if I shared my URL with someone else and they opened that URL, they'd also be editing the title.
And, the URL is connected to the browser Back button; clicking the Back button changes the URL to a previous URL. This is good if the URL specifies where you are in the application (looking at this list or that list), but weird if clicking the Back button means that you're editing something again. (Imagine if you were writing an email, and you clicked "send", the email is sent, and then you clicked the Back button to get back to what you were doing before... and you were returned to editing the email).
So ideally (if it's possible) it would be nice if the developer could choose which state to associate with the URL and which with the window.
The HTML5 session storage
feature would be the natural place to save window state (and Meteor does use session storage for persisting Session
across hot code reloads). However on iOS, when a tab is unloaded to save memory, the session storage is also cleared. This makes session storage useless for preserving state as the user switches between tabs.
If something in the tab were preserved across reloads (for example, if window.name
were preserved), then that could be used to uniquely identify the tab, and as key into another kind of browser storage which isn't cleared when tabs are unloaded. But, I didn't find anything that was: as far as I can tell, everything is cleared when the tab in unloaded (except for the URL), and there's nothing that can uniquely identify the tab.
This appears to be an unsolved problem for web applications on iOS, as far as I know. In the Amazon Kindle Cloud Reader web app, for example, you can open book A in one tab and book B in a second tab, but if the first tab was unloaded and you return to it, the app displays book B. In Google Reader if you open a post, switch tabs, and come back after the Reader tab had been unloaded the post is no longer open.
Some may wonder if this is very important. I believe that it is, if you want to be able to use Meteor for serious applications that involve making updates (and don't want to have to publish your app through the app store). Losing ones place isn't too terrible in a passive application like the Kindle Cloud Reader, is annoying in Google Reader, and really terrible when trying to do any kind of data entry.
I did have a clever idea for emulating session storage on iOS, which I described on meteor-core. This is still waiting for an implementation though.
While somewhat orthogonal to mobile (desktop web apps can be offline, and mobile apps can be online-only), people do tend to want to especially use mobile apps offline.
The appcache project implemented an offline application cache for Meteor, which allows the static parts of an application (the HTML, Javascript, CSS, and images) to be cached offline.
The app cache isn't too useful by itself (well, it does have some secondary advantages such as lessening the page refresh time on hot code reloads :), but it is a necessary component of supporting offline use.
Even if you didn't care able being able to launch an application offline, and just wanted your Meteor app to be able to survive losing the Internet connection occasionally, the app cache would still be important. The tab unloading behavior means that without the app cache the browser needs to be able to connect to the server to reload the app if it's been unloaded.
A Meteor Collection within a window tab looks something like this:
browser Internet server
.................. ........... ......
Window: Collection <---DDP---> Server
That is, within a browser window, there's an in-memory Collection which communicates with the server over a livedata connection using DDP.
Perhaps the most obvious way to support offline data is to back the in-memory Collection with a browser database (we can use local storage, SQL, or IndexedDB):
browser Internet server
.......................... ........... ......
Window: DB <--> Collection <---DDP---> Server
This might take a lot of work, but let's assume for a moment that it's doable.
However, things get more complicated if more than one window is open:
browser Internet server
............................ .............. ......
Window 1: DB <--> Collection <---DDP---\
|-> Server
Window 2: DB <--> Collection <---DDP---/
When the application is online, the separate windows share state through the server: an update made in one window is propagated to the server and then back to the other window. If, for example, I have a list of projects in one window and I have list of tasks for a project in another window, and I complete a project by finishing all the tasks, then the project status will update in the window showing the list of projects. If I'm offline the windows won't show changes made in other windows.
Now naturally there are two directions we could go here. One is to say that sharing updates between windows while offline is a "nice to have" but not super important; or, of course, to say that it is important.
Going down the road of leaving update unshared however leads to some weird-ish edge cases and user experience issues. For example, suppose I make an update to a project in the project window, and then I do some other stuff, and then I switch to the project list window, and I navigate to the project... and I don't see my changes. So it looks like my changes have been lost (even though I would see them if I switched to the other window).
OK, that's a fairly natural consequence of not sharing state between windows. But what happens if I close a window? Do I lose all my work, or do my changes get saved?
Suppose we save changes (which does sound more appealing). What happens if I have changes saved from two different windows that got closed? Presumably I'd want to see all my changes. But the two windows that got closed may not have the same data... perhaps one got an update from the server before the Internet went offline and the other didn't. So we have changes, generated by method stubs, which were running against different data.
So, conceptually at least, having windows share state is probably a lot easier... perhaps something vaguely like this:
browser Internet server
....................................... ........... ......
Window 1: Minimongo <---\ (DB)
|-> Collection <---DDP---> Server
Window 2: Minimongo <---/
Architecturally this is probably very hard. Even the idea of having windows share a single web socket connection runs into a bunch of issues (in reality the connection has to be made by some window, so what happens if that window becomes inactive or is unloaded or is closed; how would other windows detect the problem and elect another window to reopen the connection; etc. etc.)
But, if the architectural issues are solved (see e.g. browser-msg :-)... I'm imagining the high-level implementation might not be that bad. Suppose, for a moment, that we could run the central part of the diagram above in a separate process, which would communicate with individual windows via message passing and with the server through the web socket. There would be some details to figure out (where do method stubs run?), but if for example we were programming in Erlang ^_^, it might not be too hard to figure out a protocol.