Pengembangan Aplikasi Mobile

Aplikasi Mobile Modern

Pendalaman lengkap dari internal rendering Flutter hingga integrasi API siap production — mencakup budget frame, arsitektur tiga pohon, model mental performa, clean architecture, dan pola repository.

45 menit bacaTerakhir diperbarui 2026-04-13
Mulai Presentasi
Performance & Architecture
Modern
Mobile Application
From Frame Budget to Clean Architecture
Users do not care about our pattern names. They only feel whether the app is smooth or not.
This session builds a first-principles mental model: from Flutter's render pipeline to why architecture matters as apps scale.
Frame Budget Three Trees Rebuild Control Architecture Multi-Package
5
Sections
45
Minutes
E2E
Mental Model

Agenda

Section 1 — Frame to Pixel
Flutter rendering pipeline and frame budget
🌳
Section 2 — Flutter Three Trees
Widget, Element, RenderObject, and lifecycle
🔧
Section 3 — Performance Practice
const, keys, state quality, and rebuild control
🏗️
Section 4 — Architecture Foundations
SoC, dependency direction, and dependency injection
📦
Section 5 — Pattern & Scaling
Repository, use case & multi-package

What Users Actually Feel 👋

Users do not care whether you use BLoC or Riverpod. They care about one thing only: does this app feel smooth or janky?
🏃
Smoothness
Users perceive 60 FPS as normal. Dropping to 30 FPS immediately feels laggy.
👆
Responsiveness
A tap delay of around 100ms is enough to create frustration and reduce confidence in app quality.
🔋
Consistency
A sudden stutter in a smooth flow feels worse than a consistently low but stable frame rate.
🤔 Think about your own experience: Hands up if you have ever used an app that felt laggy even with fast internet. That feeling is exactly what we are unpacking today.

The Anatomy of Smoothness: Pixel vs Frame

Smoothness is an optical illusion: your screen cannot show motion, only many static images in rapid sequence.
🟦
Pixel (Spatial)
The smallest physical unit on the display. It only knows color and position.
🎞️
Frame (Temporal)
A full snapshot made of millions of pixels at a specific moment in time.
1080p
Over 2 million pixels
1 Frame
One complete image snapshot
Late page
Illusion breaks → jank
📘 Flipbook analogy: UI smoothness works exactly like a flipbook. Your brain stitches frame after frame into motion. If one page opens late, the illusion breaks. Users feel that break as lag or jank.
🎯 To preserve that motion illusion in our digital flipbook, we must keep delivering new frames continuously. How fast must we render and color millions of pixels before the screen asks for the next frame? That is where the Frame Budget begins.

Frame Budget & Deadline ⏱️

If smoothness is the symptom, frame time is the root metric. Every frame has a strict budget.

📐 Fundamental Per-Frame Formula
Tframe = Tbuild + Tlayout + Tpaint + Traster + Tmisc
Hard rule: if total Tframe exceeds the device budget, jank will inevitably happen.
60 Hz
≈ 16.67 ms/frame
120 Hz
≈ 8.33 ms/frame
> Budget
= Jank is guaranteed
VSync
Display sync signal
🚀 Now we know the hard deadline. The next question is: how does Flutter produce one frame from Dart code into pixels on screen?

Section 1 of 10
Frame to Pixel
How Flutter transforms your Dart code into physical pixels on screen — the complete rendering pipeline from build() to GPU rasterization.

The Big Picture 🖼️

Every frame you see on a mobile screen goes through four distinct phases — each one transforming your code closer to actual pixels.
"
A Flutter frame is born in Dart, measured by the layout engine, drawn as commands, and finally rasterized by the GPU.
🔨 Build
📐 Layout
🎨 Paint
⚡ Raster

Phase 1 — Build 🔨

The Build Phase is where build() composes the next UI state as an abstract blueprint before any sizing, positioning, or drawing happens.
🧩
What Happens
Flutter runs build() on UI parts affected by state changes.

The output is an abstract UI blueprint — a declarative description of what the screen should look like, not final pixels.
UI Blueprint Declarative UI Immutable
Key Characteristics
• Build is fast and lightweight
• It re-computes only the parts that changed
• It describes what the UI should look like
• It does not produce final size, position, or pixels yet
Fast Selective Rebuild Blueprint
📝 Conceptual Flow
State / Input Change
build()
UI Blueprint
Hand-off to Layout
💡 The build phase is declarative — you describe what the UI looks like, not how to make it.

Phase 2 — Layout 📐

Layout computes size and position for each element.
📏
Constraints Go Down
Parent sends size limits to each child (min/max width & height).

Flutter represents this with BoxConstraints:
BoxConstraints(minW, maxW, minH, maxH)
BoxConstraints Top-Down Parent → Child
📐
Sizes Go Up
Each child chooses a size within the limits and reports it back.

Parent then sets each child position using offset (x, y).
Size Reporting Bottom-Up Child → Parent
⬇️ Constraints Down, Sizes Up ⬆️
Scaffold
maxW: 390, maxH: 844
constraints
Column
390 × 600
AppBar
390 × 56
sizes

Phase 3 — Paint 🎨

Paint converts layout output into drawing commands for the GPU.
🖌️
Drawing Commands
In this phase, RenderObjects no longer talk in "widgets". They emit raw drawing instructions to a Canvas via paint().

Think of this as geometry-level machine language:
drawRect() — draw rectangles
drawText() — render text at coordinates
drawImage() — render bitmap/icons
drawPath() — draw custom shapes
Canvas DisplayList RenderObject
🗂️
Layer Tree
Paint commands are not merged into one giant canvas. They are grouped into Layers so they can be processed and cached independently.

OffsetLayer — positional shift
OpacityLayer — transparency
ClipRectLayer — clipping region
TransformLayer — rotation/scale

This is why RepaintBoundary matters: repaint dirty layers only, not the entire screen.
Layer Tree Caching RepaintBoundary
💡 Paint turns layout output into draw commands, not final pixels. Pixels are produced in Raster.

Phase 4 — Rasterization ⚡

The Raster Phase is where the GPU turns drawing commands into actual physical pixels on your device screen.
🖥️
Impeller Engine
Flutter uses Impeller (successor to Skia) as its rendering engine.

Pre-compiled shaders — no more jank from shader compilation
Tessellation on the CPU before sending to GPU
• Uses Metal on iOS and Vulkan/OpenGL on Android
Impeller Metal Vulkan GPU
📺
The Final Step
The GPU processes the drawing commands and writes the result into a framebuffer.

• Screen refresh at 60 or 120 FPS (16ms or 8ms per frame)
• The compositor handles layer merging and final output
VSync signals coordinate frame delivery with display hardware
Framebuffer VSync 60/120 FPS
⏱️ Frame Budget
~4ms
Build
~4ms
Layout
~4ms
Paint
~4ms
Raster
Total budget: 16ms per frame for 60 FPS  |  8ms for 120 FPS
⚡ Jank happens when any phase exceeds its budget. Use Flutter DevToolsPerformance Overlay to identify which phase is the bottleneck.

The Complete Pipeline 🔄

Here is the full journey from your Dart code to the pixels you see on screen.
🔨
1. Build Phase
build() → compose UI blueprint → prepare hand-off for layout
Dart / CPU
📐
2. Layout Phase
Constraints ↓ Sizes ↑ → every widget gets exact position & size
CPU
🎨
3. Paint Phase
paint(canvas) → drawing commands → organized into Layer Tree
CPU
4. Raster Phase
Impeller → tessellation → GPU shaders → framebuffer → physical pixels
GPU
🎯
Optimization Tip
Use const widgets, RepaintBoundary, and avoid unnecessary rebuilds to keep each phase under budget.
🔍
Debug Tool
Flutter DevTools Performance Overlay shows UI thread (Build+Layout+Paint) and Raster thread separately.
📱
Target
60 FPS = 16ms budget per frame. 120 FPS = 8ms. Every millisecond counts for smooth scrolling.
🚀 Flutter's rendering pipeline runs on two threads: the UI thread (Build + Layout + Paint) and the Raster thread (GPU execution) — enabling true parallel processing.

Bottleneck Map 🗺️

Pipeline understood. Now let's classify: where exactly can time be exhausted?
🔥
Type A: CPU-bound
Main isolate choked by heavy synchronous work:

• Parsing massive JSON payload
• Sorting thousands of records
• Encryption/hashing on UI thread
Symptom: UI total freeze, unresponsive taps
🔄
Type B: Rebuild/Layout
Rebuild scope is too wide for small changes:

• Huge parent rebuilds
• IntrinsicWidth forcing double layout
• Monolithic state triggering the whole page
Symptom: Janky scroll when data changes rapidly
🎨
Type C: Paint/Raster
GPU overwhelmed by overly fancy visual effects:

• Stacked Opacity + Shadows
• Layered BackdropFilter / Blur
• Overdraw from transparent elements
Symptom: Heavy animations, but UI thread is healthy
🤔 Let's dissect this scenario: If scrolling feels janky right when the app is parsing a massive JSON payload — according to the classifications above, which bottleneck type does this issue fall into?
🌳 To properly control this bottleneck, we must understand Flutter's structure and lifecycle under the hood.

✅ Section 1 Complete
Build → Layout → Paint → Raster: four phases that form one frame
Frame budget: 16ms (60Hz) or 8ms (120Hz) — must not be exceeded
Three bottleneck types: CPU-bound, Rebuild/Layout, Paint/Raster
Flutter runs on two threads: UI thread and Raster thread
🚀 Next up: Flutter Three Trees — Widget, Element & RenderObject

Section 2 of 10
Flutter Three Trees
Understanding Flutter performance means understanding the three layers that run every frame — Widget, Element, and RenderObject.

Deep Dive: Three Layers 🔬

Each layer has vastly different characteristics and computational costs.
📝
Widget Tree
Widget is a blueprint — a lightweight and immutable declaration of the UI you want.

Cheap to create — just config objects
• Frequently recreated whenever state changes
• Does not hold state or execute layout
Blueprint Immutable Declarative
🔗
Element Tree
Element is an identity manager — a mutable slot bridging Widget to RenderObject.

• Has a longer lifespan than Widgets
• Holds references to State, parent, children
• Maintains continuity between frames via reconciliation
Identity Mutable Bridge
🎨
Render Tree
RenderObject is the executor — computes geometry and paints pixels.

Most expensive computation cost
• Executes layout() and paint()
• If frequently recreated for no reason, FPS immediately drops
Layout Paint Heavy Computation
📝 Widget created
🔗 Element matches
🎨 Render calculates
📺 Pixels on screen

Timeline vs Actors ⏱️🎭

Now that the tree roles are clear, separate two dimensions: WHEN a frame runs vs WHO executes it.
⏱️
Pipeline (WHEN)
▶️ Chronological journey of a frame
▶️ Happens within the 16ms budget
▶️ Build → Layout → Paint → Raster
🎭
Three Trees (WHO)
👤 Data structures residing in memory
👤 The actors executing the pipeline
👤 Widget → Element → RenderObject
💡 The Build phase is handled by Widgets & Elements. The Layout & Paint phases are executed entirely by the RenderObject.

Where They Intersect 🔍

Let's peel back the curtain and see exactly WHO does WHAT during the 16ms timeframe.
🔨
1. Build Phase
Actors: Widget & Element.

The blueprint is formed. build() executes. The cheap Widget Tree is generated, and the Element Tree performs reconciliation.
📐
2. Layout Phase
Actor: RenderObject.

Widgets and Elements sit back. The Render Tree takes over to execute constraints down, sizes up.
🎨
3. Paint Phase
Actor: RenderObject.

With sizes confirmed, the RenderObject paints colors, text, and borders into a Display List (Layers).
4. Raster Phase
Actor: GPU Engine (Impeller).

The Three Trees are completely hands-off. The engine converts the Display List into physical screen pixels.

Reconciliation: The Heart of Updates 🫀

When state changes, Flutter doesn't automatically tear down the UI. It matches old and new nodes using three signals.
🏷️
runtimeType
Is the widget type still the same?
Container vs Column = different
📍
Position
Child slot order in parent
The 2nd child remains the 2nd child
🔑
Key
Explicit identity from developer
Mandatory when lists move/reorder

⚙️ canUpdate Rule
type match + key match ✅ Update/Reuse
type / key mismatch 🔄 Remount Subtree
💡 Flutter does not tear down the entire UI when state changes. It performs reconciliation: matching old and new nodes based on runtimeType + key.

Element Lifecycle 🔄

Elements don't live and die as fast as Widgets. They have a lifecycle with real performance implications.

🟢
1. Mount
Element is created and attached to parent — happens on first appearance in tree
Creation Cost
🔵
2. Active → Update
Serves build and context — receives new compatible widgets
✅ Cheap
🟡
3. Dirty → Rebuild
Marked for recalculation due to setState or dependency change
Depends on Scope
🔴
4. Deactivate → Unmount
Removed from tree — state lost, RenderObject dismantled
⚠️ Expensive
🧭 Let's test your intuition: Based on the reconciliation above, if a widget changes from Container to SizedBox, which path will Flutter take — Update or Unmount→Mount?
🔧 Now you understand the engine. Next up: concrete techniques you can use immediately — const, key, state quality, rebuild control.

✅ Section 2 Complete
Widget = lightweight blueprint, immutable, frequently discarded and recreated
Element = identity slot that maintains continuity between frames
RenderObject = heavy executor for layout and paint
Reconciliation based on runtimeType + position + key
Update path is cheap — Unmount→Mount path is expensive
🚀 Next up: Performance Practice — const, keys, state quality & rebuild control

Section 3 of 10
Performance Practice
From the three trees theory, we now enter techniques that can be applied immediately — without deep tuning.

const, Key, & State Quality 🎯

These three concepts are often cited but rarely understood why they work. The key lies in our understanding of the three layers.
🧊
const — Canonicalization
const means Flutter can reuse the same object when the value is the same.

• Same const value = same instance
• No need to create that object again and again
• Fewer new objects = lower GC pressure
• Great for UI parts that never change
Object Reuse GC Pressure Static UI
🔑
Key — Stable Identity
Keys are used when the framework needs to distinguish item identities securely:

ValueKey(id) — uses stable values from domain (safest)
ObjectKey — relies on memory reference
UniqueKey — always fresh → kills reuse if used carelessly
GlobalKey — crosses tree boundaries to access State, more expensive
ValueKey List/Reorder Identity
🎯
State Quality
Good state is compact, localized, and does not trigger chain rebuilds:

• Small state = small rebuild scope
• State separated by concern = no cross-triggers
• Equality must be accurate → selector/buildWhen becomes effective
• State with wrong equality → all controls become useless
Localized Immutable Equality

⚠️ Remember: const doesn't solve every performance issue. If the bottleneck is in heavy layout or GPU raster, adding const is not enough.

Rebuild Control: Precision, Not Castration 🎯

Our architectural optimization goal is not to eliminate rebuilds, but to make them precisely targeted.
"
The real disease isn't the rebuild itself, but when a blast radius of rebuild scope is too massive and incompatible with the actual visual change.
Anti-Pattern
❌ One setState → rebuilds entire page
❌ Monolithic state mixing all concerns
❌ Listener at Page level, not leaf level
❌ Heavy computations inside build()
Best Practice
✅ setState only on widgets that change
✅ State separated by update frequency
✅ Listener (BlocBuilder) at the leaf level
✅ Computation in BLoC/ViewModel, not build()

Four Rebuild Control Tactics 🛡️

Concrete strategies you can apply right away — from the easiest to the most impactful.
1 Separate by Update Frequency
Static elements (title, desc) shouldn't be in the same widget as dynamic elements (timer, counter). Split the subtree.
2 Listeners at the Leaf
Place BlocBuilder/Consumer as close as possible to the changing widget. Do not wrap the whole Card.
3 Selector / buildWhen
Use context.select() or BlocSelector to subscribe only to specific data. Rebuild only if that exact piece changes.
4 Keep build() Light
Avoid sorting, formatting, or heavy compute in build(). Move them to BLoC/Controller — build() should only compose UI.
💡 Rebuilds are not the enemy. Rebuilds are the heartbeat of Flutter. What is dangerous is an excessively huge rebuild radius that is disproportionate to the actual visual change.

DevTools: Observing, Not Guessing 🔍

Performance can be measured, not guessed. Just know these three things.
📊
Frame Chart
Duration history of each frame
Tall bar = frame exceeded budget
DevTools Frame Chart
Answers: WHEN did jank happen?
🔥
Flame Chart
Function hierarchy call stack
Widest block = bottleneck
DevTools Flame Chart
Answers: WHY did jank happen?
🔁
Rebuild Profiling
Monitor which widgets rebuild
Widget Rebuild Stats in DevTools
DevTools Rebuild Profiling
Answers: WHAT rebuilds too often?
👀 Observe Symptom
📊 Identify Phase
🔥 Root Cause (Flame)
🔧 Targeted Fix
📏 Re-verify
🎯 Performance is not a cosmetic task polished after features are done. Performance is a direct consequence of architectural decisions made from day one.

✅ Section 3 Complete
const = reuse same object instance when value is the same
Key = stable identity, safe default: ValueKey(id)
State quality = compact, localized, accurate equality
Rebuild control = precision scoping, not zero rebuilds
DevTools = Frame Chart (when), Flame Chart (why), Rebuild Profiling (what)
🚀 Next up: Architecture Foundations — from frame efficiency to team efficiency

Section 4 of 10
Architecture Foundations
If performance is about per-frame efficiency, architecture is about team efficiency and code evolution per month.

Bridge: From Performance to Architecture 🌉

We've understood how one frame is born to become pixels. Now the question is: what happens when the app scales?
"
If performance is about per-frame efficiency, architecture is about team efficiency and code evolution per month. Both are about keeping the system healthy as scale increases.
— Bridging Principle
📈
Feature Growth
Dependencies grow exponentially as new features arrive
👥
Team Growth
Without boundaries, merge conflicts and ownership get muddy
🔄
Changing Req.
Backend changes breaking the UI because of tight coupling
💥
Maintenance Cost
Without structure, maintenance costs explode over time
🏗️ Architecture is not an overhead for small projects. Architecture is an investment that pays for itself when the application grows complex.

Three Architectural Pillars 🏛️

Architectural foundations are built from three mutually reinforcing principles.
🧩
Clear Responsibilities
One component, one dominant reason to change.

• UI files handle appearance and user actions
• Core rules stay separate from data transport details
• Technical concerns (HTTP, DB, cache) stay outside the core
• Each part stays focused and easier to reason about
Single Responsibility Layer Boundary Cohesion
⬆️
Dependency Direction
Outer layers may depend on inner layers. Inner layers must not know about outer layers.

• Stable core stays independent of outer details
• Implementations live outside the core
• Wiring happens at the edges of the app
Inward Dependency Stable Core Stability
💉
Dependency Wiring
Classes do not create their own dependencies — they are provided from the outside.

Singleton — one instance for the entire app
Lazy Singleton — created upon first request
Factory — fresh instance every request
• One central wiring point keeps setup predictable
GetIt / Provider Wiring Testable

Separation of Concerns — Detail 🧩

One place should do one type of job. If one file handles UI, business logic, and networking together — a small change breaks everything.
🚨
Leaky Boundary
❌ Widget calling HTTP client directly
❌ Domain entity has JSON parser
❌ Core class naming feature specifics
Healthy Boundary
✅ Domain runs without UI framework
✅ Outer details can change without touching the core
✅ UI only knows state and intent
✅ Core generic and reusable across features
🔍 Let's evaluate: Based on the arrow flow rules above, is a class in the domain layer allowed to call a class from the data layer? Yes or No — and try to think why?

Dependency Direction — Arrow Flow ⬆️

Parts that frequently change should depend on stable parts. But stable parts must never depend on frequently changing parts.

🖥️ Presentation Layer
UI, Screen, Widget — frequently changing
depends on
🎯 Domain Layer
Core rules and shared meaning — highly stable
implements
💾 Data Layer
API, Database, Cache — frequent implementation changes
1 Outer details change
Won't force core changes — adjust the edge layer instead
2 UI changes
Won't break the core idea — the foundation stays intact
3 Testing core logic
You can isolate the core from external systems when testing

Dependency Injection — Proper Wiring 💉

If a class constructs all its own needs, the class becomes hard to test and hard to replace its behavior.
🎛️
Wiring Point
A central point for dependency registration:

main.dart or startup file — central setup
DI tool — stores and provides instances
bootstrap() — app initialization
⏱️
Lifecycle Patterns
Singleton — shared everywhere, one instance

Lazy Singleton — created on first initialization

Factory — newly created every time (great for screen-scoped BLoC)

Pre-resolve — async setup before use
⚠️ Anti-Pattern: All classes calling the container directly from anywhere. Domain must not know the container or DI framework — only fetch dependencies in the integration layer.
📦 Three pillars are now standing. Next we connect them to the more concrete patterns that make apps easier to organize as they grow.

✅ Section 4 Complete
Performance and architecture = two sides of the same coin: keeping the system healthy as scale increases
SoC = one component, one dominant reason for changing
Dependency Direction = outer layers depend on inner layers, not vice versa
DI = dependencies slide in from the outside, not created inside the class
One wiring point keeps setup predictable
🚀 Next up: Next Section — Concrete Patterns

Section 5 of 10
Pattern & Scaling
Repositories and use cases keep business flows explicit. Multi-package keeps structures intact as teams and features scale.

Repository & Use Case 📋

The UI should not fetch data arbitrarily — and business rules shouldn't be scattered all over the place.
🚪
Repository Pattern
Repository is the official gateway to fetch data:

• Domain defines requirements as a contract
• Data layer implements the contract with a data source strategy
• Acts as the single orchestration point for use cases
Abstraction Contract Data Source
🎯
Use Case Pattern
Use cases are the official steps for a business process:

• One use case = one business capability
• Handles rule validation and domain-level orchestration
• Returns standardized success/failure output
Single Capability Business Rule Explicit Flow

🔄 Data Flow: UI → Use Case → Repository → Data Source
🖥️ UI
calls
🎯 Use Case
validate & orchestrate
🚪 Repository
domain contract
💾 Data Source
API, DB, Cache
1 UI Knows No Infra
No need to know if data is from an API, local DB, or cache — simply call the use case.
2 Readable Domain
List of use cases = list of business capabilities. GetTodosUseCase, CreateOrderUseCase, etc.
3 Focused Scenarios
Test business flows just by mocking the repository — no network/database setup required.

Multi-Package: Advanced Scaling 📦

If clean architecture is the mindset for internal structure, multi-package is the strategy for scaling codebases and team ownership.
"
If one house gets too crowded, belongings easily mix up. Multi-package is like dividing things into different rooms with clear labels.
✅ Benefits
Boundaries — Enforced at the package level
Ownership — Highly clear who owns which module
Selective CI — Build, test, analyze per package
Reusability — Packages can be used across apps
⚠️ Tradeoffs
Setup — More complex than a single package
Versioning — Package coordination needs governance
Overhead — Initial costs only pay off at scale
Discipline — Frequent boundary reviews required

Anatomy of a Monorepo 📂

How do we actually divide the app? By extracting core services, reusable UI, and isolated features into their own decoupled packages.
core_services
HTTP, logging, error
core_database
persistence, migration
core_ui
theme, design tokens
feature_auth
domain + data + UI
feature_todo
domain + data + UI
lib/main.dart
orchestrator + bootstrap
📦 Multi-package is an advanced scaling strategy, not an initial requirement. Start simple, split when boundaries and ownership feel like they must be enforced.
🎉 The journey from render phase to large-scale architecture is complete. Before we wrap up, let's lock in the 3 main takeaways.

✅ Section 5 Complete
Repository defines the contract (domain) but is implemented by data
Use Cases hold explicit business flows that are easily testable
Data Flow: UI → Use Case → Repository → API/Cache
Multi-package acts as an enforcer for team discipline and parallel work
🚀 Next up: Takeaways & Journey Recap

Key Takeaways 💎

Three things to remember after this session ends.
Performance = Engine Understanding
Good performance comes from understanding the render engine, not just memorizing tips.

Frame budget, three trees, element lifecycle — all form the correct mental foundations about performance.
🏗️ Architecture = Boundary Discipline
Good architecture comes from strict responsibility boundaries and dependency direction.

SoC, dependency direction, and DI keep the codebase healthy as scale increases.
📦 Multi-Package = Advanced Scaling
Multi-package is an advanced scaling strategy, not an initial requirement.

Start simple, increase structural discipline only when you have measured pains.
🎯 Mental Models > Tricks
This session targeted foundational thinking, not a tuning workshop.

If the mental model is strong, optimization techniques and architectural patterns will logically make sense.

Our Journey 🗺️

A recap of everything we've covered today.
Frame to Pixel
Rendering pipeline — Build, Layout, Paint, Raster — and the 16ms frame budget.
✓ Done
🌳
Flutter Three Trees
Widget, Element, RenderObject — behaviors, costs, and lifecycles that govern performance.
✓ Done
🔧
Performance Practice
const, keys, state quality, rebuild control — from theory to actionable techniques.
✓ Done
🏗️
Architecture Foundations
SoC, dependency direction, and DI — keeping the system healthy as features and teams grow.
✓ Done
📦
Pattern & Scaling
Repository, use cases, Either pattern, and a teaser to multi-package monorepos.
✓ Done

Section 6 of 6
Knowledge Corner: FVM for Real Projects
No in-class mandatory setup: learn the concept first, then use FVM when your project requires strict SDK consistency.

Why We Place FVM at the End 🧭

To keep classroom time efficient, participants can practice using their current Flutter setup. FVM is introduced at the end as real-project readiness.
🚨 Without FVM
Different Flutter versions on each laptop
Mismatched dependency resolution
"It works on my machine" setup issues
✅ With FVM
One team, one Flutter SDK target
Reproducible local setup for everyone
Easier onboarding for new contributors
🎯 Class strategy: practical work can run with each participant's current Flutter SDK, while FVM is introduced as an important tool for real team projects.

Optional Step 1 — Install FVM Globally 📦

For real projects, install FVM once on your machine, then verify the command is available from the terminal.
Run in terminal
dart pub global activate fvm
fvm --version
Expected: package installation logs and a valid FVM version output.
Reference Screenshot — Install FVM
Install FVM using dart pub global activate fvm

Optional Step 2 — Fix PATH If FVM Is Not Recognized 🛠️

If the terminal cannot find FVM, add the Dart pub cache binary path to your user environment variables.
Add this path to User PATH
%USERPROFILE%\AppData\Local\Pub\Cache\bin
After saving PATH, restart terminal and run fvm --version again.
Reference Screenshot — Edit Environment Variables
Windows environment variable path setup for Pub Cache bin

Optional Step 3 — Example Project Initialization 🧱

Use this example flow when your team starts a real project and wants to pin one Flutter SDK baseline.
Run in terminal
mkdir project-flutter
cd project_flutter
flutter create practice_notes_app
cd practice_notes_app
You can replace practice_notes_app with any snake_case project name used by your class.
Reference Screenshot — Create Project
Create Flutter project and move into project folder

Optional Step 4 — Pin the Team Flutter SDK Version 🔒

Inside the project folder, lock a shared Flutter SDK version so everyone uses the same baseline.
Run in terminal
fvm use 3.41.6
fvm flutter --version
This creates project-level FVM config and downloads SDK if needed.
Initialize project with fvm use command
Step 5 — Check Latest Stable SDK
Source of truth for available Flutter versions:
https://docs.flutter.dev/install/archive

Pick one stable version for the class, then lock it with fvm use <version>.
Flutter SDK archive page showing stable versions
✅ Key takeaway: participants understand when to apply FVM: shared team baseline, CI consistency, and onboarding in real production projects.

When Should Teams Actually Use FVM? 🤝

Use FVM when version drift creates friction across contributors, CI pipelines, and onboarding.
Good use cases
  • Multiple contributors with different local Flutter versions.
  • CI pipeline must match the exact local SDK baseline.
  • Long-lived app where SDK upgrade should be deliberate and controlled.
  • Onboarding needs one predictable command flow for all engineers.
In short: apply FVM when consistency cost is higher than setup cost.
Reference Screenshot — Team Consumption Flow
Team members using pinned SDK workflow with FVM in real projects

✅ Knowledge Checkpoint
Participants know FVM is optional for class practicum but valuable for real projects
Participants can explain why SDK pinning helps team consistency and CI reproducibility
Participants know the minimal FVM setup flow (install, path check, fvm use)
Participants know when to adopt FVM: multi-contributor and long-lived production projects
Classroom practical flow remains focused on monorepo and Melos execution speed
🚀 Next up: Continue with monorepo execution using Flutter and Melos in class.

Section 6 of 6
Practicum Corner: Monorepo Setup
After the multi-package concept, continue with a hands-on Melos workflow using each participant's current Flutter SDK.

Quick Intro — Melos After Multi-Package 🧭

Now that participants understand package boundaries, introduce Melos as the workspace orchestrator and move directly into practice.
🎯 Melos becomes easier to adopt after participants understand multi-package boundaries, because they can see exactly why workspace orchestration matters.

Step 1 — Create Project Root and packages/ Workspace 📂

After Melos introduction, create a fresh Flutter project first, then add one `packages/` workspace directory for all local modules.
Run in terminal
flutter create practice_notes_app
cd practice_notes_app
mkdir packages
cd packages
lib/ : app bootstrap (main.dart, routing, composition)
packages/ : all local packages (shared/core/feature modules)
Reference Screenshot — Create project and packages folder
Create Flutter project root and packages folder before package generation
Expected result: project root is ready and terminal context is now inside packages/.

Step 2 — Initialize Local Packages 🧱

Use the Flutter `package` template so each package contains reusable code without platform folders (android/ios).
Run inside /packages
flutter create --template=package core_ui
flutter create --template=package core_services
flutter create --template=package note
Each package gets its own pubspec.yaml.
Reference Screenshot — Package creation output
Create core_ui and core_services packages with flutter create package
Expected result: each package is generated successfully with its own pubspec.yaml and lib/ entry file.

Step 3 — Why This Package Split Helps 📘

Before moving to Melos, make sure participants understand why we split into packages: clearer responsibility boundaries, easier testing, and safer scaling.
Responsibility map
core_ui: reusable UI primitives and design tokens.
core_services: shared services (API, storage, logging, utilities).
note: feature-specific workflows and domain behavior.
Why teams benefit
  • Smaller package scopes make code review and ownership clearer.
  • Shared logic is reused once, not duplicated per feature.
  • Testing and analyzer runs can target only affected packages.
  • Workspace tooling (Melos) can execute commands consistently across all packages.

Step 4 — Architecture Roles: Root App vs Packages 📱

After package creation, the root `lib` remains the orchestrator, while reusable implementation lives inside `packages/`.
Monorepo role mapping
lib/main.dart acts as the orchestrator: app entry point, route shell, DI startup, and package composition.

Reusable implementation is split into core_ui, core_services, and note packages.
Checkpoint — Monorepo Roles
Orchestrator: lib/main.dart
Foundation packages: packages/core_ui and packages/core_services
Feature package: packages/note

Step 5 — Install Melos in Root 🧩

Install Melos as a local dev dependency so every teammate and CI run the same version.
Install command
dart pub add melos --dev
Local install keeps tooling reproducible and avoids dependency on globally installed versions.
  • Command is committed as dev dependency in root pubspec.
  • All teammates run the same Melos version through the project lockfile.
Reference Screenshot — Melos installation output
Run dart pub add melos --dev and install melos as dev dependency

Step 6 — Configure Workspace and Scripts in pubspec ⚙️

Define workspace packages and register an analyzer command so participants can catch code issues early from the monorepo root.
Root pubspec.yaml
workspace:
  - packages/core_ui
  - packages/core_services
  - packages/note

melos:
  scripts:
    analyze:
      description: "Run Flutter analyzer across all workspace packages"
      run: dart run melos exec -- "flutter analyze"
Why introduce analyze here
name: core_ui
resolution: workspace
  • workspace: registers packages managed by Melos.
  • analyze: catches unused imports, type mismatches, and lint issues earlier.
  • exec: flutter analyze: runs the same analyze command for each package from one root command.
  • resolution: workspace: aligns dependency solving across packages.

Step 7 — Verify Bootstrap 🚀

Run bootstrap from project root to connect workspace packages and install dependencies in one pass.
Bootstrap validation
dart run melos bootstrap

# expected hints
Running "flutter pub get" in workspace...
-> 3 packages bootstrapped
Melos bootstrap success output showing 3 packages bootstrapped
What bootstrap does
It discovers workspace packages, installs package dependencies, and links local packages so tooling resolves them as one monorepo.
Pass criteria
You should see workspace pub get success and package bootstrapped summary without dependency resolution errors.

Step 8 — Why Melos Helps in Practice ✅

Connected to Step 6: from one root terminal, Melos can run Flutter commands for all packages or only selected packages without directory hopping.
Command patterns from root
# use script alias from Step 6
dart run melos run analyze

# run one Flutter command for all workspace packages
dart run melos exec -- flutter analyze

# run only for one package (no cd needed)
dart run melos exec --scope="note" -- flutter analyze
Melos analyze output with all packages no issues found
Melos benefits for classroom practice
  • No directory switching: stay in root and still run package commands.
  • One command, all packages: useful for quick class-wide health checks.
  • Scope when needed: target one package with --scope.
  • Consistent workflow: everyone executes the same command pattern from the same place.

Step 9 — Register Root Dependencies 🔗

To make local packages recognized by the main app, add path dependencies in the root pubspec, then run bootstrap.
Root pubspec.yaml dependencies
dependencies:
  flutter:
    sdk: flutter

  core_ui:
    path: packages/core_ui
  core_services:
    path: packages/core_services
  note:
    path: packages/note
After saving dependencies, run dart run melos bootstrap and the workspace links are ready.
✅ The core monorepo mechanism is path dependency: the root app links to local packages directly instead of downloading them from pub.dev.

✅ Practicum Exit Criteria
Root app remains in lib/ as the bootstrap/orchestrator layer
Monorepo package layout uses packages/core_ui, packages/core_services, and packages/note
Melos is installed with dart pub add melos --dev (no global activation)
Root pubspec.yaml defines workspace plus Melos analyze script
Each child package uses resolution: workspace for unified dependency resolution
Bootstrap and analyze commands run from the project root without switching package directories
🚀 Next up: The team is ready for HTTP client and API integration work!

Hand-off 🤝

"
Now that we have foundations in render pipelines and code structure, the next session will dive into state management and API integration — how data moves consistently from the server down to the UI.
— Next Up: State & API Foundations
🚀 Performance and architecture are the two pillars keeping mobile applications healthy. The next pillars — state management and API integration — will complete this big picture.

🚀
Thank You!
Hopefully today's mental foundations help you build healthier mobile apps — from the very first frame to a scalable architecture.
⚡ Performance First
🏗️ Architecture Matters
📦 Scale with Structure
Mobile Engineering Workshop Series
Performance & Architecture Foundations

🔌 API Integration
Menghubungkan
Flutter ke Server
Belajar HTTP Client, Error Handling, Concurrent Request,
Streaming, dan Clean Architecture — dari nol.
Flutter Mobile Development Intermediate Series

Yang Akan Kita Pelajari

🗺️
Section 06
Overview & Arsitektur Clean Arch
🌐
Section 07
HTTP Client & Interceptor (Dio)
Section 08
Error Mapping: Exception → Failure
🚀
Section 09
Concurrent Request, Streaming, & Cancellation
🏛️
Section 10
Repository Pattern: Full Clean Arch

Section 6 of 10
Overview & Arsitektur
Memahami apa itu API Integration dan mengapa Clean Architecture adalah fondasi yang wajib ada.

Apa itu API Integration?

Setiap aplikasi mobile membutuhkan data dari server — API Integration adalah jembatan yang menghubungkan keduanya.
API Integration = Menghubungkan Flutter ke Backend via HTTP,
lalu memproses dan menampilkan datanya ke user.
📡 Alur Request − Response
Flutter App
(kamu)
HTTP Request
POST /login
Backend Server
REST API
Flutter App
(tampilkan data)
JSON Response
{ "token": "..." }
Kenapa butuh Clean Architecture?
Kode terorganisir — mudah dibaca tim lain
Mudah diganti — misalnya ganti Dio ke http tanpa ubah UI
Mudah dites — setiap bagian bisa ditest sendiri-sendiri

Tiga Lapisan Clean Architecture

Clean Architecture memisahkan kode menjadi tiga lapisan dengan dependency yang hanya mengalir satu arah.
Presentation Layer
UI & State
• Widget / Page • Notifier / ViewModel • State: loading, success, error
⬇ dependency mengalir ke bawah
Domain Layer
Business Logic
• Entity (data murni Dart) • UseCase (aturan bisnis) • Repository (contract)
⬇ dependency mengalir ke bawah
Data Layer
Implementasi
• Model (JSON parsing) • RemoteDatasource (Dio HTTP call) • RepositoryImpl
⚠️ Domain TIDAK BOLEH import apapun dari Data atau Presentation

Section 7 of 10
HTTP Client & Interceptor
Menggunakan Dio sebagai HTTP client yang powerful dengan interceptor untuk otomatis mengelola autentikasi token.

Setup Dio — HTTP Client Flutter

Dio adalah HTTP client terpopuler untuk Flutter yang jauh lebih powerful dibanding package http standar.
💡 Dio itu seperti kurir — kamu kasih alamat (URL) + isi paket (body), dia yang kirim & terima balasannya.
Kenapa Dio, bukan http?
Interceptor (middleware)
Auto JSON parsing
Cancel token
Upload/download + progress
Timeout configuration
Retry mechanism
lib/core/network/dio_client.dart
class DioClient {
  // Singleton — 1 instance
  static final _instance =
    DioClient._internal();

  factory DioClient()
      => _instance;

  void _addInterceptors() {
    _dio.interceptors.addAll([
      AuthInterceptor(),
      LoggingInterceptor(),
    ]);
  }
}
💡 Selalu buat Singleton DioClient — satu instance di seluruh aplikasi agar interceptor dan konfigurasi konsisten di mana saja.

Auth Interceptor — Otomatis Kirim Token

Interceptor memungkinkan kamu inject token otomatis ke setiap request tanpa perlu menulis ulang kode di setiap API call.
🔄 Alur Auth Interceptor
Request
Dikirim
Interceptor
Cek Token
Inject
Bearer Token
Server
Balas
401?
Refresh Token
Apa yang terjadi saat token expired (401)?
1
Server balas 401 → Interceptor menangkap error ini
2
Interceptor request token baru ke /auth/refresh
3
Simpan token baru ke Secure Storage
4
Ulangi request asli dengan token baru — user tidak sadar!
⚠️ Gunakan Dio terpisah untuk refresh token → hindari infinite loop!

Section 8 of 10
Error Mapping
Memisahkan error teknis (Exception) dari error bisnis (Failure) agar UI tidak tahu detail implementasi layer bawah.

Dua Lapis Penanganan Error

Setiap error melewati dua transformasi: dari detail teknis ke pesan yang bisa dimengerti oleh Domain dan UI.
💡 Analoginya: Mesin error = Exception. Laporan ke bos = Failure.
Bos tidak perlu tahu detail teknisnya.
❌ Exception (Data Layer)
ServerException
Server error (4xx/5xx)
NetworkException
Tidak ada internet / timeout
UnauthorizedException
Token expired (401)
NotFoundException
Data tidak ditemukan (404)
ValidationException
Data form tidak valid (422)
▶▶
maps to
✅ Failure (Domain Layer)
ServerFailure
Tampilkan pesan server error
NetworkFailure
Minta cek koneksi internet
AuthFailure
Redirect ke halaman login
NotFoundFailure
Tampilkan 'Tidak ditemukan'
ValidationFailure
Tampilkan error per field
💡 Exception bersifat teknis (misal: SocketException). Failure bersifat bisnis (misal: NetworkFailure). Domain Layer hanya kenal Failure.

Either<L, R> — Sukses atau Gagal

Either adalah tipe data functional yang memaksa kamu menangani kedua kemungkinan — sukses maupun gagal.
💡 Either = kotak yang isinya salah satu:
Left(gagal) atau Right(berhasil). Tidak bisa keduanya sekaligus.
Left(failure) = ERROR
result.fold(
  (failure) => setState(() {
    errorMsg = failure.message;
  }),
  ...
);
Ketika gagal → tampilkan pesan error ke user
Right(data) = SUKSES
result.fold(
  ...,
  (user) => setState(() {
    loggedInUser = user;
  }),
);
Ketika sukses → tampilkan data ke user
🚀 Dengan Either, tidak ada exception yang tidak tertangani — compiler memaksa kamu menulis handler untuk Left (gagal) dan Right (sukses).

Section 9 of 10
Concurrent & Streaming
Kirim beberapa request bersamaan untuk performa maksimal, dan kelola streaming data serta pembatalan request yang tidak lagi dibutuhkan.

Request Barengan = Lebih Cepat! 🚀

Sequential = tunggu satu selesai baru mulai berikutnya. Concurrent = kirim semua bersamaan, tunggu yang paling lama.
Sequential (Lambat) 😴
Profile (3s)
Stats (2s)
Notif (1s)
Total: 6 detik 😴
Concurrent / Parallel (Cepat) 🚀
Profile (3s)
Stats (2s)
Notif (1s)
Total: 3 detik 🚀
concurrent_requests.dart
// Kirim semua request BERSAMAAN dengan Future.wait
final results = await Future.wait([
  _dio.get('/user/profile'),
  _dio.get('/user/stats'),
  _dio.get('/notifications?limit=5'),
]);
Future.wait gagal seluruhnya jika satu request saja error. Tangani per-request jika kamu ingin partial success.

Streaming & Cancellation

Tiga pola penting untuk real-time data dan UX yang responsif — download, AI output, dan search debouncing.
📥
Download File + Progress Bar
Tampilkan % progress saat download. User bisa tap tombol 'Batal'.
dio.download(url, path,
  onReceiveProgress:
    (recv, total) {
    progress = recv/total;
  },
  cancelToken: token,
);
🤖
AI Streaming (SSE)
Teks muncul pelan-pelan seperti ChatGPT. Server kirim data bertahap.
await for (
  final chunk in stream
) {
  yield content;
}
🔍
Search + Cancel Token
Request lama dibatalkan otomatis saat user mengetik huruf baru.
_cancelToken?.cancel(
  'new search');
_cancelToken =
  CancelToken();
await search(
  query, token);
💡 CancelToken adalah senjata rahasia UX — batalkan request usang agar tidak ada response basi yang menimpa hasil terbaru di layar user.

Section 10 of 10
Repository Pattern
Pola arsitektur yang memisahkan sumber data dari logika bisnis — membuat kode mudah diganti, dites, dan dikembangkan.

Repository Pattern — Perantara yang Rapi

Repository adalah kontrak (interface) antara Domain dan Data — UseCase hanya berinteraksi lewat kontrak ini.
💡 Repository = perantara antara Domain dan Data.
UseCase tidak perlu tahu dari mana data datang (API / DB / Cache).
🏛️ Alur Repository Pattern
Widget
UI
Notifier
State
UseCase
Logic
Repository
Interface
RepoImpl
Data
Datasource
Dio API
🔄 1 Interface, banyak impl
Bisa ganti dari API ke database tanpa ubah UseCase
🛡️ Error handling terpusat
DioException → AppException → Failure, semua di Repository
🧪 Mudah di-mock saat test
Test UseCase tanpa perlu internet sungguhan

End-to-End: Alur Login dari Tap hingga Tampil

Tracing satu aksi user melalui seluruh lapisan Clean Architecture sampai data dikembalikan ke UI.
1
User tap tombol Login
2
AuthNotifier.login(email, password)
3
LoginUseCase.execute()
4
AuthRepository.login()
5
Dio POST /auth/login → JSON → UserModel
Jika SUKSES:
Right(user) ✅
state = AuthSuccess
"Welcome, Alice!"
Jika GAGAL:
Left(failure) ❌
state = AuthError
"Cek koneksi kamu"
Either<Failure, User>
🎯 Clean Architecture bukan soal folder structure — tapi soal arah dependency. UI bergantung pada Domain, tapi Domain tidak pernah bergantung pada UI atau Data.

Ringkasan — Apa yang Sudah Kita Pelajari
06
HTTP Client & Interceptor
Dio = HTTP client. Interceptor otomatis inject token & refresh saat expired.
07
Error Mapping
Exception (teknis) → Failure (bisnis). Either supaya error tidak crash UI.
08
Concurrent Requests
Future.wait = kirim banyak request barengan → lebih cepat dari sequential.
09
Streaming & Cancellation
Download/SSE dengan progress. CancelToken untuk batalkan request usang.
10
Repository Pattern
Pisahkan UI, Logic, dan Data. Mudah diganti, mudah dites, mudah dikembangkan.

🎉
Selamat! Kamu Sudah Siap!
Kamu sekarang sudah tahu cara menghubungkan Flutter ke server dengan struktur yang rapi dan siap production.
🔌 Implementasi DioClient
🏛️ Login dengan Clean Arch
🧪 Unit test Repository
📡 Coba streaming SSE
Keep building! 💙
Flutter Mobile Development · Intermediate Series