Upgrade to Pro — share decks privately, control downloads, hide ads and more …

This Way Up: Implementing Effective Navigation on Android

This Way Up: Implementing Effective Navigation on Android

Presented at Droidcon London 2013

Jeff Gilfelt

October 25, 2013
Tweet

More Decks by Jeff Gilfelt

Other Decks in Technology

Transcript

  1. This Way Up
    Implementing Effective
    Navigation
    https://github.com/jgilfelt/ThisWayUp

    View full-size slide

  2. Effective Navigation
    • We always know where we are and how to get
    where we want to go.
    • There is structure based on the relationships of
    information.
    • Navigation mechanisms are predictable, reliable
    and consistent.

    View full-size slide

  3. Consistent Navigation
    • Eliminates the learning process.
    • Gives users a sense of mastery.
    • Makes the device (and Android itself) feel like a
    cohesive experience that people want to
    continue to use.

    View full-size slide

  4. Information Architecture

    View full-size slide

  5. Information Architecture

    View full-size slide

  6. Navigation Design Resources
    Structure in Android App Design (I/O 2013)
    http://www.youtube.com/watch?v=XpqyiBR0lJ4
    Designing Effective Navigation
    http://developer.android.com/training/design-navigation/
    index.html

    View full-size slide

  7. System Navigation

    View full-size slide

  8. Tasks
    • A task is a collection of activities that users
    interact with when performing a certain job.
    • Activities are arranged in a stack (the "back
    stack") in the order in which they are launched.
    • A task is not a process.
    • A task is not an application.

    View full-size slide

  9. Complex Task (Multiple Applications)

    View full-size slide

  10. Complex Task (Multiple Applications)

    View full-size slide

  11. Complex Task (Multiple Applications)

    View full-size slide

  12. Complex Task (Multiple Applications)

    View full-size slide

  13. Complex Task (Multiple Applications)

    View full-size slide

  14. Back Button
    • The current activity is popped
    from the top of the stack (the
    activity is finish()ed).
    • The previous activity in the
    stack is brought forward (the
    state of its UI is restored).
    • Some exceptions:
    • Dismiss the soft keyboard if visible.
    • Dismiss any dialogs or floating windows.
    • Exit any contextual action mode.
    • Pop any local FragmentManager back stack.

    View full-size slide

  15. Home Button
    • Puts the current task in the
    background and takes the
    user directly to the home
    screen.
    • Task Affinity allows the home screen to act as a
    task switcher as well as a launcher.
    • If a background task with the same affinity as
    the launch Activity already exists, it is simply
    brought to the foreground.

    View full-size slide

  16. Honeycomb and
    Beyond

    View full-size slide

  17. Recents Button
    • An efficient way of switching
    between recently used
    applications.
    • Provides a clear navigation
    path between multiple
    ongoing tasks.

    View full-size slide

  18. Recents UI
    • Evolution of the home
    button long-press UI
    present in legacy Android
    versions.
    • Unfortunately a dedicated
    button and the UI itself is
    still optional for OEMs
    building new Android
    devices.

    View full-size slide

  19. Action Bar
    • Introduced standardised navigation modes for
    view switching within an activity.
    • Introduced a new in-app navigation mechanism
    based on the hierarchical relationships between
    screens: Up.

    View full-size slide

  20. Up Button
    • Provides consistent structural
    navigation as opposed to the
    temporal navigation provided
    by the system Back button.
    • Ensures a user always remains
    within your app.
    • Should never be present in the
    topmost (root) activity of your
    application.

    View full-size slide

  21. Up vs Back - Peer Activities with Back

    View full-size slide

  22. Up vs Back - Peer Activities with Back

    View full-size slide

  23. Up vs Back - Peer Activities with Back

    View full-size slide

  24. Up vs Back - Peer Activities with Back

    View full-size slide

  25. Up vs Back - Peer Activities with Back

    View full-size slide

  26. Up vs Back - Peer Activities with Up

    View full-size slide

  27. Up vs Back - Peer Activities with Up

    View full-size slide

  28. Up vs Back - Peer Activities with Up

    View full-size slide

  29. Up - Creating a New Task

    View full-size slide

  30. Up - Creating a New Task

    View full-size slide

  31. Up - Creating a New Task

    View full-size slide

  32. Up - Creating a New Task

    View full-size slide

  33. Up - Creating a New Task

    View full-size slide

  34. Up - Creating a New Task

    View full-size slide

  35. Up - Creating a New Task

    View full-size slide

  36. Implementing Up

    View full-size slide

  37. Implementing Up
    getActionBar().setDisplayHomeAsUpEnabled(true);
    ...
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
    case android.R.id.home:
    // ...
    }
    return super.onOptionsItemSelected(item);
    }

    View full-size slide

  38. Implementing Up - The Wrong Way
    getActionBar().setDisplayHomeAsUpEnabled(true);
    ...
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
    case android.R.id.home:
    // fuck it
    finish();
    }
    return super.onOptionsItemSelected(item);
    }

    View full-size slide

  39. What’s Wrong?
    • We violate the model of structural navigation.
    • Simply finishing the activity could take us
    anywhere - it might even take us out of the app!
    • Android devices provide a Back button for this
    type of navigation, so your app should not add a
    Back button to the UI.

    View full-size slide

  40. Activity Intent Flags
    FLAG_ACTIVITY_BROUGHT_TO_FRONT
    FLAG_ACTIVITY_CLEAR_TASK
    FLAG_ACTIVITY_CLEAR_TOP
    FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET
    FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
    FLAG_ACTIVITY_FORWARD_RESULT
    FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
    FLAG_ACTIVITY_MULTIPLE_TASK
    FLAG_ACTIVITY_NEW_TASK
    FLAG_ACTIVITY_NO_ANIMATION
    FLAG_ACTIVITY_NO_HISTORY
    FLAG_ACTIVITY_NO_USER_ACTION
    FLAG_ACTIVITY_PREVIOUS_IS_TOP
    FLAG_ACTIVITY_REORDER_TO_FRONT
    FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
    FLAG_ACTIVITY_SINGLE_TOP
    FLAG_ACTIVITY_TASK_ON_HOME
    Intent intent = new Intent(this, MyActivity.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
    Intent.FLAG_ACTIVITY_CLEAR_TOP);
    startActivity(intent);

    View full-size slide

  41. Intent.FLAG_ACTIVITY_
    CLUSTERFUCK

    View full-size slide

  42. Intent Flags - Why They Suck
    • It’s not really an API.
    • Some flags only work in exact combinations.
    • Many flags are not relevant for most 3rd party
    apps (unless you’re building a launcher
    replacement).
    • Overlap/conflict with activity launchMode.
    • Confusing documentation.
    • Implementation can become a process of trial
    and error.

    View full-size slide

  43. Navigation API

    View full-size slide

  44. Navigation API
    • Native Up navigation for Jelly Bean (API 16) and
    above.
    • Based on hierarchical metadata specified for
    each in your manifest.
    • The support library provides equivalent
    functionality for earlier Android versions via
    NavUtils.
    • TaskStackBuilder offers additional utilities for
    cross-task navigation.

    View full-size slide

  45. Specifying a Parent Activity (API 16+)
    Use android:parentActivityName attribute:

    ...

    android:name="com.example.myfirstapp.MainActivity" ...>
    ...


    android:name="com.example.myfirstapp.DisplayMessageActivity"
    android:label="@string/title_activity_display_message"
    android:parentActivityName="com.example.myfirstapp.MainActivity" >


    View full-size slide

  46. Specifying a Parent Activity (Support Library)
    Add the element:

    ...

    android:name="com.example.myfirstapp.MainActivity" ...>
    ...


    android:name="com.example.myfirstapp.DisplayMessageActivity"
    android:label="@string/title_activity_display_message"
    android:parentActivityName="com.example.myfirstapp.MainActivity" >

    android:name="android.support.PARENT_ACTIVITY"
    android:value="com.example.myfirstapp.MainActivity" />


    View full-size slide

  47. Implementing Up
    getActionBar().setDisplayHomeAsUpEnabled(true);
    • No additional code is required for API 16+.
    • A small amount of boilerplate code is required in
    onOptionsItemSelected(…) if you are
    supporting earlier Android versions...

    View full-size slide

  48. Implementing Up (Support Library)
    If we know we can never launch the Activity from a
    different task (i.e. deep link), we can trivially call:
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
    case android.R.id.home:
    NavUtils.navigateUpFromSameTask(this);
    return true;
    }
    return super.onOptionsItemSelected(item);
    }

    View full-size slide

  49. Implementing Up (Support Library)
    • If an Activity provides an intent filter that allows
    other apps to launch it, we need to check for this
    condition and start a new task (with appropriate
    back history).
    • Use NavUtils.shouldUpRecreateTask(…)
    to check if we need a new task.
    • Use TaskStackBuilder to create a new task.

    View full-size slide

  50. Implementing Up (Support Library)
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
    case android.R.id.home:
    Intent upIntent = NavUtils.getParentActivityIntent(this);
    if (NavUtils.shouldUpRecreateTask(this, upIntent)) {
    // This activity is NOT part of this app's task, so create a new task
    // when navigating up, with a synthesized back stack.
    TaskStackBuilder.create(this)
    // Add all of this activity's parents to the back stack
    .addNextIntentWithParentStack(upIntent)
    // Navigate up to the closest parent
    .startActivities();
    } else {
    // This activity is part of this app's task, so simply
    // navigate up to the logical parent activity.
    NavUtils.navigateUpTo(this, upIntent);
    }
    return true;
    }
    return super.onOptionsItemSelected(item);
    }

    View full-size slide

  51. Supplying Intent Extras (API 16+)
    If a parent activity expects extras to be supplied,
    override getParentActivityIntent():
    @Override
    public Intent getParentActivityIntent() {! !
    Intent parent = super.getParentActivityIntent();
    parent.putExtra(CategoryActivity.ARG_CATEGORY, "books");
    return parent;
    }
    Or override
    onPrepareNavigateUpTaskStack(…) for
    complete control of the task stack.

    View full-size slide

  52. Supplying Intent Extras (Support Library)
    For Up within the same task:
    Intent upIntent = NavUtils.getParentActivityIntent(this);
    upIntent.putExtra(CategoryActivity.ARG_CATEGORY, "books");
    NavUtils.navigateUpTo(this, upIntent);
    For a new task stack, use editIntentAt(…):
    TaskStackBuilder tsb = TaskStackBuilder.create(this)
    .addParentStack(this);
    // Add the required Intent extras as appropriate
    tsb.editIntentAt(tsb.getIntentCount() - 1)
    .putExtra(CategoryActivity.ARG_CATEGORY, "books");
    // Navigate up to the closest parent
    tsb.startActivities();

    View full-size slide

  53. ActionBarCompat
    • Provides the same native Up functionality as
    Jelly Bean.
    • No onOptionsItemSelected(…) boilerplate
    code is required.
    Activity (API 16+):
    getParentActivityIntent()
    onCreateNavigateUpTaskStack(...)
    onPrepareNavigateUpTaskStack(...)
    onNavigateUp()
    navigateUpTo(...)
    shouldUpRecreateTask(...)
    ActionBarActivity (ActionBarCompat):
    getSupportParentActivityIntent()
    onCreateSupportNavigateUpTaskStack(...)
    onPrepareSupportNavigateUpTaskStack(...)
    onSupportNavigateUp()
    supportNavigateUpTo(...)
    supportShouldUpRecreateTask(...)

    View full-size slide

  54. Implementing Back

    View full-size slide

  55. Implementing Back
    • Do nothing - the system handles Back for us.
    • Avoid overriding onBackPressed() unless you
    have some custom UI state to dismiss - always
    call the super implementation!
    • However, we need to ensure we manage the task
    state and create a “synthetic” back stack in a
    couple of cases...

    View full-size slide

  56. Changes Since Honeycomb (API 11)
    • The back button's behavior is local to the current
    task and should not capture navigation across
    different tasks.
    • Tasks are rooted at the home screen (launcher).

    View full-size slide

  57. The Notification Scenario

    View full-size slide

  58. The Notification Scenario

    View full-size slide

  59. The Notification Scenario

    View full-size slide

  60. The Notification Scenario
    Any previous task that
    has affinity with Gmail is
    replaced entirely

    View full-size slide

  61. The Notification Scenario

    View full-size slide

  62. The Notification Scenario

    View full-size slide

  63. The Notification Scenario
    • We can’t just push the Gmail message activity on
    to the current Play Store task - it makes no sense
    there!
    • Blindly adding it to any existing task for Gmail
    will result in an unpredictable task state.
    • Replacing the task stack entirely makes Back
    more predictable for users.
    • The Recents button is the modern mechanism
    for switching context back to a prior task.

    View full-size slide

  64. So Why Fake History?

    View full-size slide

  65. So Why Fake History?

    View full-size slide

  66. So Why Fake History?

    View full-size slide

  67. Hours or days
    later...
    So Why Fake History?

    View full-size slide

  68. Hours or days
    later...
    So Why Fake History?

    View full-size slide

  69. Hours or days
    later...
    So Why Fake History?

    View full-size slide

  70. Hours or days
    later...
    WTF?
    So Why Fake History?

    View full-size slide

  71. Deep Link From a Notification or Widget
    Notification.Builder builder = new Notification.Builder(this)
    .setContentTitle("Direct Notification")
    .setContentText("This will 'deep link' into a content activity")
    .setAutoCancel(true)
    // Construct a new task stack and supply the pending intent
    .setContentIntent(TaskStackBuilder.create(this)
    .addParentStack(ContentViewActivity.class)
    .addNextIntent(new Intent(this, ContentViewActivity.class)
    .putExtra(ContentViewActivity.EXTRA_TEXT, "Hello world!"))
    .getPendingIntent(REQUEST_CODE, PendingIntent.FLAG_CANCEL_CURRENT));
    NotificationManager nm = (NotificationManager)
    getSystemService(NOTIFICATION_SERVICE);
    nm.notify("direct_tag", R.id.direct_notification, builder.build());
    Use the same pattern to for a home screen widget
    to supply
    RemoteViews.setOnClickPendingIntent(…)

    View full-size slide

  72. Navigation Drawer

    View full-size slide

  73. Navigation Drawer
    • Recently standardised in the
    framework Support Library
    and design guidelines.
    • Unlike top-level action bar
    navigation modes it can be
    accessed from anywhere in
    your app.
    • Allows for cross-navigation
    from lower levels when your
    app has deep navigation
    branches.

    View full-size slide

  74. Navigation Drawer - Root Level
    • The drawer indicator should
    appear alongside the icon or
    logo in the action bar - tapping
    it will open or close the drawer.
    • Navigation targets at the top level should work
    the same as with tab and list navigation modes -
    a view switch should occur (typically
    implemented using Fragments) with no back
    history created.

    View full-size slide

  75. Navigation Drawer - To/From Lower Levels
    • For activities that don’t have a corresponding
    entry in the navigation drawer, Up navigation
    applies as normal. The drawer should be
    accessible using the bezel swipe gesture only.
    • Navigation from the drawer should perform a
    targeted or selective Up and recreate the task:
    TaskStackBuilder.create(this)
    .addParentStack(MainActivity.class)
    .addNextIntent(new Intent(this, MainActivity.class)
    .putExtra(MainActivity.NAV_POSITION, position))
    .startActivities();

    View full-size slide

  76. External Activities

    View full-size slide

  77. The “Unexpected App” Scenario

    View full-size slide

  78. The “Unexpected App” Scenario

    View full-size slide

  79. The “Unexpected App” Scenario

    View full-size slide

  80. The “Unexpected App” Scenario

    View full-size slide

  81. The “Unexpected App” Scenario

    View full-size slide

  82. Hours or days
    later...
    The “Unexpected App” Scenario

    View full-size slide

  83. Hours or days
    later...
    WTF?
    The “Unexpected App” Scenario

    View full-size slide

  84. Launching External Activities
    FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET
    should be used when launching an activity that
    represents a logical break in your application flow:
    Intent externalActivityIntent = new Intent(Intent.ACTION_PICK);
    externalActivityIntent.setType("image/*");
    externalActivityIntent.addFlags(
    Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
    startActivity(externalActivityIntent);
    Already done for us in ShareCompat:
    ShareCompat.IntentBuilder.from(this)
    .setType("text/plain")
    .setText("I'm sharing!")
    .startChooser();

    View full-size slide

  85. Summary
    • Embrace modern navigation conventions - don’t
    just treat your task as an infinite stack of
    unordered activities.
    • Include parent activity metadata in your manifest
    and use the framework navigation APIs provided.
    • TaskStackBuilder is crucial.

    View full-size slide

  86. Summary
    • Don’t test in isolation and consider all navigation
    paths in to and out of your app.
    • The navigation design guidelines are not rules,
    but you should fully understand them and have
    good reasons to not adhere to them.

    View full-size slide

  87. finish();
    https://github.com/jgilfelt/ThisWayUp

    View full-size slide