Skip to content
academia.sh

Lesson 01 / 14

Modular Code Sharing

Modular code sharing splits common code into a shared module and a platform-specific layer. The lesson builds the course's operability measure and measures how a fully shared module can still be disabled on one platform.

Published Updated

In this lesson

  • Explains the layer boundary between the shared module and the platform-specific layer
  • Classifies a feature's operability as full, degraded, deferred, or disabled
  • Reads the nine-record opening measurement and separates the affected records
  • Measures why a fully shared module stays disabled on one platform
Contents

The earlier courses asked how a cross-platform app is built: how code is shared across platforms, how the view tree is updated, who decides the width of a component. For an app in production the real question is different. The same screen opens online on one device and offline on another; on a third the background work is throttled; a fourth runs an old client version. A single app does not run under a single condition.

This course classifies every feature with one question: how well does this feature work under this condition? The first lesson builds the question and ties it to the subject of the first topic, how common code is sliced. In this course a “feature” means an app feature: something the user does, such as browsing a catalog or paying. It does not mean a value a component receives from outside.

The Four Operability Levels

The answer to the question is called operability, and it has four values. Throughout the course these four names do not change.

  • full: every capability the feature needs is present under this condition; the feature works as designed.
  • degraded: a capability is missing; the feature works immediately by doing less in its place, for example by showing cached content.
  • deferred: a capability is missing; the feature’s work is not lost, it is postponed and done when the capability returns.
  • disabled: at least one of the missing capabilities has no fallback; the feature does not work under this condition.

In the output these names are written in ASCII: full, degraded, deferred, disabled. Disabled and deferred are not the same thing. The work of a disabled feature is not done under that condition and does not wait for later either; the work of a deferred feature stays in place.

A rule gives the operability, not a label. The feature counts the capabilities it needs, the condition counts the capabilities present at that moment, and the difference gives the missing ones. For each missing capability the feature may have defined a fallback. The rule asks three questions in order:

  1. If no capability is missing, the feature is full.
  2. If one of the missing capabilities has no fallback, the feature is disabled.
  3. Otherwise the heaviest of the fallbacks of the missing capabilities is chosen; deferred is heavier than degraded.

These three questions live in the core as code. The core is given again in the same form in every lesson of this course; it is read here for the first time.

// operability.mjs — classifies how fully a feature works under a condition
export const LEVELS = ["full", "degraded", "deferred", "disabled"];
const FALLBACKS = ["degraded", "deferred"];

export function feature(name, needs, fallback = {}) {
  return { name, needs, fallback };
}

export function condition(name, has) {
  return { name, has };
}

function judge(item, cond) {
  const missing = item.needs.filter((need) => !cond.has.includes(need)).sort();
  let level = "full";
  for (const need of missing) {
    const given = FALLBACKS.includes(item.fallback[need]) ? item.fallback[need] : "disabled";
    if (LEVELS.indexOf(given) > LEVELS.indexOf(level)) level = given;
  }
  return { level, feature: item.name, condition: cond.name, missing };
}

export function assess(features, conditions) {
  const records = [];
  for (const cond of conditions) {
    for (const item of features) records.push(judge(item, cond));
  }
  return records;
}

export function printLevels(records) {
  for (const r of records) {
    const missing = r.missing.length === 0 ? "-" : r.missing.join(",");
    console.log(`${r.level}: ${r.feature} @ ${r.condition} missing=${missing}`);
  }
}

export function countLevels(records) {
  for (const name of LEVELS) {
    console.log(`${name}: ${records.filter((r) => r.level === name).length}`);
  }
  console.log(`affected: ${records.filter((r) => r.level !== "full").length}`);
}

feature(name, needs, fallback) builds a feature: needs lists the capabilities it needs, and fallback says what it falls back to when a capability is missing. If fallback is not given it is empty; none of that feature’s capabilities has a fallback, and every missing capability drops it to disabled. condition(name, has) builds a condition; has holds the capabilities present under it. A capability is a string (network, background, storage), and the core does not interpret it; it only checks whether it is in has.

assess evaluates every feature under every condition: the condition is the outer loop, the feature the inner one. Each record carries the fields level, feature, condition, and missing. printLevels prints the records; countLevels counts the four levels in a fixed order and gives the affected total on the last line. Affected is the sum of the degraded, deferred, and disabled records: the number of records where the feature does not work as designed. Full records do not enter this number.

Nine Records

The scenario is a store app published on two platforms with shared code. There are three features. Catalog reads content from the network; when the network is missing it falls back to cached content. Checkout takes payment and has no fallback. PhotoUpload sends photos; it needs both the network and background work, and when either is missing it postpones its work. There are three conditions: online carries every capability, offline loses the network, and low-power keeps the network but loses background work.

// main.mjs
import { feature, condition, assess, printLevels, countLevels } from "./operability.mjs";

const features = [
  feature("Catalog", ["network"], { network: "degraded" }),
  feature("Checkout", ["network"]),
  feature("PhotoUpload", ["network", "background"], {
    network: "deferred",
    background: "deferred",
  }),
];
const conditions = [
  condition("online", ["network", "background"]),
  condition("offline", ["background"]),
  condition("low-power", ["network"]),
];
const records = assess(features, conditions);
printLevels(records);
countLevels(records);
full: Catalog @ online missing=-
full: Checkout @ online missing=-
full: PhotoUpload @ online missing=-
degraded: Catalog @ offline missing=network
disabled: Checkout @ offline missing=network
deferred: PhotoUpload @ offline missing=network
full: Catalog @ low-power missing=-
full: Checkout @ low-power missing=-
deferred: PhotoUpload @ low-power missing=background
full: 5
degraded: 1
deferred: 2
disabled: 1
affected: 4

The first three lines are the online condition: every capability is present and all three features are full. Under offline the network is missing. Catalog is degraded because it defined a fallback for the network, PhotoUpload is deferred because it postpones its work, and Checkout @ offline is disabled because Checkout has no fallback. Under low-power the network is present but background work is not: Catalog and Checkout, which do not need background work, are full, and PhotoUpload is deferred.

Of the nine records, five are full, one is degraded, two are deferred, and one is disabled; four are affected. This number is the course’s opening number. Later lessons refer to it and do not change it; each lesson measures its own slice separately. The only record that gives the user nothing is Checkout @ offline. In three of the four affected records the feature keeps working in some form.

SA1. The model imitates a feature’s behavior under a condition only as a set of capabilities. The rule sees the capability and the fallback, not the work itself, the user, or the time. How stale the degraded Catalog may show its cached content, when the deferred PhotoUpload will do its work, and how many times it will retry are not in the model and do not change the operability. The model builds this operability’s rule; the model is not the framework itself.

Shared Module and Platform-Specific Layer

In a cross-platform app the code is split into two parts. The shared module is code written once for both platforms: business rules, the data model, flow logic. The platform-specific layer is code written in each platform’s own language with its own API: camera, notifications, file system, biometrics. The shared module does not reach for these capabilities directly. It states what it wants as a contract, and the platform-specific layer fulfills the contract.

The line between the two parts is called the layer boundary. A well-drawn boundary means the platform-specific code stays small and narrow: the shared module says what it wants and does not know how it is fulfilled on which platform. The snippet below shows a contract and the shared feature that uses it. The snippet is not measured.

// Platform code: shared contract
interface PlatformLayer {
    val hasScanner: Boolean
    fun scan(onCode: (String) -> Unit)
}

class ScanFeature(private val layer: PlatformLayer) {
    fun open(onCode: (String) -> Unit): Boolean {
        if (!layer.hasScanner) return false
        layer.scan(onCode)
        return true
    }
}

ScanFeature is the same code on both platforms. It asks the layer whether the capability exists; if not, it returns false and nothing happens. The two fulfillments of the contract differ by platform. The Android side is in Kotlin, and the iOS side follows the Swift counterpart of the same contract.

// Platform code: Android layer (the scanner is not written yet)
class AndroidLayer : PlatformLayer {
    override val hasScanner = false
    override fun scan(onCode: (String) -> Unit) = Unit
}
// Platform code: iOS layer
final class IosLayer: PlatformLayer {
    let hasScanner = true

    func scan(onCode: @escaping (String) -> Void) {
        // sets up a camera session and passes the code it reads to onCode
    }
}

The model builds this distinction with one function. platformLayer(platform) produces a condition from a platform’s layer: if the layer fulfills a capability, that capability is in the condition’s has list; if it does not, it is not. In a real app, code produces the capability set from the state of the device and the layer; the model follows the same path.

// layers.mjs — produces the capabilities offered by the platform-specific layer
import { condition } from "./operability.mjs";

const LAYERS = {
  ios: ["network", "storage", "scanner"],
  android: ["network", "storage"],
};

export function platformLayer(platform, written = []) {
  return condition(platform, [...LAYERS[platform], ...written]);
}

In the first measurement two features are evaluated against two layers. Both need only capabilities that both layers fulfill.

// main.mjs
import { feature, assess, printLevels } from "./operability.mjs";
import { platformLayer } from "./layers.mjs";

const features = [
  feature("Catalog", ["network"], { network: "degraded" }),
  feature("Profile", ["storage"]),
];
const conditions = [platformLayer("ios"), platformLayer("android")];
printLevels(assess(features, conditions));
full: Catalog @ ios missing=-
full: Profile @ ios missing=-
full: Catalog @ android missing=-
full: Profile @ android missing=-

All four records are full. This is the case where shared code does the same job on both platforms, and it is the common case. The boundary becomes visible where a layer does not fulfill a capability.

The Bounding Measurement

In the bounding measurement one feature is added to the list: Scan. The business logic of the scan feature is in the shared module and is the same code on both platforms. But using the camera requires the scanner capability, and that capability comes from the platform-specific layer. In the scenario the layer is written for iOS and not yet written for Android. No fallback is defined for Scan.

// main.mjs
import { feature, assess, printLevels, countLevels } from "./operability.mjs";
import { platformLayer } from "./layers.mjs";

const features = [
  feature("Catalog", ["network"], { network: "degraded" }),
  feature("Scan", ["scanner"]),
];
const conditions = [platformLayer("ios"), platformLayer("android")];
const records = assess(features, conditions);
printLevels(records);
countLevels(records);
full: Catalog @ ios missing=-
full: Scan @ ios missing=-
full: Catalog @ android missing=-
disabled: Scan @ android missing=scanner
full: 3
degraded: 0
deferred: 0
disabled: 1
affected: 1

The lesson’s focus record is Scan @ android, and it comes out disabled. Scan is the same shared code on both platforms; on iOS it is full. On Android the scanner capability is missing and the feature has no fallback for that capability, so the rule reaches disabled at the second question. This measurement’s affected count is one, not the four of the opening; the two measurements count different sets.

SA2. The model does not say that the camera cannot be used on Android. The scenario only establishes that the scanner contract is not yet fulfilled in this app’s Android layer. scanner is the lesson’s construction, and the core does not interpret it. How much work it is to write the layer is not in the model.

This result differs from the question of M32/K01. That lesson asked how code is distributed across platforms: how much is shared and how much is platform-specific. Operability does not ask that. A fully shared module can also be disabled under one platform’s condition; operability classifies not where the code is written but how well it works under that condition. How much of the code is shared does not change the record, because feature carries no sharing ratio and the rule looks only at needs, fallback, and has. Whether all of Scan’s business logic is shared or only part of it, Scan @ android stays the same.

What changes a disabled record is the platform’s layer. The second measurement has the Android layer write the scanner capability and evaluates the same feature again.

// main.mjs
import { feature, assess, printLevels } from "./operability.mjs";
import { platformLayer } from "./layers.mjs";

const scan = [feature("Scan", ["scanner"])];
printLevels(assess(scan, [platformLayer("android")]));
printLevels(assess(scan, [platformLayer("android", ["scanner"])]));
disabled: Scan @ android missing=scanner
full: Scan @ android missing=-

Not a single line in the shared module changed; only the layer fulfilled the capability, and the record returned to full. Another way out of disabled is to design a fallback. If a fallback such as entering the code by hand had been defined for Scan, the same record would have been degraded. Being disabled says that no fallback was designed.

Summary

  • This lesson’s operability: disabled. In the Scan @ android record the scanner capability is missing and has no fallback; even though the shared module is entirely common, the record comes out disabled.
  • Operability is the output of a rule: full if no capability is missing, disabled if a missing one has no fallback, otherwise the heaviest fallback among the missing ones (deferred is heavier than degraded).
  • In the opening measurement, of the nine records five were full, one degraded, two deferred, and one disabled; four were affected.
  • The shared module states the contract and the platform-specific layer fulfills the capability; the layer boundary is this line.
  • The sharing ratio of the code does not change the record; Scan @ android returned from disabled to full only when the Android layer fulfilled the scanner capability.

Next Step

In this lesson what changed by platform was a capability, and when it was missing the feature was disabled. The next lesson, Design System Adaptation, measures the case where what changes by platform is appearance: a single design language is adapted differently on two platforms, and a date picker works fully in both forms.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close