Declarative Rendering
Overview
Flitter adopts a declarative rendering approach, similar to Flutter. This means you declare “how the screen should look,” and Flitter handles the rendering automatically. This is a fundamentally different approach from the traditional imperative method.
Why is it important?
Declarative programming provides the following advantages:
- Improved Code Readability: Intuitive understanding of how UI will appear
- Reduced Bugs: Simplified state management leads to predictable behavior
- Easier Maintenance: UI updates without complex DOM manipulation
- Increased Productivity: Implement complex UIs with less code
Core Concepts
Imperative vs Declarative
Imperative Approach (D3.js Example)
In the imperative approach, you specify what to do at each step:
// Centering text with D3.js
const svg = d3.select("svg");
const width = 400;
const height = 200;
// 1. Create text element
const text = svg.append("text")
.text("Hello, World!");
// 2. Measure text size
const bbox = text.node().getBBox();
// 3. Calculate center position
const x = (width - bbox.width) / 2;
const y = (height + bbox.height) / 2;
// 4. Set position
text.attr("x", x)
.attr("y", y);
// When state changes, all calculations must be redone
function updateText(newText) {
text.text(newText);
const newBbox = text.node().getBBox();
const newX = (width - newBbox.width) / 2;
const newY = (height + newBbox.height) / 2;
text.attr("x", newX).attr("y", newY);
}
Declarative Approach (Flitter Example)
In the declarative approach, you only declare the desired result:
// Centering text with Flitter
Center({
child: Text("Hello, World!")
})
// When state changes, just change the declaration
Center({
child: Text(isKorean ? "안녕하세요!" : "Hello, World!")
})
Imperative vs Declarative Comparison
Example implementing the same functionality with D3.js and Flitter. Click the button to change the text.
Imperative Approach (D3.js)
Implementation with D3.js requires direct DOM manipulation at each step:
// 1. SVG 요소 선택 및 설정
const svg = d3.select("svg");
const width = 400;
const height = 200;
// 2. 텍스트 요소 생성 및 위치 설정
const text = svg.append("text")
.text("Hello, World!")
.attr("x", width / 2)
.attr("y", height / 2)
.attr("text-anchor", "middle")
.attr("font-size", "24px");
// 3. 버튼 생성 (여러 단계 필요)
const button = svg.append("g")
.attr("transform", `translate(${width/2}, ${height/2 + 40})`);
const buttonRect = button.append("rect")
.attr("x", -60)
.attr("y", -15)
.attr("width", 120)
.attr("height", 30)
.attr("fill", "#2196F3");
button.append("text")
.text("텍스트 변경")
.attr("text-anchor", "middle")
.attr("fill", "white");
// 4. 이벤트 핸들러 (DOM 직접 조작)
let currentIndex = 0;
const texts = ["Hello!", "안녕!", "こんにちは!", "Bonjour!"];
buttonRect.on("click", () => {
currentIndex = (currentIndex + 1) % texts.length;
text.text(texts[currentIndex]);
// 텍스트가 길어지면 위치 재계산 필요...
});
Declarative Approach (Flitter)
// UI가 어떻게 보일지만 선언
class MyWidget extends StatefulWidget {
createState() {
return new MyWidgetState();
}
}
class MyWidgetState extends State<MyWidget> {
texts = ["Hello!", "안녕!", "こんにちは!", "Bonjour!"];
currentIndex = 0;
build(context) {
return Column({
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center({
child: Text(this.texts[this.currentIndex])
}),
GestureDetector({
onClick: () => {
this.setState(() => {
this.currentIndex = (this.currentIndex + 1) % this.texts.length;
});
},
child: Container({
padding: EdgeInsets.all(12),
color: Colors.blue,
child: Text("텍스트 변경")
})
})
]
});
}
}
💡 Key Differences
- Imperative Approach (D3.js): Explicitly instruct what to do at each step
- Declarative Approach (Flitter): Just declare the desired result and rendering is handled automatically
How Declarative Rendering Works
- Define State: Define the current state of the application
- Declare UI: Declare how the UI should look based on the state
- Automatic Updates: When state changes, Flitter automatically updates the UI
class _CounterWidget extends StatefulWidget {
createState() {
return new CounterWidgetState();
}
}
class CounterWidgetState extends State<_CounterWidget> {
count = 0; // State definition
build(context: BuildContext): Widget {
// UI declaration - UI is determined by count state
return Column({
children: [
Text(`Current count: ${this.count}`),
GestureDetector({
onClick: () => {
this.setState(() => {
this.count++; // Automatic re-rendering when state changes
});
},
child: Container({
padding: EdgeInsets.all(8),
color: Colors.blue,
child: Text("Increment", { style: TextStyle({ color: Colors.white }) })
})
})
]
});
}
}
// Export as factory function
export default function CounterWidget(): Widget {
return new _CounterWidget();
}
Practical Examples
1. Conditional Rendering
In declarative approach, you can easily show different UIs using conditional statements:
class ConditionalExample extends StatefulWidget {
createState() {
return new ConditionalExampleState();
}
}
class ConditionalExampleState extends State<ConditionalExample> {
isLoggedIn = false;
build(context: BuildContext): Widget {
return Center({
child: this.isLoggedIn
? Column({
children: [
Text("Welcome!"),
GestureDetector({
onClick: () => {
this.setState(() => {
this.isLoggedIn = false;
});
},
child: Text("Logout", {
style: TextStyle({ color: Colors.red })
})
})
]
})
: GestureDetector({
onClick: () => {
this.setState(() => {
this.isLoggedIn = true;
});
},
child: Container({
padding: EdgeInsets.all(12),
color: Colors.blue,
child: Text("Login", {
style: TextStyle({ color: Colors.white })
})
})
})
});
}
}
2. List Rendering
Converting array data to UI is also very intuitive:
class TodoList extends StatefulWidget {
createState() {
return new TodoListState();
}
}
class TodoListState extends State<TodoList> {
todos = ["Learn Flitter", "Understand Declarative UI", "Build an App"];
build(context: BuildContext): Widget {
return Column({
children: [
Text("Todo List", {
style: TextStyle({ fontSize: 20, fontWeight: FontWeight.bold })
}),
SizedBox({ height: 10 }),
...this.todos.map((todo, index) =>
Container({
padding: EdgeInsets.all(8),
margin: EdgeInsets.only({ bottom: 4 }),
color: Colors.grey.shade200,
child: Row({
children: [
Text(`${index + 1}. ${todo}`),
Spacer(),
GestureDetector({
onClick: () => {
this.setState(() => {
this.todos.splice(index, 1);
});
},
child: Icon(Icons.delete, { color: Colors.red })
})
]
})
})
)
]
});
}
}
3. Complex State Management
Even when multiple states are interconnected, they can be easily handled declaratively:
class ShoppingCart extends StatefulWidget {
createState() {
return new ShoppingCartState();
}
}
class ShoppingCartState extends State<ShoppingCart> {
items = [
{ name: "Apple", price: 1000, quantity: 0 },
{ name: "Banana", price: 1500, quantity: 0 },
{ name: "Orange", price: 2000, quantity: 0 }
];
get totalPrice() {
return this.items.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
}
build(context: BuildContext): Widget {
return Column({
children: [
Text("Shopping Cart", {
style: TextStyle({ fontSize: 24, fontWeight: FontWeight.bold })
}),
SizedBox({ height: 20 }),
...this.items.map(item =>
Row({
children: [
Text(item.name),
Spacer(),
Text(`₩${item.price}`),
SizedBox({ width: 20 }),
Row({
children: [
GestureDetector({
onClick: () => {
this.setState(() => {
if (item.quantity > 0) item.quantity--;
});
},
child: Icon(Icons.remove_circle)
}),
Padding({
padding: EdgeInsets.symmetric({ horizontal: 10 }),
child: Text(`${item.quantity}`)
}),
GestureDetector({
onClick: () => {
this.setState(() => {
item.quantity++;
});
},
child: Icon(Icons.add_circle)
})
]
})
]
})
),
Divider(),
Text(`Total: ₩${this.totalPrice}`, {
style: TextStyle({ fontSize: 18, fontWeight: FontWeight.bold })
})
]
});
}
}
Important Notes
- Maintain Immutability: Don’t modify state directly, always use
setState
- Pure Functions: The
build
method should be a pure function without side effects - Performance Considerations: Properly separate widgets to avoid unnecessary re-rendering
// ❌ Wrong - Direct state modification
this.count++; // UI won't update
// ✅ Correct - Use setState
this.setState(() => {
this.count++;
});
// ❌ Wrong - Side effects in build method
build(context: BuildContext): Widget {
// Side effects like API calls should be in initState
fetch('/api/data'); // Wrong!
return Text("...");
}
// ✅ Correct - Handle side effects in initState
initState() {
super.initState();
fetch('/api/data').then(data => {
this.setState(() => {
this.data = data;
});
});
}
Next Steps
Once you understand the concept of declarative rendering, here’s what to learn next:
- Widget System - Understanding StatelessWidget and StatefulWidget
- State Management - Complex state management patterns
- Animation Basics - Implementing declarative animations