Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
156471 stories
·
33 followers

OpenAPI Is the Unit of API Governance

1 Share

If landscape mapping is where governance starts, OpenAPI is where it lives. It is the center of gravity for everything I do when I govern REST APIs, and the reason is simple and worth saying out loud: OpenAPI is the machine-readable contract, and the contract is the thing you govern. Not the code. Not the running service. The contract. Once you internalize that, the whole rest of the governance stack snaps into place, because everything hangs off the OpenAPI document. Your style guide describes it. Your Spectral rules lint it. Your reviews are reviews of it. Your pipeline gates on it. Your mocks, your SDKs, and your docs are generated from it. The OpenAPI is the artifact the entire program orbits.

The first thing I hold as non-negotiable is that the OpenAPI is the source of truth, not a byproduct. If your spec is generated as an afterthought from code annotations and nobody on the team ever opens it, you are not governing your API, you are governing a description of an API that already shipped. There is a real difference, and we’ll get into design-first versus code-first later in this series. For now, just know that a spec nobody reads is a spec you can’t govern, no matter how clean the YAML looks.

The second thing, and this is the one that surprises people, is that the descriptions and summaries are the battleground. The info.description, the operation summary and description, the parameter descriptions, the schema property descriptions — this is where engineering and product actually negotiate what the API is. I learned to watch the descriptions like a hawk, because the inability or unwillingness of an engineer to describe their operation in one clean sentence correlates almost perfectly with a poorly designed operation. If you can’t say what it does, you probably didn’t design it well. So the cheapest, highest-signal governance rule you will ever ship is “every operation has a meaningful description,” because it forces design debt to the surface before deployment, when it’s still cheap to fix. I’ll say it as plainly as I can: descriptions are the cheapest form of governance there is.

The third thing is that you have tools for the cases where the contract itself isn’t the right home for something. The tone you need for a contract negotiation between engineering and product is not the tone you need for developer-facing documentation, and trying to make the OpenAPI serve both jobs makes it worse at both. This is what OpenAPI Overlays are for — you augment a spec without editing the source. It’s what extensions are for — you carry governance metadata the spec proper has no place to hold. Use them. Don’t overload the contract with everything; let it be the contract and let the other artifacts do the other jobs.

Here is the part that makes all of this more urgent than it was a few years ago. For a long time the consumer of your OpenAPI was a human developer or a code generator. Increasingly, it’s an AI agent, reading your spec to decide how to call you. A weak, under-described, inconsistent OpenAPI was always a liability, but a human would grind through it. An agent won’t. So if your OpenAPI is weak, fix it first, before you do anything else in this series, because it is about to be the input to every other fundamental — your schema governance, your rules, your pipeline, your reviews — and now it’s the thing a machine reads to figure out how to use your business. Govern the contract, and you’ve started governing everything that depends on it.



Read the whole story
alvinashcraft
just a second ago
reply
Pennsylvania, USA
Share this story
Delete

translateZ()

1 Share

The CSS translateZ() function adds depth to an element, drawing it closer or farther in space. In other words, it shifts an element along the Z-axis in a 3D space.

.box:hover {
  transform: translateZ(100px);
}

.box.perspective:hover {
  transform: perspective(500px) translateZ(100px);
}

Either the perspective() function or perspective property is necessary for translateZ() to work. Without either one, there’s no effect.

Activate the switch in the following demo, then hover over the box to see it appear closer:

While it looks like the .box element is getting bigger” on hover, that’s not what is happening. When you hover over the box, it actually moves closer to you a length of 100 pixels, making it appear larger. We’ll get more into that in just a bit so it’s more obvious what’s happening there.

translateZ() should not be mistaken for an alternative to the scale() function or scale property. Perspective and scale are two different concepts.

The translateZ() function is defined in the CSS Transform Module Level 2 specification

Syntax

translateZ() = translateZ(<length>)

The translateZ() function takes a single <length> argument that defines how far the element is from the front of the screen. It’s used with the transform property

Arguments

/* Positive lengths */
transform: translateZ(100px);
transform: translateZ(5rem);

/* Negative lengths */
transform: translateZ(-50px);
transform: translateZ(-8em);

The translateZ() function takes a single argument:

  • <length>: the distance of the element from the front of the screen. When it is positive, the element moves closer to the user, and further away when it’s negative.

How it works

The translateZ() function is tricky because it moves an element along the Z-axis, which is not visually perceptible in a browser by default. Since browsers only render elements by their height and width, not their depth, the translateZ() function may appear to do nothing.

A three-dimensional plane on top of a grid with perspective, showing the X, Y, and Z axes.

To show its depth and projection, we need to use either the perspective property or perspective() function. Additionally, we can set transform-style to preserve-3d value on its parent, which lets CSS know that child elements should be positioned in 3D.

Projection and perspective

On a website, there isn’t a sense of depth — that is, “closeness” or “furtherness” — since elements are flattened on a 2D screen. Whether it’s translate(200px) or translate(20px), we perceive the element at the same distance, so there isn’t any perspective at all unless we explicitly enable it. To show that, take for example the following HTML:

<div class="scene">
  <div class="parent">
    <div class="box">translateZ(100px)</div>
  </div>
</div>

Firstly, we’ll enable some perspective in the whole scene:

.scene {
  perspective: 800px;
}

Then, we’ll set transform-style to preserve-3d in the parent, so children will also be transformed in 3D:

.parent {
  transform-style: preserve-3d;
}

Now we move the .box 100px closer to the user using translateZ().

.box {
  transform: translateZ(100px);
}

Even though it looks like the box grew 100px in size, when you rotate the parent to the side, you’ll see that the .box size didn’t increase, but it’s the distance between the .parent and the .box that did.

A tall blue rectangle rotated left along the Y-axis adding perspective.

perspective vs. perspective()

Both do the same work: define the element’s projection. The perspective property is applied to the parent for all 3D elements, while the perspective() function can only be applied to a single 3D element, and must be declared before the 3D transform function.

The perspective property

.parent {
  perspective: 800px;
}

.child-1 {
  transform: translateZ(200px); /* Defined within a projection of 800px */
}

.child-2 {
  transform: translate3D(100px, 200px, 150px); /* Defined within a projection of 800px */
}

The perspective() function

.element {
  transform: translateZ(100px) perspective(800px); /* Nope ❌ */
}

.element {
  transform: perspective(800px) translateZ(100px); /* Yep ✅ */
}

It can also be used to optimize web performance

Did you know that you can use the translateZ() function to boost our website’s performance? CSS 3D transform functions use the GPU, which is faster and more superior to the CPU for element rendering. This performance hack prevents flickering during animations and makes transitions smoother.

Developers add translateZ(0) to shift rendering from the CPU to the GPU, improving performance. If you’re struggling with a glitching animation, now you’ve s way to fix it!

Demo

Specification

The translateZ() function is defined in the CSS Transform Module Level 2 specification.

Browser support

The translateZ() function is available on all modern browsers.

References

More on 3D transforms!


translateZ() originally handwritten and published with love on CSS-Tricks. You should really get the newsletter as well.

Read the whole story
alvinashcraft
41 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

translateY()

1 Share

The CSS translateY() function shifts an element vertically by the specified amount. Specifically, it shifts an element either up or down, depending on whether the value is positive or negative.

.parent:hover .box {
  transform: translateY(50%); /* Shift down by half the element's height */
}

Along with other transform functions, it is used inside the transform property.

It is defined in the CSS Transforms Module Level 1 draft.

Syntax

The translateY() function’s syntax looks like this:

<translateY()> = translateY( <length-percentage> )

Just a fancy way of saying: Translate (or move) the element vertically by this much.

Arguments

/* <length> */
translateY(80px) /* Element moves 80px down*/
translateY(-24ch) /* Element moves 24ch up */

/* <percentage> */
translateY(50%) /* Element moves 50% of the element's height downward */
translateY(-100%) /* Element moves 100% of the element's height upward */

The translateX() function takes a single <length-percentage> argument that specifies how far to move the element and in which direction, which can be either up (negative) or down (positive).

It can take either a <length> or a <percentage> argument:

  • <length>: When it’s positive, e.g. 20px, it pushes the element 20 pixels down, while negative values like -20px will push the element 20 pixels up.
  • <percentage>: Percentages are relative to the element’s height, so translateY(40%) on a 100px-tall element pushes the element 40px down, while translateY(-40%) pushes the element 40px up.

Basic usage

The translate functions are ideal for simpler animations since they don’t disturb the document flow. Specifically, the translateY() function is great for “pop-up” or “fade-in” animations where the elements can slide from the bottom. Take for example, a card component (let’s call it .stat-card) that slides up when the user clicks a button or scrolls down.

The components are initially displaced 50 pixels downwards, hidden with an opacity value of 0.

.stat-card {
  /* ... */
  opacity: 0;
  transform: translateY(50px);

  transition:
    opacity 0.8s ease-in,
    transform 0.8s ease-in,
    box-shadow 0.3s ease;
}

Then, when another element (let’s call it .dashboard) becomes .active, the .stat-card components become fully visible and are translated into their original position on the page.

.dashboard.active .stat-card {
  opacity: 1;
  transform: translateY(0);
}

We can even add a “micro-animation” whenever the user hovers over any of the .stat-card by moving it slightly up by 8px, thanks to using a negative value:

.dashboard.active .stat-card:hover {
  transform: translateY(-8px);
}

Focused form field animation

Form fields usually use a blue border to indicate focus, but they can be more interesting. In UI libraries like MUI, the TextField component’s label initially serves as a placeholder, but when a user focuses on the field, it moves to the top and assumes the label’s position.

We can implement a similar animation by applying the translateY() function to the input and label elements.

To start, we initially place the label element within the input element as a placeholder using absolute positioning.

label {
  position: absolute;
  left: 15px;
  top: 15px;

  pointer-events: none;
  transform-origin: left top;
  transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}

Then, when the user focuses on the input field, the label is translated 32px up to serve as a label.

input:focus ~ label,
input:not(:placeholder-shown) ~ label {
  transform: translateY(-32px) scale(0.8);
  color: #6200ee;
  font-weight: bold;
}

The label‘s position is restored when the user loses focus from the input element.

It doesn’t affect other elements

The translateY() function, like other transform functions, does not affect the document flow. Instead, it visually displaces the translated element to a new position without pushing the elements in its surroundings or the ones at the new position. Also, the space the element originally occupied remains reserved in the layout as if it hadn’t moved at all.

/* Translated element */
.translated {
  position: absolute;
  top: 0;
  left: 0;

  transform: translateY(40px);
}

Notice how shifting the “Translated” element does not affect the placement of the other elements around it.

Unlike margin which can trigger reflows or shift neighboring elements, translateY() only changes where the element is visually rendered.

Issues with pointer pseudo-classes

Using translateY() directly on a pointer pseudo-classes like :hover can sometimes break interactions. In a situation where the element is translated far and it moves away from the cursor, the :hover state gets lost, causing the element to snap back immediately to its original position. At the initial position, the cursor is there, so it translates again, resulting in a continuous flickering loop.

A simple solution to this is to place the element to be translated in a parent container, and apply the pseudo-class (:hover) to the parent element, while the main element takes the translate function.

/* Problem case */
.bad:hover {
  transform: translateY(160px);
}

/* Solution */
.parent:hover .good {
  transform: translateY(160px);
}

Demo

Specification

The CSS translateY() function is defined in the CSS Transforms Module Level 1, which is currently in Editor’s Drafts.

Browser support

The translateY() function has baseline support on all modern browsers.


translateY() originally handwritten and published with love on CSS-Tricks. You should really get the newsletter as well.

Read the whole story
alvinashcraft
46 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

translateX()

1 Share

The CSS translateX() function shifts an element horizontally by the specified amount. Specifically, it displaces an element to the right or the left, depending on whether the value is positive or negative.

.parent:hover .box {
  transform: translateX(50%);
}

Along with other transform functions, it is used inside the transform property.

It is defined in the CSS Transforms Module Level 1 draft.

Syntax

The translateX() function has a simple syntax given as:

<translateX()> = translateX( <length-percentage> )

Or, in plain English: Translate (or move) this element horizontally by this much.

Arguments

/* <length> */
translateY(80px) /* Element moves 80px to the bottom */
translateY(-24ch) /* Element moves 24ch to the top */

/* <percentage> */
translateY(50%) /* Element moves 50% of the element's height downward */
translateY(-100%) /* Element moves 100% of the element's height upward */

The translateX() function takes a single <length-percentage> argument that specifies how far to move the element and in which direction, which can be either left (negative) or right (positive).

The argument passed could be either a <length> or a <percentage>:

  • <length>: When it’s positive, say 50px, the element moves 50 pixels to the right. On the other hand, in the case of -40ch, the element moves 40 characters to the left.
  • <percentage>: Percentages are relative to the element’s width. So, for a 400px-wide element, translateX(50%) moves it 200px to the right, while translateX(-50%) moves it 200px to the left.

Basic usage

We can use the translateX() function to make elements slide onto the webpage in lots of ways. For instance, a sidebar that slides in from the left (or right) when clicking a menu button. To achieve this, we initially shift the sidebar completely off the page:

.sidebar {
  transform: translateX(-100%);
  transition: transform 0.2s ease-in;
}

Then, with a little JavaScript, we can toggle an .open class whenever the user clicks on the menu buttons. This moves the sidebar back into the page from the left:

.sidebar.open {
  transform: translateX(0);
}

Example: Infinite marquee

Marquees in web development are information banner components that scroll automatically. On most websites, they are used to display company logos, perhaps sponsors or clients, or, as in this case, announce new arrivals on an e-commerce site.

Similar to the last example, we can use the translateX() function to smoothly scroll a marquee component:

.marquee-content {
  animation: marquee-scroll 20s linear infinite;
}

@keyframes marquee-scroll {
  0% {
    transform: translateX(0);
  }
  100% {
    transform: translateX(-50%);
  }
}

The marquee-scroll keyframes defines how the component scrolls from the start stage to the stop stage, where the component is shifted by half its width towards the left.

To make it scroll infinitely, the animation-iteration-count is set to infinite on the animation shorthand property.

Example: Skeleton layout animation

Skeletons loaders act as placeholders until the main content loads and replaces them, preventing unexpected layout shifts. They can be a static gray div of different shapes and sizes of the original content, or we can make them more interesting with a shimmer effect.

Empty boxes (with a solid color or gradient background) are used as a placeholder while content is being gradually loaded. Text content is loaded and displayed first, and images are loaded and displayed after that.

Using the ::after pseudoelement, we can set a default transform: translateX(-120%);, then use a shimmer animation to move it through the .skeleton component infinitely.

.skeleton::after {
  content: "";
  position: absolute;
  inset: 0;

  transform: translateX(-120%);
  background: linear-gradient(90deg, transparent, var(--skel-highlight), transparent);
  animation: shimmer 1.15s linear infinite;
}

The shimmer keyframe translates the pseudoelement from -120% (off the element from the left) to 120% (out of the page on the right), then starts again

@keyframes shimmer {
  0% {
    transform: translateX(-120%);
  }
  100% {
    transform: translateX(120%);
  }
}

It doesn’t affect other elements

The translateX() function, like other transform functions, does not affect the document flow. Instead, it visually displaces the translated element to a new position without pushing the elements in its surroundings or the ones at the new position. Also, the space the element originally occupied remains reserved in the layout as if it hadn’t moved at all.

/* Translated element */
.translated {
  position: absolute;
  top: 0;
  left: 0;

  transform: translateX(80px);
}

Notice how the “Translated” element does not cause either the Box 1 or Box 3 elements to shift when it moves.

Unlike margin which can trigger reflows or shift neighboring elements, translate only changes where the element is visually rendered.

Issues with pointer pseudo-classes

Using translateX() directly on a pointer pseudo-classes like :hover can sometimes break interactions. In a situation where the element is translated far and it moves away from the cursor, the :hover state gets lost, causing the element to snap back immediately to its original position. At the initial position, the cursor is there, so it translates again, resulting in a continuous flickering loop.

A simple solution to this is to place the element to be translated in a parent container, and apply the pseudo-class (:hover) to the parent element, while the main element takes the translate function.

/* Problem case */
.bad:hover {
  transform: translateX(160px);
}

/* Solution */
.parent:hover .good {
  transform: translateX(160px);
}

Specification

The CSS translateX() function is defined in the CSS Transforms Module Level 1, which is currently in Editor’s Drafts.

Browser support

The translateX() function has baseline support on all modern browsers.


translateX() originally handwritten and published with love on CSS-Tricks. You should really get the newsletter as well.

Read the whole story
alvinashcraft
51 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Maintaining working memory in AI agents

1 Share
&&
TL;DR
  • An LLM has no memory between calls. Its working memory for a single request is one thing: the tokens in the context window.
  • Attention is shared across that working memory, so the more you load into it, the less focus anything in it receives.
  • Reliable behavior comes from managing working memory on purpose: keep what the task needs, clear what it does not.

A language model holds no state between calls. Everything it knows in a given moment is the text in front of it: the system prompt, the conversation so far, the files it has read, the output of its tools. That bundle of tokens, the context window, is the model's entire working memory for that request. There is no other store it can reach.

The obvious lever looks like size. Most models I reach for now offer a million tokens of working memory, and the pitch is to stop curating and let the agent keep everything. I ran agents that way for a while. Reliability broke down when the answer had to be worked out instead of looked up. The teams who tested this carefully found the same pattern: once you remove the literal word overlap that lets a model shortcut "find the needle," accuracy that was near-perfect on short inputs falls toward a coin flip well inside the advertised window.

So the million-token number describes how much working memory the agent will accept, not how much it can hold in focus at once. Those are different quantities, and reliability depends on the second one (see Fig. 1).

FIG. 1Working memory accepted vs. working memory you can trust
≈ 28k reliable 0 128k 256k 512k 1M WORKING MEMORY THE MODEL ACCEPTS
~28k
depth you can trust · approximate
Share of the 1M window3%
Fig. 1 — Read this as a sketch of the idea. The full bar is the working memory the agent accepts; the model takes every token. The bright zone is how deep it stays reliable, and it narrows sharply when the answer does not share words with the question. Drag from a literal lookup to real reasoning: one window, very different usable depth. The exact line shifts with the model and the task.

The large window still earns its place. It absorbs long histories, keeps a multi-step task from hitting a hard cutoff, and leaves room for tools. It also removes the forcing function: nothing stops you from filling it, so the agent will carry three hundred thousand tokens of accumulated context and keep going right up until its answers quietly degrade. Keeping an agent reliable means keeping its working memory clean, and that starts with knowing what the working memory is made of.

What the working memory is made of

Working memory is counted in tokens, the small pieces text is split into, each one roughly three-quarters of a word. A four-thousand-token request is a few pages; a hundred-and-fifty-thousand-token request is a small book.

From text to tokens · this line = 8 tokens
"Why isn't INotifyPropertyChanged firing?"
Why ·isn't ·INotifyPropertyChanged INotifyPropertyChanged → 4 ·firing ?
· marks a leading space; it rides along as part of the token.
Real tokens from o200k, the tokenizer behind GPT-4o, the o-series, and GPT-5, shown as they split inside this sentence. Common words stay whole; one identifier, INotifyPropertyChanged, becomes four tokens. Other models differ: GPT-4 splits it into three, and Claude uses its own tokenizer that Anthropic does not publish, so counts shift model to model. Treat the pattern as the lesson and expect the exact pieces to vary. Your working memory is counted in tokens, not words.

What turns those tokens into behavior is attention. For each token it generates, the model weighs every token already in the working memory and decides how much each one should shape what comes next. Two facts about that process drive everything else.

The working memory is finite. Past the token limit, the tooling around the model drops or summarizes something to make room; nothing waits outside. The limit is the smaller issue. A fact sitting in the middle of a long working memory loses reliability well before you reach the edge of it.

Attention is shared. It is divided across everything present, so adding tokens lowers the focus on each one (see Fig. 2). A sentence in a four-thousand-token request holds far more of the model's attention than the same sentence inside a hundred and fifty thousand. The words are identical; the attention they command is not.

0.4%
share of a fixed signal
Fig. 2 — Picture it this way: if attention were split evenly, a fixed signal of about 800 tokens would lose its share as the working memory grows. Real models do not divide attention so cleanly, but the direction holds. Drag the marker: at a few thousand tokens the signal keeps about a third of the focus; at 200k the same tokens barely register.

You choose what goes in

If the context window is the agent's working memory, then writing a prompt is deciding what it gets to think with. A weak answer is often a working-memory problem before it is a reasoning problem: the fact that mattered was missing, buried, or outnumbered.

That changes a few defaults. Loading an entire codebase or a long document feels thorough, and it lowers the quality of the answer, because the part you care about now competes with thousands of tokens that have nothing to do with the task. Handing the agent the three files that matter beats handing it thirty.

It also explains the long-session drift from the first section. As an agent runs, tool output and earlier steps accumulate in its working memory. The instructions you gave at the start are still in there, outnumbered, and the agent begins acting on whatever is most recent. The repair is to keep the working memory current: summarize and clear as you go.

A binding bug, two working memories

Say a binding in an Uno Platform view stops updating, and you bring an AI assistant to it. Same model, same question. The only thing that differs is what fills its working memory (see Fig. 3).

FIG. 3Same prompt, two working memories
A · flood it

You paste the whole 2,000-line view, three viewmodels, and the full build log, then ask why the binding will not update. The line that matters, a setter that never raises a change notification, is a thin signal among thousands of unrelated tokens. The model searches for it and answers in generalities.

B · curate it

You paste the bound property, its setter, the one XAML element, and the eight log lines around the error. The working memory is almost all signal, the relevant tokens hold the model's attention, and the answer names the cause: the setter never raises the change notification, so the binding never hears the update.

Fig. 3 — Each row is a chunk of pasted context; the bright rows carry the answer. On the left the signal is one row among seven; on the right it fills most of the working memory.

Here is the same bug as the text you would actually send.

A · flood it≈ 6k tokens
why isn't my binding updating? here's the page, the
viewmodels and the build output

[MainPage.xaml — 2,000 lines]
[MainViewModel.cs, SettingsViewModel.cs, DataService.cs]
[full build log]
B · curate it≈ 200 tokens
A TextBlock bound to Temperature shows the initial value
but never updates after it changes.

Stack: Uno Platform, .NET 10, MVVM with CommunityToolkit.Mvvm.

XAML:
  <TextBlock Text="{Binding Temperature}" />

ViewModel:
  private double _temperature;
  public double Temperature
  {
      get => _temperature;
      set => _temperature = value;   // updated from SetTemperature()
  }

Symptom: SetTemperature() changes the field, the UI never repaints.

What's the most likely cause, and the smallest fix?

Both describe one bug. The curated version spends its tokens on the binding, the property, and the suspect setter, so the model names the cause, the setter never raises a change notification, on the first pass instead of digging for it.

Curating by hand works. You can also let tooling fill the working memory for you, which is the point of the App MCP in Uno Platform Studio.

Studio shortcut

Skip the copy-paste. The App MCP connects to your running app and hands the agent the live visual tree and the control's real properties and binding values, so it reads the true runtime state instead of guessing from pasted source, then verifies its own fix against the running app. The curation happens for you.

Working memory fills as it runs

Watch a real session and you can see the working memory fill whether you manage it or not (see Fig. 4). A fresh call is mostly headroom: a short system prompt, your instructions, the one file in question, your request. That file is a large share of the few thousand tokens present, so it holds strong attention.

Then the run continues. Every file read, every build log, every test result is appended. Forty turns later the same working memory holds well over a hundred thousand tokens: a long history, stacks of tool output, your original instructions far below where the model is writing, and the file you care about reduced to a sliver. Nothing was deleted. It was outnumbered.

This is the drift from the first section, seen from the inside. The window did not shrink; the useful fraction of it did. The repair follows from the picture: summarize the history, drop tool output you are done with, and keep the current task and the relevant file near the end of the window, where the model is actively writing and attention is strongest. The teams running long agents have names for these moves, compaction, external notes, and handing a slice to a fresh sub-agent, and they share one aim: keep the working memory mostly signal.

FIG. 4One working memory, scrubbed across a session
your file
33%
of working memory is your file
Turnturn 1 / 40
Window size4,000 tok
Reliability on that filereliable
Fig. 4 — Drag from turn 1 to turn 40. Nothing is removed; history and tool output pile up and push your file from about a third of the working memory down to a sliver, and reliability on that file drops well before the token limit. The curve is drawn for illustration; the direction is the point. Clearing the pile keeps the file near where attention is strongest.

One thing to try this week

Here is the move that builds the instinct fastest. Open the longest-running AI chat or agent session you have going right now, and find the one constraint that has to hold for its work to be correct: the target framework, the API it must not touch, the rule you keep retyping. Move that line into the place that gets re-sent on every turn, the system prompt or a project rules file, so it stays in working memory instead of sinking out of it.

That is the whole exercise. One constraint, relocated from the fragile tail of a conversation into context that is always present. Do it once and you start to feel the difference between an agent that holds its instructions and one that loses them.

For the next bug, skip the pasting. Point the agent at the running app through the App MCP and let it read the real state. This is the prompt I reuse when a binding misbehaves.

Curated prompt · App MCPcopy + adapt
A TextBlock bound to Temperature never updates on screen.

Stack: Uno Platform, .NET 10, MVVM with CommunityToolkit.Mvvm.

Use the App MCP against the running app:
  1. Snapshot the visual tree and find the TextBlock bound to Temperature.
  2. Read its live binding value and the source property on the view model.
  3. Change the value and check whether the binding updates.

Tell me where the chain breaks, and the smallest fix.

The post Maintaining working memory in AI agents appeared first on Uno Platform.

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

How to Use Dart Dot Shorthands: A Handbook for Devs

1 Share

If you've written Flutter code for more than a month, you've likely written this line hundreds of times:

mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,

You know what type each of those parameters expects. The IDE knows. The Dart compiler knows. And yet every time you type it, you repeat the full type name before the dot: MainAxisAlignment.center. CrossAxisAlignment.start. MainAxisSize.min. Three words to say one thing, when the surrounding context has already made the type completely obvious.

This isn't an isolated friction. It shows up everywhere in Dart and Flutter. You write Colors.blue on a parameter typed as Color. You write BorderRadius.circular(8) on a parameter typed as BorderRadius. You write Duration.zero on a field typed as Duration. You write TextAlign.center on a parameter typed as TextAlign.

In every case, the type is already there in the parameter definition, and you're spelling it out again anyway because the language requires it.

Dart 3.10, released on November 12, 2025 alongside Flutter 3.38, introduces dot shorthands to solve this issue. With dot shorthands, when the compiler already knows the type from context, you can write just the dot and the member name. So, for example, .center instead of MainAxisAlignment.center. .circular(8) instead of BorderRadius.circular(8). .zero instead of Duration.zero. The type name you were spelling out is now optional, because the compiler can and will infer it.

This isn't a cosmetic feature. It's a substantive reduction in visual noise in the places where Flutter developers write the most code: widget trees, switch statements, enum assignments, and constructor calls.

The first time you enable it in a real codebase, your Column and Row parameters become noticeably cleaner. Your switch statements read more like prose. Your code says what it means without the prefix weight.

This handbook is your complete guide to dot shorthands. It covers not just the syntax but the mental model behind it: why the compiler can infer types in some positions and not others, how the inference rules work, where shorthands are genuinely powerful, and where they quietly make your code harder to read.

Many Flutter developers have seen the feature mentioned in a release note but haven't fully absorbed how deep it goes. This handbook gives you the complete picture.

By the end, you'll be able to use dot shorthands confidently across enums, static methods, static fields, constructors, switch statements, equality checks, nullable types, and async return expressions. You'll also know the precise situations where the feature can't work and why.

Table of Contents

Prerequisites

This guide assumes that you have some basic knowledge and skills already. You don't need to be an expert in any of these areas, but you should have a working foundation in each.

Dart fundamentals: You should understand classes, enums, static members, constructors, and named constructors. If you know the difference between ClassName.member and instance.member, and you understand what static means on a field or method, you're ready.

Flutter widget basics: You should be comfortable writing Column, Row, Container, and similar widgets. The guide uses Flutter widget parameters as the primary motivating example because that's where dot shorthands have the most visible impact.

Dart's type system: You should understand that every variable, parameter, and field in Dart has a type, and that type is either declared explicitly or inferred by the compiler. Understanding that the compiler knows types before your code runs is the foundation for understanding how context inference works.

Dart SDK 3.10 and Flutter 3.38 or higher: Dot shorthands are a language-version-gated feature. Your project must opt in to Dart 3.10. Update the SDK constraint in your pubspec.yaml:

environment:
  sdk: ^3.10.0

This constraint tells the Dart SDK that your package is written for Dart 3.10 or higher and unlocks the dot shorthand syntax for every Dart file in the project.

Without this change, using .center or .zero will produce a compile error telling you that dot shorthand requires language version 3.10 or later. If you're using Flutter, running flutter upgrade and updating the SDK constraint is all that's required.

DartPad for experimentation: You can test the examples in this guide interactively at https://dartpad.dev. DartPad supports Dart 3.10 and is the fastest way to test whether a particular shorthand works in a given context.

What Are Dot Shorthands?

Starting with a Direct Analogy

Imagine you're filling out a form that has a field labeled "Country." The field already says "Country:" on the left. You write "Nigeria." You don't write "Country: Nigeria" inside the box, because the label has already told you what category the value belongs to.

That's exactly what dot shorthands do. When Dart already knows from the surrounding context that a value must be of type MainAxisAlignment, you can write just .center instead of MainAxisAlignment.center. The type label is already there. The shorthand lets you write just the value.

The Technical Definition

A dot shorthand is an expression that begins with a leading dot (.) and resolves to a static member access on the context type. When the compiler knows from the surrounding context that an expression must be of type T, writing .member is treated as T.member. Writing .new(args) is treated as T.new(args) (the unnamed constructor). Writing .namedConstructor(args) is treated as T.namedConstructor(args).

The key phrase is "apparent context type." The context type is the type the compiler expects at the position where you're writing the expression. It comes from:

  • The declared type of a variable being assigned to

  • The declared type of a function parameter being passed a value

  • The declared return type of a function when a value is being returned

  • The static type of the left-hand side of a == or != comparison (special rule)

  • The declared type of a field in an initializer

If the compiler can determine the type from one of these sources before evaluating the expression, a dot shorthand is valid at that position. If no context type is available, the dot shorthand is a compile-time error.

The Problem: Life Before Dot Shorthands

The Repetition Pattern

Open any Flutter project and look at the widget tree of a non-trivial screen. You'll see something like this:

Column(
  mainAxisAlignment: MainAxisAlignment.center,
  crossAxisAlignment: CrossAxisAlignment.start,
  mainAxisSize: MainAxisSize.min,
  children: [
    Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      crossAxisAlignment: CrossAxisAlignment.center,
      children: [
        Text(
          'Hello',
          textAlign: TextAlign.left,
          overflow: TextOverflow.ellipsis,
        ),
        Icon(Icons.chevron_right),
      ],
    ),
    SizedBox(height: 16),
    Container(
      alignment: Alignment.centerLeft,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(8),
        color: Colors.white,
      ),
      child: Text('World'),
    ),
  ],
)

Count the enum type name repetitions in that code. MainAxisAlignment appears twice. CrossAxisAlignment appears twice. The words MainAxisAlignment, CrossAxisAlignment, TextAlign, TextOverflow, Alignment, BorderRadius, Colors are all written out in full.

And for each one, the type is already declared on the parameter: mainAxisAlignment takes a MainAxisAlignment, crossAxisAlignment takes a CrossAxisAlignment, and so on. The parameter name itself carries the type information. Yet the full type name was required before the dot.

This wasn't just visual noise. It was cognitive noise. When reading a widget tree, the type names between the parameter name and the actual value slow the eye. Your brain reads "mainAxisAlignment colon MainAxisAlignment dot center" when all the relevant information is in "mainAxisAlignment colon center."

The Switch Statement Problem

Enum-driven switch statements had the same issue:

switch (status) {
  case NetworkStatus.connecting:
    return const CircularProgressIndicator();
  case NetworkStatus.connected:
    return const Icon(Icons.wifi);
  case NetworkStatus.disconnected:
    return const Icon(Icons.wifi_off);
  case NetworkStatus.error:
    return const Icon(Icons.error);
}

The variable status is already typed as NetworkStatus. Every case therefore operates on a NetworkStatus value. Writing NetworkStatus.connecting, NetworkStatus.connected, NetworkStatus.disconnected, and NetworkStatus.error in every case is pure repetition. The type name adds no information because it's already known from the switch target.

These patterns were unavoidable before Dart 3.10. They were just the cost of the language's verbosity in static contexts.

The One Rule That Governs Everything: Context

The Single Mental Model You Need

Before diving into specific use cases, internalize this single rule, because once you have it, every dot shorthand example in the language becomes obvious:

A dot shorthand works only where the compiler already knows the expected type.

That's the complete rule. Everything else is a consequence of it.

If the compiler knows the type, .member resolves to TypeName.member. If the compiler doesn't know the type, the dot shorthand is a compile-time error. There's no guessing, no runtime inference, and no ambiguity. The compiler resolves the shorthand at compile time using the same type information it already had.

Let's see what this means concretely:

// The compiler knows the type from the variable declaration.
// NetworkStatus currentStatus = ...
// So .connecting is NetworkStatus.connecting. This works.
NetworkStatus currentStatus = .connecting;

// The compiler has no type context here.
// There is no surrounding variable, parameter, or declaration
// to tell it what type .connecting belongs to.
// This is a compile-time error.
var x = .connecting; // ERROR: No context type available

// The compiler knows the type from the parameter declaration.
// The parameter `status` is declared as NetworkStatus.
// So passing .connected resolves to NetworkStatus.connected. This works.
void update(NetworkStatus status) { }
update(.connected); // Works: parameter type provides context

NetworkStatus currentStatus = .connecting works because the explicit type annotation NetworkStatus on the variable declaration gives the compiler all it needs.

var x = .connecting fails because var means "infer from the right-hand side," and the right-hand side starts with a dot shorthand, which itself requires context from the left-hand side. That's circular. There's no context, so there's no shorthand.

update(.connected) works because the function's parameter type NetworkStatus is the context.

This is the single insight the entire feature is built on. Every valid and invalid example in this handbook traces back to whether a context type is available at that position.

Enums: The Primary Use Case

Why Enums Benefit Most

Enums are the primary and most recommended use case for dot shorthands for two reasons.

First, they appear everywhere in Flutter: alignment, sizing, color schemes, text overflow, font weights, button styles, and dozens more. Second, the type context for an enum value is almost always obvious from the assignment target or the parameter being set, making the shorthand maximally unambiguous.

Assignments

enum Status { idle, loading, success, error }

// Before Dart 3.10
Status currentStatus = Status.idle;

// With dot shorthands (Dart 3.10+)
Status currentStatus = .idle;

The variable declaration Status currentStatus provides the context type. When the compiler reaches the right-hand side and sees .idle, it looks up the context type (Status), checks that Status has a member named idle, and resolves the expression to Status.idle. The resulting compiled code is identical to the before version. There's no runtime difference, only a syntactic one.

Flutter Widget Parameters

// Before Dart 3.10
Column(
  mainAxisAlignment: MainAxisAlignment.center,
  crossAxisAlignment: CrossAxisAlignment.start,
  mainAxisSize: MainAxisSize.min,
)

// With dot shorthands (Dart 3.10+)
Column(
  mainAxisAlignment: .center,
  crossAxisAlignment: .start,
  mainAxisSize: .min,
)

The Column widget's constructor declares its parameter types explicitly: mainAxisAlignment is MainAxisAlignment, crossAxisAlignment is CrossAxisAlignment, mainAxisSize is MainAxisSize. Each parameter declaration is the context type for the argument passed to it. When the compiler sees .center in the mainAxisAlignment position, the context type is MainAxisAlignment, so .center becomes MainAxisAlignment.center. Each shorthand resolves independently using its own parameter's declared type.

The three-line version and the new version compile to exactly the same bytecode. The shorthand is a compile-time transformation, not a runtime one.

Enhanced Enums

Dart's enhanced enums (introduced in Dart 2.17) can have fields, methods, and constructors. Dot shorthands work with all members that are statically accessible on the enum type:

enum Priority {
  low(1),
  medium(5),
  high(10);

  final int weight;
  const Priority(this.weight);

  static Priority fromWeight(int w) {
    if (w <= 3) return low;
    if (w <= 7) return medium;
    return high;
  }
}

// Dot shorthand on an enum value
Priority taskPriority = .high;

// Dot shorthand on a static factory method defined on the enum
Priority resolved = .fromWeight(8);

Priority taskPriority = .high uses the variable's declared type as context. .high resolves to Priority.high. Priority resolved = .fromWeight(8) calls the static fromWeight method on Priority without spelling out the type name. Both work because the variable type provides the context.

Inside Functions with Enum Return Types

Priority getDefaultPriority() {
  return .medium; // return type provides context: Priority
}

When the declared return type of a function is an enum type, the return statement's value has that type as its context. .medium resolves to Priority.medium because the function's return type is Priority. The same applies to any function, method, or getter whose return type is explicit.

Static Fields and Constants

Static Constants

Static constants, especially sentinel values like Duration.zero, EdgeInsets.zero, and Offset.zero, are common throughout Flutter and Dart. Dot shorthands make them noticeably cleaner:

// Before Dart 3.10
Duration timeout = Duration.zero;
EdgeInsets padding = EdgeInsets.zero;
Offset position = Offset.zero;

// With dot shorthands (Dart 3.10+)
Duration timeout = .zero;
EdgeInsets padding = .zero;
Offset position = .zero;

In each case, the variable's declared type (Duration, EdgeInsets, Offset) is the context. .zero resolves to the appropriate type's static zero constant in each case.

This is particularly valuable because these zero-value sentinels appear frequently in animation code, layout code, and geometric calculations, so the repetition saving compounds across a real codebase.

Static Fields on Built-In Dart Types

Dart's built-in types also expose static fields, and they work equally well:

// Duration.zero is a static field on Duration
Duration animationDuration = .zero;

// double.infinity is a static field on double
double maxWidth = .infinity;

// String.isEmpty and similar static constants on types
int maxRetries = .maxFinite.toInt(); // double context, then chained

Duration animationDuration = .zero resolves .zero as Duration.zero from the variable's type. double maxWidth = .infinity resolves .infinity as double.infinity. The second example also shows the beginnings of chaining, which is covered in its own section.

Static Methods

Calling Static Methods with Shorthands

Static methods are called the same way as static fields: with a leading dot, followed by the method name and arguments. The context type tells the compiler which class to look up the method on:

// Before Dart 3.10
int port = int.parse('8080');
double ratio = double.parse('1.618');
DateTime now = DateTime.now();

// With dot shorthands (Dart 3.10+)
int port = .parse('8080');
double ratio = .parse('1.618');
DateTime now = .now();

int port = .parse('8080') resolves to int.parse('8080') because the variable's declared type is int, and int has a static method named parse that accepts a String and returns an int. double ratio = .parse('1.618') resolves to double.parse('1.618') using the same mechanism. DateTime now = .now() resolves to DateTime.now() from the DateTime context.

The method's return type must be compatible with the context type. If int.parse returned a String, the compiler would report a type error. The shorthand resolution happens first (find the static member on the context type), then the result is type-checked against the context as normal.

In Function Arguments

void configure({required Duration timeout, required int retryCount}) {}

configure(
  timeout: .zero,          // Duration context -> Duration.zero
  retryCount: .parse('3'), // int context -> int.parse('3')
);

Each named argument's declared parameter type is the context for the argument value. timeout is declared as Duration, so .zero resolves to Duration.zero. retryCount is declared as int, so .parse('3') resolves to int.parse('3'). Each argument's shorthand resolves independently using its own parameter's type.

Constructors and Named Constructors

Named Constructors

Named constructors are one of Dart's most idiomatic patterns. They exist on EdgeInsets, BorderRadius, Color, TextStyle, Duration, and dozens of other types you use in every Flutter app. Dot shorthands work with all of them:

// Before Dart 3.10
EdgeInsets padding = EdgeInsets.all(16);
BorderRadius radius = BorderRadius.circular(8);
Color accent = Color.fromARGB(255, 66, 133, 244);
TextStyle headline = TextStyle();

// With dot shorthands (Dart 3.10+)
EdgeInsets padding = .all(16);
BorderRadius radius = .circular(8);
Color accent = .fromARGB(255, 66, 133, 244);
TextStyle headline = TextStyle(); // still fine with full form too

EdgeInsets padding = .all(16) works because EdgeInsets is the context type and .all(16) resolves to EdgeInsets.all(16), which is a named constructor. BorderRadius radius = .circular(8) follows the same pattern.

The full form continues to work, as dot shorthands are always optional. You choose the shorthand when it improves readability and keep the full form when the type name adds clarity.

In Widget Constructors

Named constructors shine in widget parameters, which is where most Flutter developers will use them most:

// Before Dart 3.10
Padding(
  padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
  child: Container(
    decoration: BoxDecoration(
      borderRadius: BorderRadius.circular(12),
      border: Border.all(color: Colors.grey, width: 1),
    ),
    child: Text('Hello'),
  ),
)

// With dot shorthands (Dart 3.10+)
Padding(
  padding: .symmetric(horizontal: 16, vertical: 8),
  child: Container(
    decoration: BoxDecoration(
      borderRadius: .circular(12),
      border: .all(color: Colors.grey, width: 1),
    ),
    child: Text('Hello'),
  ),
)

padding: .symmetric(horizontal: 16, vertical: 8) resolves .symmetric(...) as EdgeInsets.symmetric(...) because the padding parameter of Padding is declared as EdgeInsets. borderRadius: .circular(12) resolves as BorderRadius.circular(12) because the borderRadius field of BoxDecoration is typed as BorderRadius?. border: .all(color: Colors.grey, width: 1) resolves as Border.all(...) because the border field of BoxDecoration is typed as BoxBorder?, which Border implements.

The shorthand resolution checks the static type of the member, not just the exact declared type.

The .new Shorthand

Invoking the Default Constructor

Dart's ClassName.new is the named reference to the unnamed default constructor. Dot shorthands support .new(args) as a shorthand for calling the default constructor:

class AppConfig {
  final String baseUrl;
  final int timeout;

  AppConfig(this.baseUrl, this.timeout);
}

// Before Dart 3.10
AppConfig config = AppConfig('https://api.example.com', 30);

// With dot shorthand using .new
AppConfig config = .new('https://api.example.com', 30);

.new('https://api.example.com', 30) resolves to AppConfig.new('https://api.example.com', 30), which is the same as calling AppConfig('https://api.example.com', 30). The context type AppConfig from the variable declaration drives the resolution.

When .new Is Most Useful

The .new shorthand is most valuable in generic contexts and in function tear-offs, where the class name would otherwise need to be spelled out as a constructor reference.

In direct variable assignments, it doesn't save much compared to just typing the class name, since the class name is already in the type annotation. The real benefit comes in patterns like this:

// A list of items where each item is constructed in place
List<AppConfig> configs = [
  .new('https://api.example.com', 30),
  .new('https://staging.example.com', 60),
  .new('https://dev.example.com', 120),
];

List<AppConfig> configs provides the context type through the list's element type AppConfig. Each .new(...) inside the list literal resolves to AppConfig(...). In a list with many similar constructor calls, the shorthand removes the repetitive type prefix that would otherwise appear on every item.

Chaining After a Shorthand

Chaining Instance Methods

The dot shorthand doesn't need to be the complete expression. After the static access, you can chain instance method calls, property accesses, and other selectors. The chain can be as long as needed, as long as the final result's type is compatible with the context:

// Chain an instance method after a static method call
int value = .parse('  42  ').abs();

// Chain a property access after a constructor call
double distance = .fromARGB(255, 255, 0, 0).opacity;

// Chain a method after an enum value's instance method
String statusLabel = .loading.name.toUpperCase();

int value = .parse(' 42 ').abs() resolves .parse(' 42 ') as int.parse(' 42 '), which returns an int. Then .abs() is called on that int instance. The result is an int, which matches the variable's declared type.

The shorthand only applies to the leading static access. The rest of the chain is ordinary instance member access. String statusLabel = .loading.name.toUpperCase() demonstrates chaining on an enum value. The context type for the shorthand resolution comes from the enum (here assumed to be a Status or similar), .name is a built-in property on every enum value that returns the value's name as a String, and .toUpperCase() is an instance method on String.

Why This Matters

Chaining means dot shorthands don't force you to stop at the static member. If you need to transform or access a property of the result, you can do so in the same expression. The rule is: the leading .member is the shorthand, everything after it is a normal instance access chain.

// Combining a static constructor call with a property read
Color primary = .fromARGB(255, 66, 133, 244);
double alpha = .fromARGB(255, 66, 133, 244).opacity; // context is double

Color primary = .fromARGB(255, 66, 133, 244) uses the Color context to resolve the shorthand. double alpha = .fromARGB(255, 66, 133, 244).opacity has double as the context type, not Color. This means .fromARGB would need to resolve to a static method on double that exists, which it does not.

This particular example would fail. The context type governs the leading access, so the context for the leading shorthand is double, not Color. This is a subtle point: when chaining, make sure the context type at the expression position matches the type you're targeting.

Equality Operators: The Special Rule

How == and != Work with Dot Shorthands

The == and != operators have a special rule for dot shorthands that's different from the general context rule. When a dot shorthand appears on the right-hand side of a == or != expression, the context type is derived from the static type of the left-hand side, not from any surrounding variable or parameter:

enum Color { red, green, blue }

Color myColor = Color.red;

// The LHS is myColor, which has static type Color.
// So .green is resolved as Color.green.
if (myColor == .green) {
  print('The color is green.');
}

// Works the same with !=
if (myColor != .blue) {
  print('The color is not blue.');
}

myColor == .green works because myColor is declared as Color, making Color the context for the right-hand side .green. The compiler resolves .green as Color.green before performing the equality comparison.

This special rule exists because == expressions don't have a surrounding context type the way variable assignments do. The left-hand side is used instead.

Equality in Conditional Expressions

Color selectedColor = Color.red;
bool condition = true;

Color inferredColor = condition ? .red : .blue;

Color inferredColor = condition ? .red : .blue resolves both .red and .blue as Color values. The context type for a ternary expression comes from the assignment target's type, which is Color. Both branches of the ternary receive the same context type, so both shorthands resolve correctly.

What Does Not Work

// ERROR: No context for the shorthand on the right side
// because the left side is `var`, which has no known type yet.
var isMatch = someValue == .green; // FAILS if someValue's type is not clear

// This works if someValue is explicitly typed
Color someValue = Color.blue;
bool isMatch = someValue == .green; // Works: someValue is Color

var isMatch = someValue == .green fails when someValue's type isn't inferable before evaluation. The rule depends on the static type of the left-hand side being known at compile time. If the compiler can't determine the left-hand side's type, the shorthand has no context to resolve from.

Switch Statements and Pattern Matching

Switch on Enums

Switch statements on enum values are where dot shorthands make the most dramatic readability improvement in real code. The switch target's type is used as the context for all case patterns:

enum AppState { loading, loaded, error, empty }

AppState state = .loading;

// Before Dart 3.10
switch (state) {
  case AppState.loading:
    return const CircularProgressIndicator();
  case AppState.loaded:
    return const ContentWidget();
  case AppState.error:
    return const ErrorWidget();
  case AppState.empty:
    return const EmptyStateWidget();
}

// With dot shorthands (Dart 3.10+)
switch (state) {
  case .loading:
    return const CircularProgressIndicator();
  case .loaded:
    return const ContentWidget();
  case .error:
    return const ErrorWidget();
  case .empty:
    return const EmptyStateWidget();
}

state is declared as AppState, making AppState the context type for every case in the switch. Each .loading, .loaded, .error, and .empty resolves to the corresponding AppState value. The switch is exhaustive – checking works the same way. The compiler still verifies that all enum cases are covered.

Switch Expressions

Dart's switch expressions (the expression form that returns a value) work identically:

Widget content = switch (state) {
  .loading => const CircularProgressIndicator(),
  .loaded  => const ContentWidget(),
  .error   => const ErrorWidget(),
  .empty   => const EmptyStateWidget(),
};

switch (state) where state is AppState provides AppState as the context for each pattern on the left side of the =>. Each .loading, .loaded, .error, and .empty resolves to the corresponding AppState value. The right side of each => arrow isn't affected by the switch context; each => branch is a normal expression.

Pattern Matching in Switch

void handleResult(Result result) {
  switch (result) {
    case .success when result.value > 0:
      print('Positive success: ${result.value}');
    case .success:
      print('Non-positive success');
    case .failure:
      print('Failed: ${result.error}');
  }
}

Guard clauses (when) work naturally alongside dot shorthands. .success when result.value > 0 is a case pattern for the enum value Result.success with an additional guard condition. The shorthand resolves to the enum value for matching purposes, and the guard is evaluated separately.

Nullable Types

Accessing Members of T Through T?

When a variable or parameter has a nullable type T?, you can still use dot shorthands to access static members of the underlying type T. The Dart specification explicitly allows this:

// A parameter typed as nullable Status
void updateStatus(Status? newStatus) {
  // You can pass a non-null Status value using a shorthand
}

updateStatus(.loading); // passes Status.loading, which is a valid Status?

updateStatus(.loading) works because the parameter type Status? provides a context of Status?, and the dot shorthand rules allow accessing members of Status in a Status? context. The value .loading resolves to Status.loading, which is a non-null Status, and non-null values are always valid in a nullable position.

Nullable Variable Assignments

Status? maybeStatus = .error; // Assigns Status.error to a Status? variable
Status? nothing = null;       // Still works; null is valid for Status?

Status? maybeStatus = .error resolves .error as Status.error (from the Status? context), which is then assigned to the nullable variable. The nullability of the type doesn't prevent the shorthand from working – it just means the variable can also hold null. The shorthand always produces a non-null value of the underlying type.

What Nullable Context Does Not Grant

The nullable context allows accessing members of T, but not members of Null. Null has no useful static members for this purpose, and the feature doesn't expose them:

// This resolves to Duration.zero (from the Duration? context's underlying Duration type)
Duration? elapsed = .zero;

// You cannot access static members of Null through a nullable context
// There are no meaningful Null static members to access

Duration? elapsed = .zero resolves .zero as Duration.zero from the Duration? context. The nullable wrapper is transparent for the purposes of static member lookup.

FutureOr and Async Returns

Returning Values from Async Functions

Inside an async function, the effective return type of every return statement is FutureOr<T> where T is the declared return type. The dot shorthand specification explicitly handles this case by allowing T's static members to be accessed in a FutureOr<T> context:

Future<Status> fetchStatus() async {
  // The function's declared return type is Future<Status>.
  // Inside an async function, return accepts a FutureOr<Status>.
  // Dot shorthand resolves .loaded as Status.loaded.
  return .loaded;
}

return .loaded inside a Future<Status> async function works because the async function's return context is FutureOr<Status>, and the dot shorthand rules allow accessing Status members through a FutureOr<Status> context.

The Dart team specifically decided to support this case because returning bare values from async functions is extremely common, and requiring Status.loaded when the function's return type already says Status was seen as unnecessary verbosity.

FutureOr in Non-Async Contexts

FutureOr<Duration> getDelay() {
  // Can return either a Duration or a Future<Duration>
  return .zero; // Resolves to Duration.zero
}

return .zero in a function returning FutureOr<Duration> resolves .zero as Duration.zero because the FutureOr<Duration> context grants access to Duration's members. The returned value is a synchronous Duration, which is a valid FutureOr<Duration>.

Dot Shorthands in Flutter Widget Trees

The Transformation in Practice

Flutter widget trees are the most impactful place to see dot shorthands in action, because they contain the most enum values and named constructors in any Flutter codebase.

Here's a realistic profile card widget, before and after:

// Before Dart 3.10: A profile card widget
class ProfileCard extends StatelessWidget {
  final String name;
  final String role;
  final bool isOnline;

  const ProfileCard({
    super.key,
    required this.name,
    required this.role,
    required this.isOnline,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 2,
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            CircleAvatar(
              backgroundColor: isOnline ? Colors.green : Colors.grey,
              radius: 24,
              child: Text(
                name[0].toUpperCase(),
                style: TextStyle(
                  color: Colors.white,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
            SizedBox(width: 12),
            Expanded(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    name,
                    style: TextStyle(
                      fontWeight: FontWeight.w600,
                      overflow: TextOverflow.ellipsis,
                    ),
                  ),
                  Text(
                    role,
                    style: TextStyle(
                      color: Colors.grey,
                      fontSize: 12,
                    ),
                  ),
                ],
              ),
            ),
            Icon(
              isOnline ? Icons.circle : Icons.circle_outlined,
              color: isOnline ? Colors.green : Colors.grey,
              size: 12,
            ),
          ],
        ),
      ),
    );
  }
}

This is clean, idiomatic Flutter code. But look at how much is repeated: the full type names for every enum value and every constructor call.

Now the same thing with dot shorthands:

// With dot shorthands (Dart 3.10+)
class ProfileCard extends StatelessWidget {
  final String name;
  final String role;
  final bool isOnline;

  const ProfileCard({
    super.key,
    required this.name,
    required this.role,
    required this.isOnline,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 2,
      child: Padding(
        padding: .all(16),
        child: Row(
          mainAxisAlignment: .start,
          crossAxisAlignment: .center,
          children: [
            CircleAvatar(
              backgroundColor: isOnline ? Colors.green : Colors.grey,
              radius: 24,
              child: Text(
                name[0].toUpperCase(),
                style: TextStyle(
                  color: Colors.white,
                  fontWeight: .bold,
                ),
              ),
            ),
            const SizedBox(width: 12),
            Expanded(
              child: Column(
                mainAxisSize: .min,
                crossAxisAlignment: .start,
                children: [
                  Text(
                    name,
                    style: TextStyle(
                      fontWeight: .w600,
                      overflow: .ellipsis,
                    ),
                  ),
                  Text(
                    role,
                    style: TextStyle(
                      color: Colors.grey,
                      fontSize: 12,
                    ),
                  ),
                ],
              ),
            ),
            Icon(
              isOnline ? Icons.circle : Icons.circle_outlined,
              color: isOnline ? Colors.green : Colors.grey,
              size: 12,
            ),
          ],
        ),
      ),
    );
  }
}

padding: .all(16) resolves to EdgeInsets.all(16) because Padding.padding is typed EdgeInsets. mainAxisAlignment: .start resolves to MainAxisAlignment.start because Row.mainAxisAlignment is typed MainAxisAlignment. crossAxisAlignment: .center resolves to CrossAxisAlignment.center. fontWeight: .bold resolves to FontWeight.bold because TextStyle.fontWeight is FontWeight?. mainAxisSize: .min resolves to MainAxisSize.min. overflow: .ellipsis resolves to TextOverflow.ellipsis.

Each shorthand is driven by the declaring parameter's type.

The before and after produce identical compiled output. The difference is purely in how the source reads: with shorthands, the parameter name and the value are adjacent, and the eye moves cleanly from one to the other without wading through the repeated type names.

Advanced Concepts

Where the Inference Does Not Kick In

Understanding the failure cases is as important as understanding the success cases. The following situations don't provide a context type and so don't support dot shorthands:

// var infers from the RHS, but RHS needs LHS context: circular, fails
var status = .loading; // ERROR

// The list literal does not know its element type from a leading dot
var items = [.loading, .error]; // ERROR: var provides no context

// Explicitly typed list works fine
List<Status> items = [.loading, .error]; // Works

// Dynamic removes type information entirely
dynamic value = .loading; // ERROR: dynamic is not a usable context type

// Conditional assignment where context is ambiguous
Object status = condition ? .loading : 'string'; // ERROR: Object too broad

var status = .loading fails because var means the type is inferred from the right-hand side, but the right-hand side (the shorthand) needs the left-hand type for context. It's circular.

var items = [.loading, .error] fails for the same reason: the list's element type would come from its contents, but the contents need the element type.

List<Status> items = [.loading, .error] works because the explicit type annotation gives the compiler the Status context before it evaluates the list elements.

But dynamic value = .loading fails because dynamic bypasses the type system and doesn't provide a usable static context type for member lookup.

Nested Shorthands

A "nested shorthand" is when you attempt to use a dot shorthand inside an expression that is itself using a dot shorthand. The outer shorthand's resolution doesn't propagate its type as context into nested positions:

// The outer shorthand resolves from the BoxDecoration context
BoxDecoration decoration = BoxDecoration(
  borderRadius: .circular(8), // Outer shorthand: BorderRadius.circular(8)
  border: .all(                // Outer shorthand: Border.all(...)
    color: Colors.grey,
    width: 1,
  ),
);

This works. Each shorthand resolves independently: .circular(8) from the BorderRadius? context of boxDecoration.borderRadius, and .all(...) from the BoxBorder? context of boxDecoration.border. They aren't nested in the sense of depending on each other.

A truly nested shorthand would be using a shorthand inside the arguments of another shorthand's call:

// Attempting to use a shorthand inside another shorthand's arguments
EdgeInsets padding = .fromLTRB(
  .zero.left,  // ERROR: .zero has no context here
  8, 8, 8,
);

.zero.left fails because .zero inside the argument to .fromLTRB doesn't have an established context type. The DCM linter provides an avoid-nested-shorthands rule that flags these cases. The fix is always to be explicit in the inner position where context is unclear:

EdgeInsets padding = .fromLTRB(
  EdgeInsets.zero.left, // Explicit: fine
  8, 8, 8,
);

Dot Shorthands with Extension Types

Extension types (introduced in Dart 3.3) also support dot shorthands. If an extension type has static members, they can be accessed with a shorthand when the extension type is the context:

extension type Milliseconds(int value) {
  static Milliseconds get zero => Milliseconds(0);
  static Milliseconds fromSeconds(int seconds) => Milliseconds(seconds * 1000);
}

Milliseconds delay = .zero;             // Milliseconds.zero
Milliseconds timeout = .fromSeconds(5); // Milliseconds.fromSeconds(5)

Milliseconds delay = .zero resolves .zero as Milliseconds.zero from the variable's declared type. Milliseconds timeout = .fromSeconds(5) resolves the static factory method on Milliseconds.

Extension types are still relatively new, but their support for dot shorthands means you can design them with the same shorthand-friendly static member API that built-in types have.

Linter Support

The DCM (Dart Code Metrics) tool provides four lint rules specifically for dot shorthands, which help enforce consistent adoption:

# analysis_options.yaml (using DCM)
dcm:
  rules:
    - prefer-shorthands-with-enums
    - prefer-shorthands-with-static-fields
    - prefer-returning-shorthands
    - prefer-shorthands-with-constructors:
        entries:
          - EdgeInsets
          - BorderRadius
          - Radius
          - Border
          - Duration
    - avoid-nested-shorthands

prefer-shorthands-with-enums flags any enum value access where the type name could be dropped because context makes it clear. prefer-shorthands-with-static-fields does the same for static field accesses. prefer-returning-shorthands flags return statements where the type name could be omitted. prefer-shorthands-with-constructors with an entries list flags specific classes where named constructor calls could use shorthands. avoid-nested-shorthands flags the problematic nested cases described above.

Enabling these rules gradually (starting with prefer-shorthands-with-enums, the most impactful) is the recommended migration strategy for an existing codebase.

Best Practices

Start With Enums and Switch Statements

The highest-value, lowest-risk places to adopt dot shorthands are enum assignments and switch case patterns. These are the cases where the type context is most obvious to any reader, the compiler's inference is most reliable, and the readability gain is highest. Migrate these first in any existing codebase.

Always Keep the Full Form When Type Is Genuinely Unclear

The goal of dot shorthands is to reduce noise, not to introduce ambiguity. When a shorthand makes a reader pause and wonder what type the dot refers to, use the full form.

A concrete signal: if you would need to hover over the expression in your IDE to know what type it resolves to, the full form is more appropriate.

// Clear: the parameter name `alignment` tells you the type
alignment: .centerLeft,

// Less clear in isolation: what type does .fromARGB belong to?
// The full form communicates more clearly here
color: Color.fromARGB(255, 66, 133, 244), // more readable than .fromARGB

alignment: .centerLeft is clear because the parameter name alignment strongly implies Alignment. Color.fromARGB(...) is more readable than .fromARGB(...) because fromARGB as a method name doesn't clearly signal which type it comes from, and Color in front of it removes any ambiguity instantly.

Be Consistent Across a File or Team

Inconsistency is worse than either consistent adoption or consistent avoidance. If half your widget tree uses shorthands and half uses full forms, the code looks inconsistent and the mix of styles creates cognitive load.

Pick a convention for your team: either adopt shorthands for enums and avoid them for constructors, or adopt them across the board for types where the parameter name makes the type obvious.

Update Your pubspec.yaml Before Using Any Shorthands

The feature is gated on the language version. Using a shorthand in a file under a project that hasn't updated its SDK constraint will produce a compile error.

Update the constraint before adopting the syntax:

environment:
  sdk: ^3.10.0

sdk: ^3.10.0 means "Dart 3.10.0 or any higher patch or minor version, but not 4.0 or higher." This is the standard constraint for Dart 3 projects. If your team has a monorepo with multiple packages, each package's pubspec.yaml needs its own updated constraint for that package to use dot shorthands.

When to Use Dot Shorthands and When Not To

Where Dot Shorthands Are Clearly the Right Choice

Enum values in Flutter widget parameters are the canonical use case. mainAxisAlignment: .center, crossAxisAlignment: .start, mainAxisSize: .min, textAlign: .left are all unambiguous, save significant horizontal space in already-deep widget trees, and make the code read more naturally.

Switch statements on enums are the second canonical case. Every case in a switch on a typed enum variable can use a shorthand, and the result is switch statements that read as a list of values rather than a list of prefixed type-and-value pairs.

Well-known sentinels like .zero, .empty, .none on types where that member is universally understood are also excellent candidates. Duration timeout = .zero is clearer than Duration timeout = Duration.zero because the context gives you the type and zero is a universally understood sentinel.

Where to Prefer the Full Form

Any constructor or static method call where the method name doesn't clearly signal the type is a case for the full form. .fromARGB(255, 66, 133, 244) is not as self-explanatory as Color.fromARGB(255, 66, 133, 244). The explicit type name acts as documentation.

Any context where a new developer might not know what type they're looking at deserves the full form. If a parameter is named config and the type is a custom class ServerConfig, writing .defaults() is less clear than ServerConfig.defaults() because config is a vague name and the shorthand hides the class being instantiated.

Any place where two different types have a static member with the same name, and both could plausibly be the context type, should use the full form to remove any possible confusion. Even if the compiler is unambiguous, human readers may not be.

Common Mistakes

Using var Instead of an Explicit Type

The most common beginner mistake with dot shorthands is trying to use them with var:

// ERROR: var cannot provide a context type
var status = .loading;

// CORRECT: explicit type annotation provides the context
Status status = .loading;

var status = .loading looks like it should work because var eventually gets inferred as Status if you assign a Status value. But type inference for var works by looking at the right-hand side first, and the right-hand side (the shorthand) needs the left-hand type to resolve.

var doesn't provide a type before evaluation – it defers to the evaluation result. The fix is always to add the explicit type annotation, which is a one-word change and the result is cleaner code.

Forgetting to Update the SDK Constraint

# BEFORE: Will not support dot shorthands
environment:
  sdk: ^3.9.0

# AFTER: Enables dot shorthands for all files in this package
environment:
  sdk: ^3.10.0

Attempting to use .loading or any other shorthand in a project with the old constraint produces a compile error that points to the language version. The fix is to update the sdk constraint in pubspec.yaml, then run flutter pub get or dart pub get. No code changes are needed beyond the pubspec.yaml update to enable the feature.

Assuming Shorthands Work Inside Generic Type Arguments

// ERROR: Type arguments do not provide a shorthand context
List<.center> items; // Meaningless and invalid
Map<String, .loading> cache; // Invalid

Type argument positions (the <T> in generic types) aren't expression positions. They can't contain dot shorthands.

A dot shorthand must be a value expression, not a type expression. This distinction is clear once stated but can trip up developers who are getting comfortable with how broadly shorthands apply.

Over-Using Shorthands Where Type Context Is Thin

// Problematic: the shorthand obscures which type fromJSON belongs to
SomeConfig config = .fromJSON(data); // What class is this?

// Better: be explicit when the type name adds real information
SomeConfig config = SomeConfig.fromJSON(data);

.fromJSON(data) is a shorthand that technically works if SomeConfig is the context type, but fromJSON as a method name is generic enough that a reader encountering it for the first time wouldn't know which class it comes from without looking at the variable's type. Including SomeConfig explicitly in the constructor call makes it immediately readable. Not every valid shorthand is an improvement.

Mini End-to-End Example

Let's build a complete, realistic feature that demonstrates dot shorthands across every major context: enums, static methods, named constructors, switch statements, and Flutter widget parameters.

The feature is a network status indicator widget for an app that shows different UI states based on connection status.

The Enum and State Model

// lib/models/connection_state.dart

enum ConnectionState {
  connecting,
  connected,
  disconnected,
  limited,
  error;

  bool get isActive => this == .connected || this == .limited;
  bool get isTerminal => this == .disconnected || this == .error;

  static ConnectionState fromCode(int code) {
    return switch (code) {
      0 => .connecting,
      1 => .connected,
      2 => .limited,
      3 => .disconnected,
      _ => .error,
    };
  }

  String get label => switch (this) {
    .connecting   => 'Connecting...',
    .connected    => 'Connected',
    .disconnected => 'Disconnected',
    .limited      => 'Limited Connection',
    .error        => 'Connection Error',
  };
}

bool get isActive => this == .connected || this == .limited uses the == special rule. this is a ConnectionState instance, so this == .connected resolves .connected as ConnectionState.connected from the static type of the left-hand side this.

static ConnectionState fromCode(int code) is a static factory method on the enum. Inside the switch expression, the return type ConnectionState provides context for each => result. .connecting resolves to ConnectionState.connecting, .connected to ConnectionState.connected, and so on.

The _ wildcard case returns .error, which also resolves to ConnectionState.error. String get label uses a switch expression on this, which is typed ConnectionState, providing context for the case patterns. Each .connecting, .connected, .disconnected, .limited, and .error resolves to the corresponding enum value.

The Config Model

// lib/models/network_config.dart

class NetworkConfig {
  final Duration timeout;
  final int maxRetries;
  final bool showDetailedErrors;

  const NetworkConfig({
    required this.timeout,
    required this.maxRetries,
    required this.showDetailedErrors,
  });

  factory NetworkConfig.standard() {
    return NetworkConfig(
      timeout: .zero,     // Duration context -> Duration.zero
      maxRetries: .parse('3'), // int context -> int.parse('3')
      showDetailedErrors: false,
    );
  }

  factory NetworkConfig.debug() {
    return NetworkConfig(
      timeout: .fromSeconds(60),  // Duration context -> Duration.fromSeconds(60)
      maxRetries: .parse('10'),   // int context -> int.parse('10')
      showDetailedErrors: true,
    );
  }
}

timeout: .zero uses the field's declared type Duration as context. .zero resolves to Duration.zero. maxRetries: .parse('3') uses the field's declared type int as context. .parse('3') resolves to int.parse('3'), which returns an int. timeout: .fromSeconds(60) resolves to Duration.fromSeconds(60), a named constructor on Duration.

These are simple but realistic patterns: factory constructors that use static methods and sentinels from other types, now without spelling out those types.

The Status Widget

// lib/widgets/connection_status_widget.dart

import 'package:flutter/material.dart';
import '../models/connection_state.dart';

class ConnectionStatusWidget extends StatelessWidget {
  final ConnectionState state;
  final VoidCallback? onRetry;

  const ConnectionStatusWidget({
    super.key,
    required this.state,
    this.onRetry,
  });

  @override
  Widget build(BuildContext context) {
    return AnimatedSwitcher(
      duration: .fromMilliseconds(300), // Duration context
      child: _buildContent(context),
    );
  }

  Widget _buildContent(BuildContext context) {
    return Padding(
      padding: .symmetric(horizontal: 16, vertical: 12), // EdgeInsets context
      child: Row(
        mainAxisAlignment: .spaceBetween, // MainAxisAlignment context
        crossAxisAlignment: .center,      // CrossAxisAlignment context
        children: [
          Row(
            mainAxisSize: .min, // MainAxisSize context
            children: [
              _buildIcon(),
              const SizedBox(width: 8),
              Text(
                state.label,
                style: TextStyle(
                  fontWeight: .w500,    // FontWeight context
                  color: _textColor(),
                ),
              ),
            ],
          ),
          if (state == .error && onRetry != null)
            TextButton(
              onPressed: onRetry,
              child: const Text('Retry'),
            ),
        ],
      ),
    );
  }

  Widget _buildIcon() {
    final (IconData icon, Color color) = switch (state) {
      .connecting   => (Icons.sync,          Colors.orange),
      .connected    => (Icons.wifi,           Colors.green),
      .disconnected => (Icons.wifi_off,       Colors.grey),
      .limited      => (Icons.signal_wifi_4_bar_lock, Colors.amber),
      .error        => (Icons.error_outline,  Colors.red),
    };

    return Icon(icon, color: color, size: 18);
  }

  Color _textColor() => switch (state) {
    .connected    => Colors.green,
    .error        => Colors.red,
    .disconnected => Colors.grey,
    _             => Colors.orange,
  };
}

duration: .fromMilliseconds(300) resolves to Duration.fromMilliseconds(300) because AnimatedSwitcher.duration is typed Duration. padding: .symmetric(horizontal: 16, vertical: 12) resolves to EdgeInsets.symmetric(...) because Padding.padding is typed EdgeInsets. mainAxisAlignment: .spaceBetween resolves to MainAxisAlignment.spaceBetween. crossAxisAlignment: .center resolves to CrossAxisAlignment.center. mainAxisSize: .min resolves to MainAxisSize.min. fontWeight: .w500 resolves to FontWeight.w500 because TextStyle.fontWeight is FontWeight?.

if (state == .error && onRetry != null) uses the equality special rule. state is typed ConnectionState, so .error resolves to ConnectionState.error. The switch inside _buildIcon() switches on state (typed ConnectionState), providing context for all case patterns.

Each .connecting, .connected, .disconnected, .limited, and .error resolves to the corresponding enum value. The _textColor() method's switch has the same structure.

The Screen

// lib/screens/network_demo_screen.dart

import 'package:flutter/material.dart';
import '../models/connection_state.dart';
import '../models/network_config.dart';
import '../widgets/connection_status_widget.dart';

class NetworkDemoScreen extends StatefulWidget {
  const NetworkDemoScreen({super.key});

  @override
  State<NetworkDemoScreen> createState() => _NetworkDemoScreenState();
}

class _NetworkDemoScreenState extends State<NetworkDemoScreen> {
  ConnectionState _state = .connecting;        // enum shorthand on field
  NetworkConfig _config = .standard();         // named constructor shorthand

  void _simulateConnection() {
    setState(() => _state = .connected);       // enum shorthand in closure
  }

  void _simulateError() {
    setState(() => _state = .error);           // enum shorthand in closure
  }

  void _simulateDisconnect() {
    setState(() => _state = .disconnected);    // enum shorthand in closure
  }

  void _resetToConnecting() {
    setState(() {
      _state = .connecting;                    // enum shorthand in block
      _config = .debug();                      // named constructor shorthand
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Network Status Demo'),
        centerTitle: true,
      ),
      body: Column(
        mainAxisAlignment: .center,            // enum shorthand on parameter
        crossAxisAlignment: .stretch,
        children: [
          ConnectionStatusWidget(
            state: _state,
            onRetry: _state == .error ? _resetToConnecting : null,
          ),
          const Divider(),
          Padding(
            padding: .all(16),                // named constructor shorthand
            child: Column(
              mainAxisSize: .min,
              children: [
                Text(
                  'Simulate state change:',
                  style: TextStyle(fontWeight: .bold),
                ),
                const SizedBox(height: 12),
                Row(
                  mainAxisAlignment: .spaceEvenly,
                  children: [
                    ElevatedButton(
                      onPressed: _simulateConnection,
                      child: const Text('Connect'),
                    ),
                    ElevatedButton(
                      onPressed: _simulateDisconnect,
                      child: const Text('Disconnect'),
                    ),
                    ElevatedButton(
                      onPressed: _simulateError,
                      child: const Text('Error'),
                    ),
                  ],
                ),
                const SizedBox(height: 8),
                TextButton(
                  onPressed: _resetToConnecting,
                  child: const Text('Reset'),
                ),
              ],
            ),
          ),
          Padding(
            padding: .symmetric(horizontal: 16), // named constructor shorthand
            child: Card(
              child: ListTile(
                title: const Text('Config'),
                subtitle: Text(
                  'Timeout: ${_config.timeout.inSeconds}s | '
                  'Retries: ${_config.maxRetries}',
                ),
                trailing: Switch(
                  value: _config.showDetailedErrors,
                  onChanged: null,
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

ConnectionState _state = .connecting declares the field with an explicit type ConnectionState, which provides the context for .connecting. This is one of the most impactful uses: initializing a stateful field in a widget's state class is now a one-read expression.

NetworkConfig _config = .standard() calls the static factory method on NetworkConfig using the field's declared type as context. setState(() => _state = .connected) uses .connected inside a lambda where _state is already declared as ConnectionState. The assignment target _state provides the context type.

_state == .error ? _resetToConnecting : null uses the equality special rule: _state is ConnectionState, so .error resolves to ConnectionState.error. mainAxisAlignment: .center, crossAxisAlignment: .stretch, mainAxisSize: .min, fontWeight: .bold, mainAxisAlignment: .spaceEvenly all resolve from their respective parameter types. padding: .all(16) and padding: .symmetric(horizontal: 16) resolve from the EdgeInsets type of Padding.padding.

The Entry Point

// lib/main.dart

import 'package:flutter/material.dart';
import 'screens/network_demo_screen.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Dot Shorthand Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const NetworkDemoScreen(),
    );
  }
}

This is a standard Flutter entry point. The dot shorthand feature doesn't change how apps are wired up. Every shorthand in this codebase resolves at compile time, producing exactly the same binary as if you had written the full TypeName.member form throughout.

Conclusion

Dot shorthands aren't a dramatic language redesign. They're a precision quality-of-life improvement that removes a specific, well-defined category of noise from Dart and Flutter code: the repetition of a type name that the compiler already knows.

In the places where they work, they work cleanly and unambiguously, and the resulting code communicates meaning without the visual overhead of prefix repetition.

The feature's power is proportional to how much you use enums, static factories, named constructors, and switch statements. If you write Flutter widgets, you use all of these constantly. That's why the Flutter community's reaction to dot shorthands was strong and positive: these are the patterns Flutter developers write every day, and the noise reduction is immediately visible from the first widget you edit.

The mental model to keep is the single rule at the center of the feature: a dot shorthand works only where the compiler already knows the expected type. Once that rule is clear, the feature becomes predictable.

You'll know instantly whether a shorthand is valid at any given position: look for the context type. If there is one (from a variable declaration, a parameter type, a return type, or the left side of an equality comparison), the shorthand works. If there's not (from var, dynamic, or an unannotated expression), it does not.

The adoption path for an existing codebase is straightforward. Update the SDK constraint in pubspec.yaml. Enable the prefer-shorthands-with-enums lint rule from DCM if your team uses it. Let the linter find the highest-value opportunities. Migrate switch statements and widget parameter enums first, where the context is clearest and the visual gain is highest. Work outward from there to named constructors and static methods where the type name adds genuinely redundant information.

The feature is available now in Dart 3.10, Flutter 3.38, and DartPad. The existing code you write using the full form continues to compile without change. Adoption is fully incremental. There's no migration deadline, no deprecation warning, and no behavioral difference. It's simply a cleaner way to say what your code was already saying.

References



Read the whole story
alvinashcraft
2 minutes ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories