Ana içeriğe atla

İlk Riverpod uygulamanız

Bu öğreticide Riverpod kullanarak rastgele şaka üreten bir uygulama geliştireceğiz:

Önemli noktalar

  • Riverpod'u kurmayı öğrenin
  • Ağ isteği yapmak için ilk provider'ınızı oluşturun
  • Veriyi göstermek için Consumer kullanın
  • Yükleme ve hata durumlarını göstermek için AsyncValue'yu ele alın

Projeyi hazırlamak

Bir Flutter projesi oluşturmak

Başlangıç olarak yeni bir Flutter projesi oluşturalım:

flutter create first_app

Ardından projeyi tercih ettiğiniz editörde açın.

Sahte bir arayüz oluşturmak

Herhangi bir mantık yazmaya başlamadan önce uygulamamızın arayüzünü oluşturalım. Gerçek bir API kullanmak yerine statik veriyle başlayacağız.

Projemizin lib dizininde home.dart adında yeni bir dosya oluşturalım. İçine aşağıdaki kodu yapıştırabilirsiniz:

lib/home.dart
import 'package:flutter/material.dart';

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


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Random Joke Generator')),
body: SizedBox.expand(
child: Stack(
alignment: Alignment.center,
children: [
const SelectableText(
'What kind of bagel can fly?\n\n'
'A plain bagel.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 24),
),

Positioned(
bottom: 20,
child: ElevatedButton(
onPressed: () {},
child: const Text('Get another joke'),
),
),
],
),
),
);
}
}

Ardından main.dart dosyamızı bu yeni HomeView widget'ını kullanacak şekilde güncelleyebiliriz:

lib/main.dart
import 'package:flutter/material.dart';

import 'home.dart';

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

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


Widget build(BuildContext context) {
return const MaterialApp(home: HomeView());
}
}

Uygulamayı şimdi çalıştırırsanız şunu görmelisiniz:

Sahte arayüz

Projeye Riverpod eklemek

Projeyi oluşturduktan sonra Riverpod'u bağımlılık olarak eklememiz gerekiyor.

Riverpod'u Flutter ile kullanacağımız için flutter_riverpod paketini kuracağız.
Benzer şekilde ağ isteklerini Dio paketiyle yapacağımız için onu da kuracağız.

Bunu terminalinizde aşağıdaki komutu yazarak yapabilirsiniz:

flutter pub add flutter_riverpod dio

Bu komut, Riverpod'un en son sürümünü Dio ile birlikte projenize ekleyecektir.

(İsteğe bağlı) riverpod_lint eklemek

Daha iyi Riverpod kodu yazmanıza yardımcı olması için riverpod_lint paketini kurabilirsiniz.
Bu paket, Riverpod kodunu daha kolay yazmanızı sağlayan yeniden düzenleme (refactoring) seçenekleri ve sık yapılan hatalardan kaçınmanıza yardımcı olan lint kuralları sunar.

Riverpod_lint, analysis_server_plugin kullanılarak geliştirilmiştir. Bu nedenle analysis_options.yaml üzerinden kurulur.

Kısacası, pubspec.yaml dosyanızın yanına bir analysis_options.yaml oluşturun ve şunu ekleyin:

analysis_options.yaml
plugins:
riverpod_lint: <https://pub.dev/packages/riverpod_lint adresindeki en son sürüm>

main fonksiyonumuza ProviderScope eklemek

Riverpod'un çalışabilmesi için main fonksiyonumuzu bir ProviderScope içerecek şekilde güncellememiz gerekir.
Bu nesneler hakkında ProviderContainers/ProviderScopes bölümünden bilgi edinebilirsiniz.

Güncellenmiş main fonksiyonu şöyle:

lib/main.dart
void main() {
runApp(
// Uygulamanızın üstüne ProviderScope ekleyin
const ProviderScope(
child: MyApp(),
),
);
}

Bir model sınıfı oluşturmak

Bu öğreticide veriyi bir rastgele şaka üreteci API'sinden çekeceğiz.

Bu API şuna benzeyen bir JSON nesnesi döndürür:

{
"type": "general",
"setup": "Why did the scarecrow win an award?",
"punchline": "Because he was outstanding in his field.",
"id": 333
}

Bu veriyi uygulamamızda temsil etmek için Joke adında bir model sınıfı oluşturacağız.

Bunun için projemizin lib dizininde joke.dart adında yeni bir dosya oluşturalım. Joke sınıfı şöyle görünüyor:

lib/joke.dart
class Joke {
Joke({
required this.type,
required this.setup,
required this.punchline,
required this.id,
});

factory Joke.fromJson(Map<String, Object?> json) {
return Joke(
type: json['type']! as String,
setup: json['setup']! as String,
punchline: json['punchline']! as String,
id: json['id']! as int,
);
}

final String type;
final String setup;
final String punchline;
final int id;
}

fromJson factory kurucusuna dikkat edin.
API'miz bir JSON nesnesi döndürdüğü için, JSON verisini Joke sınıfımıza dönüştürecek bir yola ihtiyacımız var. Bu kurucu bir Map<String, Object?> alır ve bir Joke örneği döndürür.

API'yi çağıran bir fonksiyon yazmak

Model sınıfımız hazır olduğuna göre, veriyi API'den çeken bir fonksiyon yazabiliriz. Burada Dio paketini kullanacağız; çünkü bir istek başarısız olduğunda doğal olarak exception fırlatıyor ve bu bizim senaryomuz için elverişli. Ancak tercih ettiğiniz herhangi bir HTTP istemcisini kullanabilirsiniz.

Bu mantığı, Joke sınıfıyla yakından ilişkili olduğu için az önce oluşturduğumuz joke.dart dosyasına yerleştirebiliriz.

lib/joke.dart
final dio = Dio();

Future<Joke> fetchRandomJoke() async {
// Herkese açık bir API'den rastgele bir şaka çekiyoruz
final response = await dio.get<Map<String, Object?>>(
'https://official-joke-api.appspot.com/random_joke',
);

return Joke.fromJson(response.data!);
}
bilgi

API çağrısından gelen hiçbir hatayı yakalamadığımıza dikkat edin.
Bu bilinçli bir tercih. Riverpod hataları bizim için ele alacak, dolayısıyla bunu elle yapmamıza gerek yok.

Veriyi çeken bir provider oluşturmak

API'yi sorgulayan bir fonksiyonumuz olduğuna göre, artık o API'nin sonucunu önbelleğe almaktan sorumlu bir "provider" oluşturabiliriz.
Provider'lar hakkında daha fazla bilgi için Provider'lar sayfasına bakın.

fetchRandomJoke fonksiyonumuz bir Future<Joke> döndürdüğü için FutureProvider kullanacağız. Provider'ı da Joke sınıfıyla ilişkili olduğu için aynı joke.dart dosyasına yerleştirebiliriz.

Bunu yaparak fetchRandomJoke fonksiyonunun çalıştırılması önbelleğe alınacak ve değere kaç kez erişirsek erişelim ağ isteği yalnızca bir kez yapılacaktır.

lib/joke.dart
final randomJokeProvider = FutureProvider<Joke>((ref) async {
// Rastgele bir şaka almak için fetchRandomJoke fonksiyonunu kullanıyoruz
return fetchRandomJoke();
});
bilgi

fetchRandomJoke fonksiyonumuz ile randomJokeProvider arasındaki ayrım zorunlu değildir.
Dilerseniz fetchRandomJoke içeriğini doğrudan provider'ın içine de yazabilirsiniz:

final randomJokeProvider = FutureProvider<Joke>((ref) async {
final response = await dio.get<Map<String, Object?>>(
'https://official-joke-api.appspot.com/random_joke',
);

return Joke.fromJson(response.data!);
});

Veriyi arayüzde göstermek

Arayüzümüzü bir Consumer ile sarmalamak

Artık bir provider'ımız olduğuna göre, HomeView widget'ımızı veriyi dinamik olarak yükleyecek şekilde güncelleme zamanı geldi.

Bunun için Riverpod'un bir başka özelliğine ihtiyacımız olacak: Consumer widget'ı.
Bu widget, bir provider'ın değerini okumamızı ve değer değiştiğinde arayüzü yeniden oluşturmamızı sağlar. Kullanımı StreamBuilder gibi widget'ları andırır.

Özellikle Stack'i bir Consumer widget'ı içine almak isteyeceğiz.
Önceki adımda riverpod_lint'i kurduysanız, yerleşik yeniden düzenleme seçeneklerinden birini kullanabilirsiniz:

Consumer ile sarmalama yeniden düzenlemesi iş başında

Güncellenmiş home.dart kodu şöyle görünmeli:

lib/home.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

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


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Random Joke Generator')),
body: SizedBox.expand(
child: Consumer(
builder: (context, ref, child) {
return Stack(
alignment: Alignment.center,
children: [
const SelectableText(
'What kind of bagel can fly?\n\n'
'A plain bagel.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 24),
),

Positioned(
bottom: 20,
child: ElevatedButton(
onPressed: () {},
child: const Text('Get another joke'),
),
),
],
);
},
),
),
);
}
}

Şakamızı elde etmek ve değişikliklerini dinlemek

Artık bir Consumer'ımız olduğuna göre, provider'ımızı okumak için onun ref parametresini kullanabiliriz.
Bu nesneyi kullanarak, provider'ın geçerli değerini elde etmek için ref.watch(randomJokeProvider) çağırabiliriz. Ancak provider'larla etkileşime girmenin başka yolları da var! Daha fazla bilgi için Ref'ler sayfasına bakın.

Güncellenmiş Consumer'ımız şöyle görünmeli:

Consumer(
builder: (context, ref, child) {
final randomJoke = ref.watch(randomJokeProvider);
// ...
},
)

Bu satırla birlikte Riverpod, şakayı API'mizden otomatik olarak çekecek ve sonucu önbelleğe alacaktır. Artık şakayı arayüzümüzde göstermek için randomJoke değişkenini kullanabiliriz.

Yükleme ve hata durumlarını ele almak

Az önce oluşturduğumuz randomJoke değişkeni Joke türünde değil, AsyncValue<Joke> türündedir.
AsyncValue, ağ isteği gibi asenkron bir işlemin durumunu temsil eden bir Riverpod türüdür. Yükleme, başarı ve hata durumları hakkında bilgi içerir. AsyncValue birçok bakımdan StreamBuilder'da kullanılan AsyncSnapshot türüne benzer.

Farklı durumları ele almanın elverişli bir yolu Dart'ın switch özelliğini kullanmaktır. Bir if/else if zincirine benzer, ancak belirli bir nesne üzerindeki koşulları ele almak için özelleştirilmiştir.

AsyncValue ile birlikte kullanılan yaygın bir yöntem şöyledir:

switch (asyncValue) {
// "value" null değilse, elimizde veri var demektir.
case AsyncValue(:final value?):
return Text(value);
// "error" null değilse, işlem başarısız olmuş demektir.
case AsyncValue(error: != null):
return Text('Error: ${asyncValue.error}');
// Ne veri ne de hata durumundaysak, yükleme durumundayız demektir.
case AsyncValue():
return const CircularProgressIndicator();
}
DİKKAT

İşlem sırası önemlidir!
Yukarıdaki sözdizimini kullanıyorsanız, değerleri hatalardan önce kontrol etmek ve yükleme durumunu en sona bırakmak önemlidir.

Farklı bir sıra kullanırsanız, istek çoktan tamamlanmışken ilerleme göstergesi görüntülenmesi gibi hatalı davranışlarla karşılaşabilirsiniz.

Artık Stack'imizi, randomJoke'un durumuna göre şakayı, yükleme göstergesini veya hata mesajını gösterecek şekilde güncelleyebiliriz:

return Stack(
alignment: Alignment.center,
children: [
switch (randomJoke) {
// İstek başarıyla tamamlandığında şakayı gösteriyoruz.
AsyncValue(:final value?) => SelectableText(
'${value.setup}\n\n${value.punchline}',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 24),
),
// Hata durumunda basit bir hata mesajı gösteriyoruz.
AsyncValue(error: != null) => const Text('Error fetching joke'),
// İstek yüklenirken bir ilerleme göstergesi gösteriyoruz.
AsyncValue() => const CircularProgressIndicator(),
},

// <buton kodu değişmeden kalıyor>
],
);

Bu aşamada uygulamamız internete bağlandı ve uygulama açıldığında rastgele bir şaka gösteriliyor!

"Get another joke" butonunu bağlamak

Şu anda uygulama açıldığında rastgele bir şaka gösteriyoruz, ancak butona tıklamak hiçbir şey yapmıyor. Butonu, tıklandığında yeni bir şaka çekecek şekilde güncelleyelim.

ChangeNotifier'a benzer bir kalıp kullanıp durumu elle yönetebilirdik.
Riverpod bu tür kalıpları destekler, ancak burada buna gerek yok.

Bunun yerine, butona tıklandığında Riverpod'a provider'ımızın mantığını yeniden çalıştırmasını söyleyebiliriz. Bu, Ref.invalidate kullanılarak şöyle yapılabilir:

ElevatedButton(
onPressed: () => ref.invalidate(randomJokeProvider),
child: const Text('Get another joke'),
),

Yapmamız gereken tek şey bu!
Butona tıklandığında Riverpod, randomJokeProvider'ın mantığını yeniden çalıştıracak; bu da API'den yeni bir şaka çekecek ve arayüzü buna göre güncelleyecektir.

Yeni bir şaka çekilirken LinearProgressIndicator eklemek

"Get another joke" butonuna tıklandığında uygulamanın herhangi bir yükleme göstergesi göstermediğini fark etmiş olabilirsiniz.

Bunun nedeni, Ref.invalidate çağırdığımızda mevcut önbelleğin yok edilmemesidir. Bunun yerine, yeni şaka çekilirken önceki şakaya dair bilgiyi elimizde tutarız. Bu da yeni şaka çekilirken önceki şakayı göstermemizi sağlar.

Ancak kullanıcı arayüzleri bu durumları ele alıp hem bir yükleme göstergesini hem de önceki şakayı göstermek isteyebilir. Bunun yaygın bir yolu LinearProgressIndicator kullanmaktır. Bu göstergeyi eklemek için AsyncValue.isRefreshing değerini kontrol edebiliriz. Bu bayrak, eski veri mevcutken yeni bir istek yapılıyorsa true olur.

Güncellenmiş Stack'imiz şöyle görünmeli:

return Stack(
alignment: Alignment.center,
children: [
// İkinci istek sırasında özel bir yükleme göstergesi gösteriyoruz
if (randomJoke.isRefreshing)
const Positioned(
top: 0,
left: 0,
right: 0,
child: LinearProgressIndicator(),
),

// Veriyi ve butonu önceki gibi göster
],
);

Hepsi bu kadar!
Artık API'den şaka çeken ve bunları arayüzde gösteren, tamamen çalışır durumda bir rastgele şaka üreteci uygulamamız var.
Üstelik yükleme ve hata durumları gibi tüm uç durumları da ele almış olduk.

Hiçbir yerde try/catch yazmak ya da isLoading = true/false gibi bir kod yazmak zorunda kalmadığımıza dikkat edin.