Flutter: Implicit Animations ile Sıfır Çaba, Maksimum Etki
Animasyon dendiğinde aklınıza karmaşık matematik formülleri, AnimationController'lar ve sayfalar dolusu kod mu geliyor? Ya size tek bir değişiklikle widget'larınızın akıcı bir şekilde animasyon yapabileceğini söylesem?
Flutter'ın Implicit Animations (örtük animasyonlar) özelliği tam da bunu yapıyor. setState çağırdığınızda, widget'ınız eski değerinden yeni değerine otomatik olarak geçiş yapıyor. Sihir mi? Hayır, Flutter mühendisliği.
Explicit vs Implicit: Fark Nerede?
İki tür animasyon vardır:
Explicit Animations (Belirgin Animasyonlar)
- AnimationController gerektirir
- Tam kontrol isterseniz kullanılır
- Daha fazla kod, daha fazla esneklik
- Örnek: Karmaşık zincir animasyonlar
Implicit Animations (Örtük Animasyonlar)
- Controller gerekmez
- 90% kullanım senaryosu için yeterli
- Az kod, harika sonuç
- Örnek: Buton rengi, boyut değişimi, konum geçişleri
Altın Kural: Animasyonu baştan sona kontrol etmeniz gerekmiyorsa, implicit animation kullanın.
Canlı Demo
Aşağıdaki interaktif örnekte bu widget'ı deneyebilirsiniz:
💡 Eğer yukarıdaki örnek açılmazsa, DartPad linkine tıklayarak yeni sekmede çalıştırabilirsiniz.
AnimatedContainer: Swiss Army Knife
AnimatedContainer, Container'ın animasyonlu versiyonu. Herhangi bir özelliği değiştirdiğinizde otomatik olarak animasyon yapar.
Temel Kullanım
class AnimatedBoxDemo extends StatefulWidget {
@override
_AnimatedBoxDemoState createState() => _AnimatedBoxDemoState();
}
class _AnimatedBoxDemoState extends State<AnimatedBoxDemo> {
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () {
setState(() {
_isExpanded = !_isExpanded;
});
},
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
width: _isExpanded ? 200 : 100,
height: _isExpanded ? 200 : 100,
decoration: BoxDecoration(
color: _isExpanded ? Colors.blue : Colors.red,
borderRadius: BorderRadius.circular(_isExpanded ? 50 : 10),
),
child: Center(
child: Text(
'Tap Me',
style: TextStyle(color: Colors.white),
),
),
),
),
);
}
}Tek bir setState çağrısı, üç farklı özelliği aynı anda animasyon yapıyor:
- Genişlik ve yükseklik
- Renk
- Köşe yuvarlama
AnimatedContainer ile Neler Yapılabilir?
AnimatedContainer, Container'ın tüm özelliklerini animasyonla değiştirebilir:
1. Boyut Animasyonu
AnimatedContainer(
duration: Duration(seconds: 1),
width: isLarge ? 300 : 100,
height: isLarge ? 300 : 100,
child: FlutterLogo(),
)2. Renk Geçişi
AnimatedContainer(
duration: Duration(milliseconds: 500),
color: isActive ? Colors.green : Colors.grey,
child: Text('Status'),
)3. Padding Animasyonu
AnimatedContainer(
duration: Duration(milliseconds: 300),
padding: EdgeInsets.all(isExpanded ? 32 : 8),
child: Icon(Icons.star),
)4. Gradient Animasyonu
AnimatedContainer(
duration: Duration(milliseconds: 800),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: isDarkMode
? [Colors.black, Colors.grey[900]!]
: [Colors.blue, Colors.purple],
),
),
)5. Transform (Dönme/Ölçekleme)
AnimatedContainer(
duration: Duration(milliseconds: 400),
transform: Matrix4.rotationZ(isRotated ? 3.14 : 0),
child: Icon(Icons.refresh),
)Diğer Implicit Animation Widget'ları
Flutter, farklı ihtiyaçlar için özelleşmiş implicit animation widget'ları sunar:
AnimatedOpacity - Görünürlük Geçişleri
Web sitelerinde "fade in" efektleri için mükemmel:
class FadeInDemo extends StatefulWidget {
@override
_FadeInDemoState createState() => _FadeInDemoState();
}
class _FadeInDemoState extends State<FadeInDemo> {
bool _isVisible = false;
@override
void initState() {
super.initState();
// 1 saniye sonra göster
Future.delayed(Duration(seconds: 1), () {
setState(() => _isVisible = true);
});
}
@override
Widget build(BuildContext context) {
return AnimatedOpacity(
opacity: _isVisible ? 1.0 : 0.0,
duration: Duration(milliseconds: 600),
child: Text(
'Merhaba Dünya!',
style: TextStyle(fontSize: 32),
),
);
}
}Kullanım Alanları:
- Bildirim mesajları
- Loading bitince içerik gösterme
- Hover efektleri
- Onboarding ekranları
AnimatedPositioned - Stack İçinde Konum Değişimi
class SlidingMenuDemo extends StatefulWidget {
@override
_SlidingMenuDemoState createState() => _SlidingMenuDemoState();
}
class _SlidingMenuDemoState extends State<SlidingMenuDemo> {
bool _isMenuOpen = false;
@override
Widget build(BuildContext context) {
return Stack(
children: [
// Ana içerik
Container(color: Colors.white),
// Kayan menü
AnimatedPositioned(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
left: _isMenuOpen ? 0 : -250,
top: 0,
bottom: 0,
width: 250,
child: Container(
color: Colors.blue,
child: ListView(
children: [
ListTile(title: Text('Menü 1', style: TextStyle(color: Colors.white))),
ListTile(title: Text('Menü 2', style: TextStyle(color: Colors.white))),
ListTile(title: Text('Menü 3', style: TextStyle(color: Colors.white))),
],
),
),
),
// Hamburger butonu
Positioned(
top: 50,
left: 20,
child: IconButton(
icon: Icon(Icons.menu),
onPressed: () {
setState(() => _isMenuOpen = !_isMenuOpen);
},
),
),
],
);
}
}Kullanım Alanları:
- Drawer (çekmece) menüler
- Floating action button konumu
- Oyun karakterleri
- Parallax efektleri
AnimatedAlign - Hizalama Geçişleri
Widget'ı parent içinde farklı konumlara kaydırın:
class BouncingBallDemo extends StatefulWidget {
@override
_BouncingBallDemoState createState() => _BouncingBallDemoState();
}
class _BouncingBallDemoState extends State<BouncingBallDemo> {
Alignment _alignment = Alignment.topLeft;
void _moveBall() {
setState(() {
_alignment = _alignment == Alignment.topLeft
? Alignment.bottomRight
: Alignment.topLeft;
});
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _moveBall,
child: Container(
width: 300,
height: 300,
color: Colors.grey[200],
child: AnimatedAlign(
alignment: _alignment,
duration: Duration(seconds: 1),
curve: Curves.bounceOut,
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
),
),
);
}
}AnimatedPadding - Boşluk Animasyonu
AnimatedPadding(
duration: Duration(milliseconds: 300),
padding: EdgeInsets.all(isSelected ? 20 : 8),
child: Card(
child: Text('Seçili Kart'),
),
)AnimatedDefaultTextStyle - Text Stil Geçişleri
class TextStyleDemo extends StatefulWidget {
@override
_TextStyleDemoState createState() => _TextStyleDemoState();
}
class _TextStyleDemoState extends State<TextStyleDemo> {
bool _isLarge = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => _isLarge = !_isLarge),
child: AnimatedDefaultTextStyle(
duration: Duration(milliseconds: 300),
style: TextStyle(
fontSize: _isLarge ? 48 : 24,
color: _isLarge ? Colors.red : Colors.blue,
fontWeight: _isLarge ? FontWeight.bold : FontWeight.normal,
),
child: Text('Bana Dokun'),
),
);
}
}AnimatedCrossFade - İki Widget Arası Geçiş
İki widget arasında yumuşak geçiş yapar:
class ToggleViewDemo extends StatefulWidget {
@override
_ToggleViewDemoState createState() => _ToggleViewDemoState();
}
class _ToggleViewDemoState extends State<ToggleViewDemo> {
bool _showFirst = true;
@override
Widget build(BuildContext context) {
return Column(
children: [
AnimatedCrossFade(
firstChild: Container(
width: 200,
height: 200,
color: Colors.blue,
child: Center(child: Text('İlk Widget', style: TextStyle(color: Colors.white))),
),
secondChild: Container(
width: 200,
height: 200,
color: Colors.red,
child: Center(child: Text('İkinci Widget', style: TextStyle(color: Colors.white))),
),
crossFadeState: _showFirst
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: Duration(milliseconds: 500),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => setState(() => _showFirst = !_showFirst),
child: Text('Değiştir'),
),
],
);
}
}Kullanım Alanları:
- Liste ve grid görünümü değiştirme
- Boş durum (empty state) gösterme/gizleme
- Login/Register form geçişi
- Oyun modu değiştirme
AnimatedSwitcher - Herhangi Bir Widget Değişimi
AnimatedCrossFade'den daha esnek:
class CounterWithAnimation extends StatefulWidget {
@override
_CounterWithAnimationState createState() => _CounterWithAnimationState();
}
class _CounterWithAnimationState extends State<CounterWithAnimation> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedSwitcher(
duration: Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
return ScaleTransition(scale: animation, child: child);
},
child: Text(
'$_count',
key: ValueKey<int>(_count), // Önemli: Key gerekli!
style: TextStyle(fontSize: 72, fontWeight: FontWeight.bold),
),
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.remove),
onPressed: () => setState(() => _count--),
),
SizedBox(width: 20),
IconButton(
icon: Icon(Icons.add),
onPressed: () => setState(() => _count++),
),
],
),
],
);
}
}Pro İpucu: AnimatedSwitcher için key parametresi zorunludur. Flutter, key sayesinde widget'ın değiştiğini anlar.
Duration ve Curve: Animasyonun Ruhu
Duration (Süre)
Animasyonun ne kadar süreceğini belirler:
// Çok hızlı - Dikkat çekmeden değişim
Duration(milliseconds: 150)
// Standart - Çoğu kullanım için ideal
Duration(milliseconds: 300)
// Yavaş - Dikkat çekmek için
Duration(milliseconds: 600)
// Çok yavaş - Dramatik efektler
Duration(seconds: 1)Kullanıcı Deneyimi İpucu:
- Küçük değişimler: 150-200ms
- Orta değişimler: 300-400ms
- Büyük değişimler: 500-800ms
- 1 saniyeden uzun: Kullanıcıyı bekletir
Curve (Eğri)
Animasyonun hızlanma/yavaşlama davranışı:
// Doğrusal - Sabit hız (genelde kullanılmaz)
curve: Curves.linear
// Yavaş başla, hızlan, yavaş bitir - En doğal
curve: Curves.easeInOut
// Yavaş başla
curve: Curves.easeIn
// Yavaş bitir
curve: Curves.easeOut
// Elastik - Geri sekme hissi
curve: Curves.elasticOut
// Bounce - Zıplama efekti
curve: Curves.bounceOut
// Hızlı başla, yavaş bitir
curve: Curves.decelerate
// Aşırı gitme ve geri gelme
curve: Curves.anticipateCurve Karşılaştırma
class CurveComparison extends StatefulWidget {
@override
_CurveComparisonState createState() => _CurveComparisonState();
}
class _CurveComparisonState extends State<CurveComparison> {
bool _expanded = false;
Widget _buildAnimatedBox(String label, Curve curve) {
return Column(
children: [
Text(label, style: TextStyle(fontSize: 12)),
AnimatedContainer(
duration: Duration(seconds: 1),
curve: curve,
width: _expanded ? 200 : 50,
height: 50,
color: Colors.blue,
),
],
);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
_buildAnimatedBox('linear', Curves.linear),
SizedBox(height: 10),
_buildAnimatedBox('easeInOut', Curves.easeInOut),
SizedBox(height: 10),
_buildAnimatedBox('bounceOut', Curves.bounceOut),
SizedBox(height: 10),
_buildAnimatedBox('elasticOut', Curves.elasticOut),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => setState(() => _expanded = !_expanded),
child: Text('Animasyonları Başlat'),
),
],
);
}
}Gerçek Dünya Kullanım Senaryoları
1. Genişleyen Kart (Expandable Card)
class ExpandableCard extends StatefulWidget {
final String title;
final String content;
ExpandableCard({required this.title, required this.content});
@override
_ExpandableCardState createState() => _ExpandableCardState();
}
class _ExpandableCardState extends State<ExpandableCard> {
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => _isExpanded = !_isExpanded),
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: _isExpanded ? 10 : 5,
spreadRadius: _isExpanded ? 2 : 0,
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.title,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
AnimatedRotation(
duration: Duration(milliseconds: 300),
turns: _isExpanded ? 0.5 : 0,
child: Icon(Icons.expand_more),
),
],
),
AnimatedCrossFade(
firstChild: SizedBox.shrink(),
secondChild: Padding(
padding: EdgeInsets.only(top: 12),
child: Text(widget.content),
),
crossFadeState: _isExpanded
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: Duration(milliseconds: 300),
),
],
),
),
);
}
}2. Like Butonu Animasyonu
class LikeButton extends StatefulWidget {
@override
_LikeButtonState createState() => _LikeButtonState();
}
class _LikeButtonState extends State<LikeButton> {
bool _isLiked = false;
int _likeCount = 42;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
setState(() {
_isLiked = !_isLiked;
_likeCount += _isLiked ? 1 : -1;
});
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedContainer(
duration: Duration(milliseconds: 200),
curve: Curves.easeInOut,
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: _isLiked ? Colors.red.withOpacity(0.2) : Colors.transparent,
shape: BoxShape.circle,
),
child: AnimatedSwitcher(
duration: Duration(milliseconds: 200),
transitionBuilder: (child, animation) {
return ScaleTransition(scale: animation, child: child);
},
child: Icon(
_isLiked ? Icons.favorite : Icons.favorite_border,
key: ValueKey<bool>(_isLiked),
color: _isLiked ? Colors.red : Colors.grey,
size: 28,
),
),
),
SizedBox(width: 4),
AnimatedDefaultTextStyle(
duration: Duration(milliseconds: 200),
style: TextStyle(
color: _isLiked ? Colors.red : Colors.grey,
fontWeight: _isLiked ? FontWeight.bold : FontWeight.normal,
fontSize: 16,
),
child: Text('$_likeCount'),
),
],
),
);
}
}3. Loading Placeholder (Skeleton Screen)
class ShimmerLoading extends StatefulWidget {
@override
_ShimmerLoadingState createState() => _ShimmerLoadingState();
}
class _ShimmerLoadingState extends State<ShimmerLoading> {
bool _isLoading = true;
@override
void initState() {
super.initState();
// 3 saniye sonra içeriği göster
Future.delayed(Duration(seconds: 3), () {
setState(() => _isLoading = false);
});
}
@override
Widget build(BuildContext context) {
return AnimatedCrossFade(
firstChild: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: double.infinity,
height: 200,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(12),
),
),
SizedBox(height: 12),
Container(
width: 200,
height: 20,
color: Colors.grey[300],
),
SizedBox(height: 8),
Container(
width: 150,
height: 20,
color: Colors.grey[300],
),
],
),
secondChild: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
'https://picsum.photos/400/200',
height: 200,
width: double.infinity,
fit: BoxFit.cover,
),
SizedBox(height: 12),
Text('Başlık Buraya Gelecek', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Text('Alt başlık buraya gelecek', style: TextStyle(color: Colors.grey)),
],
),
crossFadeState: _isLoading
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: Duration(milliseconds: 500),
);
}
}4. Floating Action Button Morph
class MorphingFAB extends StatefulWidget {
@override
_MorphingFABState createState() => _MorphingFABState();
}
class _MorphingFABState extends State<MorphingFAB> {
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: _isExpanded ? 200 : 56,
height: 56,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(28),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(28),
onTap: () => setState(() => _isExpanded = !_isExpanded),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.add, color: Colors.white),
if (_isExpanded) ...[
SizedBox(width: 12),
Text(
'Yeni Ekle',
style: TextStyle(color: Colors.white, fontSize: 16),
),
],
],
),
),
),
),
);
}
}Performans İpuçları
1. const Constructor Kullanın
// ❌ Kötü - Her rebuild'de yeni instance
AnimatedContainer(
duration: Duration(milliseconds: 300),
child: Text('Sabit Metin'),
)
// ✅ İyi - Const child cache'lenir
AnimatedContainer(
duration: Duration(milliseconds: 300),
child: const Text('Sabit Metin'),
)2. Gereksiz AnimatedWidget Kullanmayın
// ❌ Kötü - Sadece renk değişiyorsa Container yeterli
AnimatedContainer(
duration: Duration(milliseconds: 300),
width: 100, // Hiç değişmiyor
height: 100, // Hiç değişmiyor
color: selectedColor,
)
// ✅ İyi - Sadece gerekli widget'ı animasyonla
Container(
width: 100,
height: 100,
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
color: selectedColor,
),
)3. RepaintBoundary ile Optimize Edin
RepaintBoundary(
child: AnimatedOpacity(
opacity: isVisible ? 1.0 : 0.0,
duration: Duration(milliseconds: 300),
child: HeavyWidget(),
),
)4. Çok Fazla Eş Zamanlı Animasyon Yapmayın
// ❌ Kötü - 100 widget aynı anda animasyon yapıyor
ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return AnimatedContainer(
duration: Duration(seconds: 1),
height: heights[index],
child: ListTile(title: Text('Item $index')),
);
},
)
// ✅ İyi - Sadece görünür olanlar animasyon yapsınYaygın Hatalar ve Çözümleri
Hata 1: AnimatedSwitcher'da Key Unutmak
// ❌ Yanlış - Animasyon çalışmaz
AnimatedSwitcher(
duration: Duration(milliseconds: 300),
child: Text('$count'), // Key yok!
)
// ✅ Doğru
AnimatedSwitcher(
duration: Duration(milliseconds: 300),
child: Text('$count', key: ValueKey(count)),
)Hata 2: Duration Çok Uzun
// ❌ Kullanıcı 3 saniye bekliyor
AnimatedContainer(
duration: Duration(seconds: 3),
color: newColor,
)
// ✅ Hızlı ve responsive
AnimatedContainer(
duration: Duration(milliseconds: 300),
color: newColor,
)Hata 3: Curve Seçimi Yanlış
// ❌ Linear çok mekanik görünür
AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.linear,
width: newWidth,
)
// ✅ Doğal görünür
AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: newWidth,
)Tüm Implicit Animation Widget'ları
Flutter'da 15+ implicit animation widget'ı var:
| Widget | Neyi Animasyonlar |
|---|---|
| AnimatedContainer | Tüm Container özellikleri |
| AnimatedOpacity | Opacity (saydamsızlık) |
| AnimatedPadding | Padding (boşluk) |
| AnimatedPositioned | Stack içinde konum |
| AnimatedAlign | Hizalama |
| AnimatedDefaultTextStyle | Text stili |
| AnimatedPhysicalModel | Gölge ve elevation |
| AnimatedCrossFade | İki widget arası geçiş |
| AnimatedSwitcher | Herhangi bir widget değişimi |
| AnimatedSize | Widget boyutu |
| AnimatedRotation | Dönme açısı |
| AnimatedScale | Ölçek |
| AnimatedSlide | Kayma (offset) |
| AnimatedFractionallySizedBox | Parent'a oranla boyut |
| AnimatedTheme | Tema değişiklikleri |
Özet
- Implicit Animations: Kod yazmadan animasyon (setState yeterli)
- AnimatedContainer: En çok yönlü implicit animation widget'ı
- Duration: Animasyon süresi (ideal: 200-400ms)
- Curve: Hızlanma/yavaşlama davranışı (ideal: easeInOut)
- AnimatedOpacity: Fade in/out efektleri için
- AnimatedPositioned: Stack içinde konum değişimi
- AnimatedCrossFade: İki widget arası geçiş
- AnimatedSwitcher: Herhangi bir widget değişimi (key gerekli!)
- Performans: const, RepaintBoundary, gereksiz animasyon yok
- UX: Kısa süreler, doğal curve'ler, amaçlı kullanım
Implicit animations, Flutter'ın en güçlü özelliklerinden biri. Minimum kodla maksimum etki yaratır. Uygulamanıza hayat katmak için setState + Animated* widget kombinasyonu yeterli!