Appearance
Work with screens
The Simulator is screen-first: you do what a person does in the app, one screen at a time. This page explains the handles you get; the screen pages list what a person can do on each screen.
- A screen handle is a snapshot of one open screen.
handle.datais what the screen shows,handle.rawthe answers of Instagram behind it, and the handle's methods are the actions a person can take there (feed.openReelsTab(),post.like(),comments.comment('…')). A list on a screen is a list of item handles with their own actions (inbox.threads[0].open()). - You open a screen through an edge: an action or an item's
open()that leads somewhere. The call returns the new screen's handle. - Narrow with the guard. A union of screens is narrowed with the screen's own guard,
ReelsViewerScreen.is(screen), and a list item with the item's guard,FeedTimelinePostItem.is(item). This is the one style on every page: the guard narrows the type exactly, and a screen or an item that is renamed fails to compile instead of silently never matching. - Close a screen with
back(). A screen opened over another one hasback(); a root screen (the launch's first screen, a bottom tab's root) has none.
IMPORTANT
Only the screen in focus can act. The screen in focus is the one on top; a person cannot tap a screen another one hides, so the Simulator never sends a request the app could not. Acting on a covered screen (swiping the Reels viewer while its comments sheet is open) is a defect, ScreenNotOnTopError, and its message names the screen that is in focus. It is a hint, not a trap: close what covers the screen (back()), then keep working. A covered screen is not lost: once the cover is closed, the very same handle works again. A handle whose screen was closed, opened again as a new frame or replaced by an updating action's answer never works again; continue with the handle that call returned.
Keep working through your own variable. You already hold the handle of the screen you came from, and it is the right one to continue with once the screen over it is closed:
ts
// The "Work with screens" example shows, typechecked as written.
import { AndroidIgpapi, FeedTimelineScreen } from '@igpapi/android';
import { Effect } from 'effect';
import { readFile } from 'node:fs/promises';
// A checkpoint read back from your storage (see "Keep a session").
const saved = JSON.parse(await readFile('session.json', 'utf8'));
await AndroidIgpapi.launch({
checkpoint: saved,
screen: FeedTimelineScreen,
main: feed =>
Effect.gen(function* () {
// The Reels tab always opens on the viewer: a comments sheet left open
// over it is closed, as the app does on a tap of the selected tab.
const viewer = yield* feed.openReelsTab();
const reel = viewer.current;
if (reel === undefined) return;
// The comments sheet opens over the viewer and takes the focus.
const comments = yield* reel.openComments({});
// `yield* viewer.swipeToNext()` here would die with `ScreenNotOnTopError`:
// the viewer is covered, and a person cannot swipe a reel under a sheet.
yield* comments.comment('Great reel!');
// Close the sheet; the viewer is in focus again. Keep using your own
// variable: it is the same screen, so there is nothing to re-fetch.
yield* comments.back();
yield* viewer.swipeToNext();
}),
});back() also returns the screen underneath, typed as the union of the screens that can open the one you close (comments.back() is the post or the Reels viewer). That is the tool for when the previous screen is not in hand (a handle restored from a checkpoint, a helper that was given only the top screen, a second back()), and you narrow it with XxxScreen.is(previous). When you do hold the previous screen, ignore the return value and use your variable.
Deeper: How the Simulator works, The app process, Ways to start the app, Keep a session and Lists.
What a handle holds
A handle keeps what the screen shows, and an item what its entry shows, under data, apart from its methods: profile.data.username, post.data.mediaId. Everything the screen shows is under data. The one main identifier is also on the top level (a profile's profile.userId, an item's key from its list), and a list's items are on the top level too (inbox.threads, results, items). JSON.stringify(handle) is the data's JSON, so a handle serialises as what it shows.
A screen's data carries Instagram's own fields where the screen shows them unchanged. That is why some are in snake_case (thread_title, item_type, is_group) and some are camelCase (mediaId): the names Instagram uses stay as Instagram wrote them.
handle.raw is the answers of Instagram that the visit received, or an item's own undecoded entry, and the result of an action (thread.sendMessage('hi'), post.like()) carries the answers its run received under the same raw. A handle carries the process's services, so its actions (logIn, recoverPassword, nextPage, back) run without providing a layer.
Lists and feeds on a screen
A feed screen's handle is one page of the feed: items holds that page's items, and nextPage() returns a new handle for the next page, or undefined at the end of the feed. Every feed also has one stream, a lazy Stream of its items.
A feed where one item is in front and a swipe brings the next (the home feed, the Reels viewer) is swipeable: its handle also has current, the item in front (undefined when there is none), and swipeToNext(), which brings the next item in front and returns it, or undefined at the end of the feed. Viewing is the item's visit: it starts when the item comes in front and ends when a swipe moves it away, so there is no separate viewing action. The handle is a snapshot, so current stays the item that was in front when the handle was taken, while swipeToNext() always moves on from where the screen is.
ts
import { AndroidIgpapi, FeedTimelineScreen } from '@igpapi/android';
import { Effect, Stream } from 'effect';
import { readFile } from 'node:fs/promises';
const saved = JSON.parse(await readFile('session.json', 'utf8'));
const summary = await AndroidIgpapi.launch({
checkpoint: saved,
screen: FeedTimelineScreen,
main: feed =>
Effect.gen(function* () {
// `nextPage()` is a new handle for the next page, one page request as the app sends it,
// or `undefined` at the end of the feed. It never changes the handle it is asked of.
const second = yield* feed.nextPage();
// The home feed is swipeable: `current` is the item in front, and `swipeToNext()` brings the next one
// in front (requesting the next page itself when it must) and returns it.
const keys: string[] = [];
let item = feed.current;
for (let swipes = 0; item !== undefined && swipes < 5; swipes += 1) {
keys.push(item.key);
// Wait between swipes, as a person does.
yield* Effect.sleep('3 seconds');
item = yield* feed.swipeToNext();
}
// Every feed also has one `stream`, a lazy Stream of its items; this one walks the feed by swipes.
const firstThree = yield* feed.stream.pipe(Stream.take(3), Stream.runCollect);
return { secondPageItems: second?.items.length ?? 0, keys, streamed: firstThree.length };
}),
});
console.log(summary);A feed item opens with open(), wherever its kind leads: a post opens the post screen, a Reel the Reels viewer.