Skip to content
academia.sh

Lesson 07 / 10

Platform Design Language Differences

The two platforms do the same job with different design languages. The lesson classifies a single function that picks the navigation pattern from the screen type, and gives different results on each platform, as branched, then measures how an adapted design raises sharing.

Published Updated

In this lesson

  • Classify a feature as branched when the same code gives different results depending on the platform
  • Recognize the navigation pattern, back behavior, and tab placement as differences in the platforms' design languages
  • Measure that writing the same behavior as two separate functions yields split, not branched
  • Explain that adapting the design to one pattern on both platforms raises sharing, while the deviation from the design language goes unmeasured
Contents

The previous topic compared the approaches and, in its last lesson, listed the selection criteria. One of those criteria was whether the interface would look the same on both platforms or each platform in its own language. This topic opens up the facts behind that question, and the first fact is the design language. The course’s question does not change: how is this feature’s code shared between the platforms? The four forms of the answer and the names in the core were introduced in the opening lesson; they are not redefined here, only referred to.

A short reminder. If the same code gives the same result in every sample, the feature is shared. If the same code gives a different result per platform in at least one sample, it is branched. If the feature is written as two separate function objects, it is split, even when the results come out the same. If it is missing on one platform, it is single. The umbrella name is sharing; when we speak of a feature’s sharing, we mean one of these four answers.

The core supplies the rule and arrives in every lesson as the same file. The label does not decide the sharing; the rule does, and it is never assigned by hand.

// sharing.mjs — classifies how a feature's code is shared between the platforms
export const PLATFORMS = ["ios", "android"];

export class Sharing {
  constructor(label, impl, samples) {
    this.label = label;
    this.total = samples.length;
    this.missing = PLATFORMS.filter((p) => typeof impl[p] !== "function");
    const present = PLATFORMS.filter((p) => !this.missing.includes(p));
    this.code = new Set(present.map((p) => impl[p])).size;
    if (this.missing.length > 0) {
      this.differ = null;
      this.verdict = "single";
      return;
    }
    this.differ = samples.filter((s) => {
      const outs = PLATFORMS.map((p) => JSON.stringify(impl[p](s, p)));
      return new Set(outs).size > 1;
    }).length;
    if (this.code > 1) this.verdict = "split";
    else this.verdict = this.differ === 0 ? "shared" : "branched";
  }

  line() {
    const detail = this.differ === null
      ? `missing=${this.missing.join(",")}`
      : `diff=${this.differ}/${this.total}`;
    console.log(`${this.label}: code=${this.code} | ${detail} -> ${this.verdict}`);
  }
}

export function countSharing(items) {
  for (const name of ["shared", "branched", "split", "single"]) {
    console.log(`${name}: ${items.filter((s) => s.verdict === name).length}`);
  }
}

What a Design Language Separates

A design language is the set of shared look-and-behavior rules a platform applies to the interface. On one platform, users have learned how screens open, how they close, and where the main navigation sits. When the same job is done differently on another platform, it looks unfamiliar rather than wrong.

The difference shows up in three places. The first is the navigation pattern: moving between the main sections can be done with a bottom tab, a side drawer, or a screen stack, and each platform has its own habit. The second is back behavior: on one platform, swiping from the edge of the screen goes back, while on the other the system has a back gesture or button. The third is the placement of the tab bar and the toolbar: the same tabs may sit at the bottom on one platform and at the top on the other.

Cross-platform code meets this difference in one of two ways. Either a single function looks at the screen type and the platform and picks the pattern, or each platform writes its own function. The first is branched, the second is split. Both can give users a look that suits their platform; the difference lies in how the code is shared.

One Function, Two Patterns

The model is a single function that picks the navigation pattern from the screen type (navigationStyle). The samples are screen types: home, list, detail, settings. The function is given as the same object on both platforms, so there is exactly one piece of code. On the home screen the two platforms pick different patterns, and so they do on the settings screen; on list and detail both pick the stack.

// main.mjs — one function that picks the navigation pattern from the screen type
import { Sharing } from "./sharing.mjs";

const screens = ["home", "list", "detail", "settings"];

function navigationStyle(screen, platform) {
  if (screen === "home") return platform === "ios" ? "bottom-tab" : "bottom-bar";
  if (screen === "settings") return platform === "ios" ? "stack" : "drawer";
  return "stack";
}

const nav = new Sharing("navigation-style", { ios: navigationStyle, android: navigationStyle }, screens);
nav.line();
navigation-style: code=1 | diff=2/4 -> branched

There is one piece of code, but in two of the four screens the result changes with the platform. By definition this is branched. The function looks at the platform parameter itself; shared code and the platform difference meet in the same place. A team usually keeps this structure in a single file and leaves the platform difference readable inside an if.

What has the rule seen here, and what has it not? It has seen that the code is one and that the results differ in two samples. It does not see how large the function has grown: a function with two branches and a function with twenty branches fall into the same class. Prose says that part.

PR1. The model builds this sharing’s rule; the model is not the framework itself. In the model, a navigation pattern is a string and nothing is drawn on screen. In a real application, the framework’s navigation layout makes the same decision, and the platform code looks something like this.

// Platform code: TabRoot
import SwiftUI

struct TabRoot: View {
    var body: some View {
        TabView {
            HomeScreen().tabItem { Label("Home", systemImage: "house") }
            SettingsScreen().tabItem { Label("Settings", systemImage: "gear") }
        }
    }
}
// Platform code: AppScaffold
@Composable
fun AppScaffold(navController: NavHostController) {
    Scaffold(bottomBar = { BottomBar(navController) }) { padding ->
        NavHost(navController, startDestination = "home", modifier = Modifier.padding(padding)) {
            composable("home") { HomeScreen() }
            composable("settings") { SettingsScreen() }
        }
    }
}

The two pieces write the same home screen in two separate native UI languages. The pieces produce no output; the only thing measured is the choice in the model. If a shared upper layer existed over these two pieces, there would be a single function asking it for the pattern, like navigationStyle in the model.

Three Features, Three Answers

A single feature does not make a table. Take three features together on the same set of screens: back behavior, tab placement, and brand color. Back behavior differs by platform on the home screen and on the others. Tab placement only means something on screens that carry tabs. Brand color is a constant, independent of the platform.

// main.mjs — three features, the same four screens
import { Sharing, countSharing } from "./sharing.mjs";

const screens = ["home", "list", "detail", "settings"];

function backBehavior(screen, platform) {
  if (screen === "home") return platform === "ios" ? "none" : "exit-app";
  return platform === "ios" ? "edge-swipe" : "system-back";
}

function tabPlacement(screen, platform) {
  if (screen === "home" || screen === "list") return platform === "ios" ? "bottom" : "top";
  return "none";
}

function brandColor() {
  return "blue";
}

const rows = [
  new Sharing("back-behavior", { ios: backBehavior, android: backBehavior }, screens),
  new Sharing("tab-placement", { ios: tabPlacement, android: tabPlacement }, screens),
  new Sharing("brand-color", { ios: brandColor, android: brandColor }, screens),
];
rows.forEach((r) => r.line());
countSharing(rows);
back-behavior: code=1 | diff=4/4 -> branched
tab-placement: code=1 | diff=2/4 -> branched
brand-color: code=1 | diff=0/4 -> shared
shared: 1
branched: 2
split: 0
single: 0

Back behavior differs in all four screens, tab placement in two. Both are a single function and both are branched; to be branched, a feature does not need to differ on every screen, since one sample is enough. Brand color differs on no screen and is shared. A small rule shows up here as well: a function that never reads the platform and returns a constant always comes out shared. The reason is not that the function is well shared, but that it carries nothing that could differ.

The Same Behavior, Two Functions

Back behavior can be written in another form. This time each platform has its own function: one knows the edge swipe, the other knows the system back gesture. The results are identical to the previous block, but the feature is now two separate function objects.

The second feature is more striking: the format of the screen title. The functions on the two platforms give the same result, yet they are written separately. The rule counts the number of objects, not the result, so this one is split as well.

// main.mjs — the same behavior, two separate functions
import { Sharing } from "./sharing.mjs";

const screens = ["home", "list", "detail", "settings"];

function iosBack(screen) {
  return screen === "home" ? "none" : "edge-swipe";
}

function androidBack(screen) {
  return screen === "home" ? "exit-app" : "system-back";
}

const titleIos = (screen) => screen.toUpperCase();
const titleAndroid = (screen) => screen.toUpperCase();

new Sharing("back-behavior-split", { ios: iosBack, android: androidBack }, screens).line();
new Sharing("title-format", { ios: titleIos, android: titleAndroid }, screens).line();
back-behavior-split: code=2 | diff=4/4 -> split
title-format: code=2 | diff=0/4 -> split

Back behavior came out split this time, yet the results are the same as in the previous block: it differs in all four of four screens. Only the form of the code changed. The title format is an even clearer example: the two functions give the same result, the difference is zero, and still the code is two. Here the rule says two things together: the result is shared, the code is split.

The cost of this distinction is real. In a branched feature, the platform difference is read in one place; in a split feature, keeping the same difference up to date means updating two functions together. The rule does not see this cost: a three-line split and a three-hundred-line split fall into the same class. Prose says how large it is.

The Bounding Measurement

Instead of leaving the design to the platforms, adapt it to the same pattern on both platforms. A bottom tab on the home screen, a stack everywhere else: both platforms make the same decision. The function is one, and the result is the same on every screen.

// main.mjs — an adapted design that gives the same pattern on both platforms
import { Sharing, countSharing } from "./sharing.mjs";

const screens = ["home", "list", "detail", "settings"];

function navigationStyle(screen, platform) {
  if (screen === "home") return platform === "ios" ? "bottom-tab" : "bottom-bar";
  if (screen === "settings") return platform === "ios" ? "stack" : "drawer";
  return "stack";
}

function adaptedNavigation(screen) {
  return screen === "home" ? "bottom-tab" : "stack";
}

const rows = [
  new Sharing("navigation-style-by-platform", { ios: navigationStyle, android: navigationStyle }, screens),
  new Sharing("navigation-style-adapted", { ios: adaptedNavigation, android: adaptedNavigation }, screens),
];
rows.forEach((r) => r.line());
countSharing(rows);
navigation-style-by-platform: code=1 | diff=2/4 -> branched
navigation-style-adapted: code=1 | diff=0/4 -> shared
shared: 1
branched: 1
split: 0
single: 0

The same feature was branched in the first row and became shared in the second. The code did not change, it is still a single function; what changed is that the function stopped splitting by platform. Sharing went up: from branched to shared.

What the rule cannot see is the cost of this move. The shared pattern deviates from the design language of one of the platforms: a bottom tab on the home screen may match habit on one platform and not on the other. The model does not measure this deviation and cannot, because user habit is not the result of a function call. What is measured is only this: the shared pattern gives the same result on both platforms. How far the design has moved from habit is stated in prose, and it is a decision the team weighs itself.

PR2. The model measures that a pattern gives the same result on both platforms; it does not measure whether users find that pattern natural. This assumption does not stand in for a design decision. Knowing that a shared sharing contains a deviation from the design language does not say that the deviation is acceptable.

Summary

  • This lesson’s sharing: branched. The single function that picks the navigation pattern from the screen type gave a different pattern depending on the platform in two of the four screens.
  • Across four measurements, branched, split, and shared came out: back behavior and tab placement were branched, the same back behavior written as two functions was split, and brand color was shared.
  • A function that does not read the platform and returns a constant always comes out shared; this shows not that the function is well shared but that it carries nothing that could differ.
  • The title format came out split even though two functions gave the same result: the rule counts the form of the code, not the result.
  • Adapting the design to one pattern on both platforms raises sharing from branched to shared; the deviation from the design language is stated by prose, not by the rule.

Next Step

The navigation pattern could be chosen from shared code because the choice was only a string. A job that touches the platform’s own capability cannot be solved this way: shared code cannot make a biometric authentication request on its own. The next lesson, “The Need for Native Modules,” measures how the code that makes such a request is shared between the platforms.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close