DEV Community

Cover image for Flutter'da Yönlendirme (Routing) 💫 🌌 ✨
Gülsen Keskin
Gülsen Keskin

Posted on • Edited on

Flutter'da Yönlendirme (Routing) 💫 🌌 ✨

Flutter'da Yönlendirme
Flutter'da sayfalar yalnızca rota(route)'lara atadığımız widget'lardır ve route'lar bir widget olan Navigator tarafından yönetilir.

Not: Flutter'da routing(yönlendirme) hiçbir zaman gerçekten statik değildir, ancak tüm rotalarınızı önceden bildirebilirsiniz.

Declarative routing and named routes (Bildirime dayalı yönlendirme ve adlandırılmış route'lar)

Her RouteDefinition'ın bir yolu(path) ve bir component'i vardır (bu muhtemelen bir sayfadır). Bu genellikle bir uygulamanın en üst düzeyinde yapılır.

Mobil uygulamalar genellikle onlarca sayfayı destekler ve uygulamanın her yerinde adsız yollar oluşturmak yerine, onları bir kez tanımlayıp ardından adlarına göre başvurmak daha kolay bir yoldur.

Declaring routes

Adlandırılmış route'ları kullanmanın iki bölümü vardır. Birincisi route'ları belirlemektir.

return MaterialApp(
  debugShowCheckedModeBanner: false,
  theme: _theme,
  routes: {
    ECommerceRoutes.catalogPage: (context) =>
        PageContainer(pageType: PageType.Catalog),
    ECommerceRoutes.cartPage: (context) =>
        PageContainer(pageType: PageType.Cart),
    ECommerceRoutes.userSettingsPage: (context) =>
        PageContainer(pageType: PageType.Settings),
    ECommerceRoutes.addProductFormPage: (context) =>
        PageContainer(pageType: PageType.AddProductForm),
  },
  navigatorObservers: [routeObserver],
);

class ECommerceRoutes {
  static final catalogPage = '/';
  static final cartPage = '/cart';
  static final userSettingsPage = '/settings';
  static final cartItemDetailPage = '/itemDetail';
  static final addProductFormPage = '/addProduct';
}

Enter fullscreen mode Exit fullscreen mode

Adlandırılmış rotalarda gezinme:
Adlandırılmış rotalara gitmek, Navigator.pushNamed methodunu kullanmak kadar kolaydır. pushNamed methodu bir BuildContext ve bir route adı gerektirir, böylece BuildContext'e erişim olan her yerde kullanabilir.

final value = await Navigator.pushNamed(context, "/cart");

Navigator sayfaları bir "stack" yapısında yerleştirir. Stack "last in first out" (son giren ilk çıkar ilkesine göre çalışır). Uygulamanızın home page'ine bakıyorsanız ve yeni bir sayfaya giderseniz, o yeni sayfayı stack'in üstüne (ve ana sayfanın en üstüne) itersiniz. Stack'deki en üst öğe, ekranda gördüğünüz şeydir.

Navigator, stack benzeri bir yapıdır.
Image description

Navigator sınıfı, stack'i yönetmek için bir dizi yararlı yönteme sahiptir.

pop
popUntil
canPop
push
pushNamed
popAndPushNamed
replace
pushAndRemoveUntil

Adlandırılmış route'ları pushlamak'la ilgili önemli bir not, bir Future döndürmeleridir. await anahtar sözcüğü asenkron(eş zamansız) bir değer döndüren ifadeleri işaretlemek için kullanılır.

onPressed: () {
    return Navigator.of(context).pushNamed("/cartPage");
},
Enter fullscreen mode Exit fullscreen mode

Not: Navigator.of(context).pushNamed(String routeName) fonksiyon imzasının, daha önce bahsedilen Navigator.pushNamed(BuildContext context, String routeName) imzasıyla aynı olmadığını fark edebilirsiniz. Bunlar değiştirilebilir.

MaterialDrawer widget'ı ve full menü
Bir Materyal Design uygulaması gördüyseniz, muhtemelen aşağıdaki resim de gösterilen app drawer türünü biliyorsunuzdur.

Image description

İlk önce, menünün gerçekte ne yapmasını istediğimizi düşünelim:

  1. Bir kullanıcı bir menü düğmesine dokunduğunda menü görüntülenmelidir.

  2. Dokunarak bir route'a giden her sayfa için bir menü item olmalıdır.

  3. Uygulama bilgilerini içeren bir modal gösteren bir About menü item olmalıdır.

  4. Kullanıcı bilgilerini gösteren bir menü başlığı olmalıdır. Kullanıcı ayarlarına dokunduğunuzda, user settings sayfasına yönlendirilmelidir.

  5. Menü, o anda hangi route'ın aktif olduğunu vurgulamalıdır.

  6. Bir menü item seçildiğinde veya bir kullanıcı menünün sağındaki menü katmanına dokunduğunda menü kapanmalıdır.

  7. Menü açıldığında veya kapatıldığında, güzel bir şekilde içeri ve dışarı hareket etmelidir.

Bu özel menü drawer, tümü Flutter'da yerleşik olarak bulunan yalnızca beş gerekli widget'ın birleşimidir:

Drawer
ListView
UserAccountsDrawerHeader
ListTile
AboutListTile

Drawer, bu menüyü barındıran widget'tır. Alt argümanında tek bir widget alır. Bir Drawer , büyük olasılıkla drawer argumanında bir Scaffold'a iletilecektir.

Ayrıca drawer'lı bir scaffold'da bir AppBar'ınız varsa, Flutter app bar'da sağ tarafta bulunan simgeyi otomatik olarak bir menü butonuna ayarlar ve dokunulduğunda menüyü açar. Menü güzel bir şekilde canlandırılacak ve sola kaydırdığınızda veya sağdaki butona dokunduğunuzda kapanacaktır.

Menü öğeleri ve uygun widget'lar: ListView ve ListItems

Not: Scaffold'un .automaticallyImplyLeading öğesini false olarak ayarlayarak otomatik menü butonunu geçersiz kılabilirsiniz.

ListView, widget'ları kaydırılabilir bir container'da düzenleyen bir layout widget'ıdır. Çocuklarını varsayılan olarak dikey(vertically) olarak düzenler, ancak oldukça özelleştirilebilirdir. Child adında bir argüman bekleyen diğer genelleştirilmiş widget'ların aksine, ListTile title, subtitle ve leading (satır başı) gibi özelliklere sahiptir ve ayrıca bir onTap özelliği ile donatılmıştır.

Image description

Bir ListView'i daha "materyal-vari" bir menü drawer'a dönüştürmek için özel olarak kullanılan başka bazı özel Flutter widget'ları da vardır.

Örnek AppMenu widget'ı oluşturma methodu:

@override
  Widget build(BuildContext context) {
    _activeRoute ??= "/";
    return Drawer(
      child: ListView(
        children: <Widget>[
          StreamBuilder( //bir child beklemek yerine builder modelini izler
            // ...
            builder: (
              BuildContext context,
              AsyncSnapshot<ECommerceUser>
            ) => UserAccountsDrawerHeader(),
          ), // StreamBuilder
          ListTile(
            leading: Icon(Icons.apps),
            title: Text("Catalog"),
            selected: _activeRoute == ECommerceRoutes.catalogPage,
            onTap: () => _navigate(ECommerceRoutes.catalogPage),
          ),
          ListTile(...),
          ListTile(...),
          AboutListTile(...),
        ],
      ),
    );
  }

Enter fullscreen mode Exit fullscreen mode

UserAccountsDrawerHeader, önemli kullanıcı bilgilerini görüntülemek için kullanılan bir Material widget'ıdır. Butona dokunarak kullanıcı hesapları arasında geçiş yapmanızı sağlayan GMail gibi bir Google uygulaması hayal edin. Bu GMail benzeri kullanıcı arayüzünü, UserAccountsDrawerHeader kullanarak elde edebilirsiniz.

AboutListTile widget'ı ListView.children listesine aktarılabilir ve bu aşağıdaki listede gösterildiği gibi yalnızca birkaç satır kodla yapılandırılabilir.

AboutListTile(
  icon: Icon(Icons.info),
  applicationName: "Produce Store",
  aboutBoxChildren: <Widget>[
    Text("Thanks for reading Flutter in Action!"),
  ],
),
Enter fullscreen mode Exit fullscreen mode

Image description

Navigator.pushReplacementNamed

Navigator.pushReplacementNamed, rota yığınının yeni sayfalar eklemeye devam etmemesini sağlar.

Yeni rotanın animasyonu bittiğinde, navigasyon yaptığınız rotayı kaldırır.

İletişim kutuları veya popup menüler gibi rotalar, kullanıcı tarafından seçilen değeri rotasını oluşturan widget'a döndürmek için genellikle bu mekanizmayı kullanır.

void _switchToBrightness() {
  Navigator.pushReplacementNamed(context, '/settings/brightness');
}
Enter fullscreen mode Exit fullscreen mode

NavigatorObserver: RouteAware ile aktif rotayı vurgulama

Navigator observer, onu dinleyen herhangi bir widget'a "Hey, eğer ilgileniyorsanız, Navigator bir olay gerçekleştiriyor" diyen bir nesnedir.

RouteObserver ise NavigatorObserver'ın bir alt sınıfıdır.
Bu observer(gözlemci), aktif route değişirse tüm dinleyicilerini bilgilendirir.

final RouteObserver<Route> routeObserver =
    RouteObserver<Route>();

class ECommerceApp extends StatefulWidget {
  @override
  _ECommerceAppState createState() => _ECommerceAppState();
}

Enter fullscreen mode Exit fullscreen mode

Route observer'larını MaterialApp widget'ına iletin:

return MaterialApp(
    debugShowCheckedModeBanner: false,
    theme: _theme,
    home: PageContainer(pageType: PageType.Catalog,),
    routes: { ... }
    navigatorObservers: [routeObserver],
);
Enter fullscreen mode Exit fullscreen mode

Artık observer'ı herhangi bir State nesnesinde dinleyebilirsiniz.

State nesnesinin burada gösterildiği gibi RouteAware ile genişletilmesi gerekir:

class AppMenu extends StatefulWidget {
  @override
  AppMenuState createState() => AppMenuState();
}

class AppMenuState extends State<AppMenu>
    with RouteAware { ... }

Enter fullscreen mode Exit fullscreen mode

RouteAware , bir route observer ile etkileşim kurmak için arabirim sağlayan soyut(abstract) bir sınıftır. Artık state nesnenizin didPop, didPush ve diğer birkaç methoda erişimi vardır.

Menüyü aktif route ile güncellemek için stack'de yeni bir sayfa olduğunda bilgilendirilmemiz gerekiyor. Bunun iki adımı vardır: ilk adım, route observer'dan değişiklikleri dinlemek, ikinci adımsa, route değiştiğinde haberdar olmak için observer'ı dinlemektir.

class AppMenuState extends State<AppMenu> with RouteAware {
  String _activeRoute;
  UserBloc _bloc;

    @override
    void didChangeDependencies() {
        super.didChangeDependencies();
        routeObserver.subscribe(
          this,
          ModalRoute.of(context),
        );
        _bloc = AppStateContainer.of(context)
            .blocProvider.userBloc;
    }
}
Enter fullscreen mode Exit fullscreen mode

Artık bu widget, rota değişikliklerinin farkında olduğundan, herhangi bir Navigator etkinliği gerçekleştiğinde aktif rota değişkenini güncellemesi gerekir. Bu, RouteAware'den devralınan didPush yönteminde yapılır:

@override
void didPush() {

  _activeRoute =
    ModalRoute.of(context).settings.name;

}

Enter fullscreen mode Exit fullscreen mode

Anında yönlendirme(Routing on the fly)

Anında yönlendirme, bir olaya yanıt olarak oluşturulana kadar var olmayan bir sayfaya yönlendirme yapabileceğiniz fikridir. Örneğin, kullanıcı bir liste öğesine dokunduğunda yeni bir sayfaya gitmek isteyebilirsiniz. Bu rotayı önceden oluşturmanız gerekmez, çünkü rotalar yalnızca widget'lardır.

void _showListItemDetailPage() async {
    await Navigator.push(//adlandırılmış route'lara Navigator.pushNamed aracılığıyla gidilir
      context,
      MaterialPageRoute(
        builder: (context) => SettingsPage(
              settings: settings,
            ),
        fullscreenDialog: true, //tam ekran
      ),
    );
  }
Enter fullscreen mode Exit fullscreen mode

Flutter'da, route stack'de yeni bir widget gibi görünen her şey bir rotadır. Modallar, bottom sheet'ler, snack bar'lar ve diyalog'ların tümü rotalardır ve bunlar anında yönlendirme için mükemmel adaylardır.

MaterialRouteBuilder

Future _toProductDetailPage(Product product) async {
    await Navigator.push(//stack'e yeni bir sayfa eklemek için Navigator.push'u kullanabilirsiniz.
        context,
        MaterialPageRoute(//MaterialPageRoute, PageRoute'un bir alt sınıfıdır ve tüm Material widget işlevselliğini widget tree'deki yeni yerinde sağlar.
            builder: (context) => //MaterialPageRoute gibi route nesneleri, bir callback alan ve widget döndüren bir builder argument değişkeni gerektirir.
            ProductDetailPageContainer(
              product: product,
            ),
        ),
    );
}\
Enter fullscreen mode Exit fullscreen mode

showSnackBar, showBottomSheet ve benzerleri

Flutter, modallar ve snackbarlar gibi sayfa olmayan route'ları kullanmayı çok kolaylaştıran widget'lara ve mantığa sahiptir. Bunlar bir sayfaya eklenmek yerine Navigator stack'ine eklenen widget'lardır.

Bazı route'lar ekranın tamamını kaplamaz:
Image description

Bu uygulamada, bottom sheet (iOS uygulamalarında yaygın olan) ve bir snackbar kullanıyoruz. Bunlar, ekranın altından görünmeleri ve ekranın yalnızca bir bölümünü kaplamaları bakımından benzerdir. Yine de farklılardır çünkü bottom sheet bir ModalRoute'dır. Yani, görüntülendiğinde, altındaki sayfayla etkileşime giremezsiniz. Snack bar, uygulamayı engellemez, bu nedenle görünümdeyken etkileşimde bulunmaya devam edebilirsiniz.

Aşağıdaki örnekte bottom sheet aynı Catalog widget'ında uygulanır ve ProductDetailCard.onLongPress methodu aracılığıyla bir ProductDetailCard'a basılı tutularak başlatılır:

return ProductDetailCard(
    key: ValueKey(_product.imageTitle.toString()),
    onTap: () => _toProductDetailPage(_product),
    onLongPress: () =>
        _showQuickAddToCart(_product), //Uzun bir basışta, bottom sheet'i gösterir.
    product: _product,
);

Enter fullscreen mode Exit fullscreen mode

Bottom sheet widget örneği:
Image description

_showQuickAddToCart methodu:

void _showQuickAddToCart(BuildContext context, Product product) async {
  CartBloc _cartBloc = AppState.of(context).blocProvider.cartBloc;

  int qty = await showModalBottomSheet<int>(//1

    context: context, //2

    builder: (BuildContext context) { //3
        return AddToCartBottomSheet(
            key: Key(product.id),
        );
    });

    _addToCart(product, qty, _cartBloc);
}
Enter fullscreen mode Exit fullscreen mode

1: showModalBottomSheet, Flutter tarafından sağlanan ve sizin için yönlendirmeyle(routing) ilgilenen global bir methoddur. Tür bildirimi (), bottom sheet'den ne tür verilerin geri gönderilmesini bekleyebileceğinizi söyler. Bu satır ayrıca showModalBottomSheet'in dönüş değerine bir değer atar. Method bir Future döndürdüğünden, wait anahtar sözcüğünü kullanmanız gerekir. Future, temel olarak, "Kullanıcı bottom sheet'i kapatır kapatmaz size doğru değeri vereceğim ve doğru değeri alacağım" der.

2: Tüm rotaların bir BuildContext'e ihtiyacı vardır, böylece Flutter bunları ağaçta nereye ekleyeceğini bilir.

3: Tüm rota değiştirme methodları, widget döndüren bir callback bekler

Image description

class AddToCartBottomSheet extends StatefulWidget {
  const AddToCartBottomSheet({Key key}) : super(key: key);

  @override
  _AddToCartBottomSheetState createState() => _AddToCartBottomSheetState();
}

class _AddToCartBottomSheetState extends State<AddToCartBottomSheet> {
  int _quantity = 0;
  // ...

  @override
  Widget build(BuildContext context) {
    return ConstrainedBox(
      constraints: BoxConstraints(
        minWidth: MediaQuery.of(context).size.width,
        minHeight: MediaQuery.of(context).size.height / 2,
      ),
      child: Column(
        children: <Widget>[
          Padding(
            // ...
            child: Text("Add item to Cart"),
          ),
          Padding(
            // ...
            child: Row(
              children: <Widget>[
                IconButton(...) // decrease qty button
                Text(...) // current quanity
                IconButton(...) // increase qty button
              ],
            ),
          ),
          RaisedButton(
            color: AppColors.primary[500],
            textColor: Colors.white,
            child: Text(
              "Add To Cart".toUpperCase(),
            ),
            onPressed: () =>
              Navigator.of(context).pop(_quantity),
          )
        ],
      ),
    );
}


Enter fullscreen mode Exit fullscreen mode

Yönlendirme animasyonları

Sayfalar yalnızca widget'lardır, bu nedenle diğer widget'lar gibi canlandırılabilirler.
Sayfaların, platforma göre farklılık gösteren varsayılan geçişleri vardır: iOS style ya da Material style.
Tüm geçişler, biri görüntülenmekte olan ve biri görüntüden çıkmakta olan iki sayfa içerir.

Geçişler PageRoute tarafından veya PageRoute'u genişleten MaterialPageRoute, ModalRoute'u ve TransitionRoute'u genişleten MaterialPageRoute tarafından işlenir. Bu karmaşanın bir yerinde, diğer şeylerin yanı sıra argüman olarak iki animasyon alan buildTransitions adlı bir method vardır. Birisi çıkarken kendisi içindir, ikincisi ise onun yerini alan rota ile koordinelidir. MaterialPageRoute zaten geçişleri uygular, bu da MaterialPageRoute.buildTransitions'ı override edebileceğiniz anlamına gelir.

Bu, animasyonlu bir widget'a sarılmış sayfayı basitçe döndürebileceğiniz ve sayfanın buna göre canlandıracağı anlamına gelir.

class FadeInSlideOutRoute<T> extends
    MaterialPageRoute<T> {
  FadeInSlideOutRoute({WidgetBuilder builder, RouteSettings settings})
      : super(builder: builder, settings: settings);

  @override
  Widget buildTransitions(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    if (settings.isInitialRoute) return child;
    if (animation.status == AnimationStatus.reverse) {
      return super.buildTransitions(
        context,
        animation,
        secondaryAnimation,
        child,
      );
    }
    return FadeTransition(
        opacity: animation,
        child: child,
    );
  }
}


Enter fullscreen mode Exit fullscreen mode

Özet:

• Flutter, yönlendirmeyi çok daha esnek ve akıcı hale getiren dinamik yönlendirme (routing) kullanır.

• Flutter's Navigator, tıpkı bazı kullanıcı etkileşimleri gerçekleşirken veya uygulama yeni veriler alırken, kodunuzda "anında" rotalar oluşturmanıza olanak tanır.

• Navigator, bazı kullanıcı etkileşimleri gerçekleşirken veya uygulama yeni veriler alırken, kodunuzda "anında" (on the fly) rotalar oluşturmanıza olanak tanır.

• Flutter, adlandırılmış route'ları kullanarak statik yönlendirmeyi destekler.

• Navigator, tüm rotaları yığın(stack) şeklinde yönetir.

• Rotalara yönlendirme işi, Navigator.push ve Navigator.pop methodları çağrılarak yapılır.

Navigator.push çağrıları, yeni rota tarafından geri aktarılacak bir değeri bekleyen bir Future döndürür.

• Flutter'da Material-style menu drawer'ı oluşturmak birkaç widget gerektirir: Drawer, ListView, ListTile, AboutListTile ve DrawerHeader.

• Bir RouteObserver kurarak ve herhangi bir widget'ın state nesnesine abone olarak yönlendirmedeki değişiklikleri tahmin edebilirsiniz.

• Birkaç UI öğesi Navigator ile yönetilir ve teknik olarak rotalardır, ancak bunlar snackbarlar, alt sayfalar, çekmeceler ve menüler gibi sayfalar değildir.

• GestureDetector widget'ını kullanarak kullanıcı etkileşimlerini dinleyebilirsiniz.

• Özel sayfa geçişlerini uygulamak, Route sınıflarını extend ederek yapılır.

Resources:
•Flutter in Action Chapter: 7
https://api.flutter.dev/flutter/widgets/Navigator/pushReplacementNamed.html
https://api.flutter.dev/flutter/widgets/RouteObserver-class.html

Top comments (0)