Skip to content
awwx edited this page Mar 1, 2013 · 27 revisions

This is my attempt to gather in one place the various issues and my thoughts on fully supporting Meteor on mobile devices. See also Improve mobile browser experience on the Meteor Roadmap.

I'm focusing only on web applications here. Some of these issues don't matter for native apps, and native apps might have other needs not covered here.

Unwanted highlighting on Android

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 used preventDefault 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.

Touch events

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.

Excessive reconnect timeout

See Meteor issue 696 Excessively long timeout on stream reconnect.

In my tests Firebase took about a minute to reconnect after restoring the Internet connection (which in my opinion is too long). Pusher reconnected instantly (I was turning "airplane mode" on and off, which is visible through navigator.onLine, so I speculate maybe Pusher was using that transition to know when to try to reconnect).

Continuous Internet Activity Spinner in iOS

On iOS devices, the "Internet Activity" spinner runs continuously.

This appears to be be a result of using long polling. The spinner is not active when using web sockets and just waiting for data. According to this presentation and Meteor issue #332 Enable WebSockets and Streaming in Meteor, A) Using web sockets with a 3G connection on iOS can crash Safari, and B) there's is no way to detect from the browser whether the Internet connection is 3G or not.

On my iPad connecting over WiFi, both Firebase and Pusher use a web socket (and the Internet activity icon doesn't spin continuously). I don't have an iOS device with 3G, so I don't know if Firebase or Pusher have solved A or B, or if they let the browser crash.

Battery life and data charges

Mobile devices range from tablets used to stream movies over Wifi (where the additional overhead of a Meteor data connection is negligible) to phones connecting over 3G or even 2G (where having the network connection powered up puts a noticeable load on the battery, and data charges may be expensive).

A thought is to have a "low power" mode which for example polls occasionally instead of continuously. On Android it's possible to detect whether the device is connecting over 3G using navigator.connection.type. Perhaps for devices where we can't detect the connection type there could be a UI to allow the user to choose to put the app into low power mode. We could also go into "low power" mode in inactive tabs (though on iOS this happens today anyway because of the suppression of timeout events).

For myself, the kinds of applications where I care most about Meteor's real-time capabilities and sophisticated UI tend to be more on the "tablet and WiFi" side of the spectrum than on phones with limited bandwidth. Thus I'd prefer to see "full power" mode be the default (so that Meteor works just as well on tablets as it does on the desktop), with "low power" mode available for when we can detect that it should be used or when requested by the user.

(As a side note, native iOS apps are able to detect 3G, so Meteor apps using PhoneGap would be able to put themselves into "low power" mode).

Remote debugging

Debugging is harder on mobile devices without the extensive developer tools that desktop browsers have, so a mechanism for remote debugging might be nice. (Which might be nothing more than a suggestion to use e.g. jsconsole).

setTimeout in inactive iOS tabs

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.

If having setTimeout(fn, 0) not fire in inactive tabs turns out to be undesirable for some reason, there is a setImmediate polyfill library, which I've wrapped for Meteor.

Window state

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).

Meteor's Session

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.

Why not just use the URL?

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.

HTML5 Session Storage

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.

Emulating Session Storage

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.

Offline Support

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.

Offline application cache

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.

Offline data

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, at a conceptual level 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 may be 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.

Such a "separate process" sounds a lot like shared workers, which interestingly enough is actually supported on recent versions of iOS, though not Android yet. The shared worker model has the nice property that it is the only process talking to the server and is also the only process talking to the browser database -- so we're not worried about locks or mutating shared state.

Can we emulate the shared worker on devices that don't support it yet... a shared worker polyfill so to speak? Perhaps. It does at least encapsulate the issues I mentioned above (how would one window be elected to make the connection, etc.)... the code that was running in the "shared worker" would itself not be worrying about which window it was running in.

Clone this wiki locally