In Software development, navigation is one of the most crucial parts of a user experience. So when I decided to switch my navigation from GetX to GoRouter, I thought it would be a simple swap—until I realized that navigation was directly referenced in every single screen of my app. What could have been a smooth transition turned into a challenge, and I quickly found myself wishing I had abstracted my navigation logic from the very beginning.
This experience taught me a valuable lesson about the power of abstraction in Flutter apps. In this article, I’ll walk you through why abstracting navigation functionalities early on is essential and how it could save you from hours of code refactoring in the future. Let's dive in!
The Problem: Tight Coupling with GetX Navigation
When I first built my app, I used GetX for navigation. It worked well, but all my navigation calls were scattered across individual screens. At the time, it felt natural to do Get.to()
directly whenever I needed to move between screens. However, when I decided to transition to GoRouter for its powerful deep-linking capabilities, I realized this choice came at a cost: I had to replace Get.to()
calls in every file where navigation happened.
If I had taken the time to abstract my navigation logic into a single service, swapping routing libraries would have been far simpler. But why, exactly, is abstraction so beneficial, and how can we make it work for our navigation needs?
Understanding the Power of Abstraction
Abstraction, in simple terms, is the process of separating implementation details from the main logic. In the case of navigation, it means centralizing routing logic in one place, separate from individual screens. This approach allows us to modify or swap the routing library without touching every screen component directly.
Let’s look at the key benefits:
Maintainability: With navigation logic centralized, we only need to make updates in one place, keeping our code more organized and manageable.
Flexibility: When it comes to swapping routing solutions, abstraction allows for seamless transitions by updating just the navigation service.
Separation of Concerns: Screens stay focused on UI logic, and routing is handled in one centralized service, creating a cleaner codebase.
Implementing a Navigation Service in Flutter
So, how can we apply this in a Flutter app? Let’s start by creating a simple NavigationService
that abstracts our routing logic.
Step 1: Define the Abstract Class
Define an abstract class, NavigationService
, with essential navigation methods like navigateTo
, goBack
, and replaceRoute
.
abstract class NavigationService {
void navigateTo(String route, {Map<String, String>? params});
void goBack();
void replaceRoute(String route, {Map<String, String>? params});
}
This abstract class will serve as the blueprint, and each router-specific implementation will need to follow it.
Step 2: Implement the NavigationService for Each Router
Here’s an implementation of NavigationService
using GoRouter.
GoRouter Implementation
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class GoRouterNavigationService implements NavigationService {
final GoRouter _router;
GoRouterNavigationService(this._router);
@override
void navigateTo(String route, {Map<String, String>? params}) {
_router.go(route, extra: params);
}
@override
void goBack() {
_router.pop();
}
@override
void replaceRoute(String route, {Map<String, String>? params}) {
_router.pushReplacement(route, extra: params);
}
}
GetX Implementation
import 'package:get/get.dart';
class GetXNavigationService implements NavigationService {
@override
void navigateTo(String route, {Map<String, String>? params}) {
Get.toNamed(route, arguments: params);
}
@override
void goBack() {
Get.back();
}
@override
void replaceRoute(String route, {Map<String, String>? params}) {
Get.offNamed(route, arguments: params);
}
}
Step 3: Set Up Dependency Injection
Now, we introduce Dependency Injection (DI) using the get_it
package to decide which navigation service to use. This will allow us to swap between different router libraries (like GoRouter or GetX) without changing the rest of the app’s code.
To get started, first add get_it to your pubspec.yaml
:
dependencies:
get_it: ^7.2.0
Then, set up DI in your app initialization:
import 'package:get_it/get_it.dart';
final getIt = GetIt.instance;
void setupNavigationService() {
// if you want to use GoRouter
getIt.registerSingleton<NavigationService>(GoRouterNavigationService());
// if you want to use GetRouter
getIt.registerSingleton<NavigationService>(GetXNavigationService());
}
Step 4: Initialize DI in main.dart
In your main.dart
, call the dependency injection (get_it
) initialization function:
void main() {
setupNavigationService();
runApp(MyApp());
}
Step 5: Using the NavigationService in Your App
Now, when you need to navigate, you simply retrieve the NavigationService
from get_it
:
import 'package:get_it/get_it.dart';
class SomeScreen extends StatelessWidget {
final NavigationService _navigationService = GetIt.instance<NavigationService>();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
_navigationService.navigateTo('/someRoute');
},
child: Text('Navigate'),
),
),
);
}
}
This way, you don’t have to worry about the routing logic inside your individual screens. Instead, you rely on the abstract NavigationService
that points to whichever routing library you're using at the moment.
The Benefits in Action: A Cleaner, More Scalable Codebase
By using DI, I can easily switch between GoRouter and GetX without touching the screens. This keeps the app modular, flexible, and easy to maintain. Want to change the navigation library in the future? Just update the DI configuration.
Conclusion: The Value of Early Abstraction
If I had abstracted navigation from the beginning, I could have easily swapped between routing libraries without major changes. This lesson taught me the importance of abstraction not only for navigation but across all layers of an app. It keeps the code flexible, testable, and maintainable. So, if you’re starting a Flutter app, consider abstracting navigation—and other core features—from the start to save time in the long run.
Now, I’d love to hear from you! Which part of your app’s business logic are you planning to abstract next? Or, if you’ve already implemented abstraction elsewhere in your app, how did you go about it? Drop a comment below and share your experience—I’d love to learn from your approach!
Top comments (0)