티스토리 뷰

Flutter 애플리케이션을 개발하면서 화면 간 데이터를 전달해야 하는 경우가 자주 있습니다. 이번 블로그에서는 Flutter의 Navigatorarguments를 활용해 데이터를 전달하고 관리하는 방법을 심도 있게 알아보겠습니다.

참고. Pass arguments to a named route

Flutter에서 Navigator와 arguments란?

Flutter의 Navigator는 화면 간 전환을 관리하는 핵심 도구입니다. Navigator는 스택(stack) 구조를 사용해 화면을 추가하거나 제거하며, 이를 통해 간단하고 직관적인 화면 전환을 제공합니다.

arguments는 화면 전환 시 데이터를 전달할 수 있는 속성으로, 이를 활용하면 화면 간 데이터를 쉽게 공유할 수 있습니다.

Flutter에서 데이터를 전달하는 단계

1. 전달할 데이터 정의

전달하고자 하는 데이터를 담는 클래스를 생성합니다. 이 클래스는 전달할 데이터의 구조를 정의합니다.

class ScreenArguments {
  final String title;
  final String message;

  ScreenArguments(this.title, this.message);
}

2. 데이터를 전달받는 화면 구성

데이터를 전달받아 화면에 표시할 위젯을 작성합니다. ModalRoute.of(context)!를 사용해 전달된 데이터를 추출합니다.

class ExtractArgumentsScreen extends StatelessWidget {
  static const routeName = '/extractArguments';

  @override
  Widget build(BuildContext context) {
    final args = ModalRoute.of(context)!.settings.arguments as ScreenArguments;

    return Scaffold(
      appBar: AppBar(title: Text(args.title)),
      body: Center(child: Text(args.message)),
    );
  }
}

3. routes에 화면 등록

MaterialApp 위젯의 routes 속성에 화면을 등록합니다.

MaterialApp(
  routes: {
    ExtractArgumentsScreen.routeName: (context) => ExtractArgumentsScreen(),
  },
);

4. 데이터와 함께 화면 전환

화면 전환 시 데이터를 Navigator.pushNamed()arguments 속성을 통해 전달합니다.

Navigator.pushNamed(
  context,
  ExtractArgumentsScreen.routeName,
  arguments: ScreenArguments(
    'Hello Flutter!',
    'This message is passed as arguments.',
  ),
);

onGenerateRoute로 동적 경로 처리하기

Flutter에서는 onGenerateRoute를 사용해 더 유연한 경로 설정이 가능합니다. 이를 활용하면 동적으로 데이터를 처리하거나 조건에 따라 화면을 렌더링할 수 있습니다.

MaterialApp(
  onGenerateRoute: (settings) {
    if (settings.name == ExtractArgumentsScreen.routeName) {
      final args = settings.arguments as ScreenArguments;

      return MaterialPageRoute(
        builder: (context) => ExtractArgumentsScreen(
          title: args.title,
          message: args.message,
        ),
      );
    }
    return null;
  },
);

Named Routes와 arguments의 장점

  • 가독성: 명명된 경로를 사용하면 화면 전환을 보다 명확히 이해할 수 있습니다.
  • 유지보수: 경로 이름을 중앙에서 관리하므로 코드 변경이 간단합니다.
  • 유연성: 동적으로 데이터를 전달하거나 경로를 설정할 수 있습니다.

데이터 전달 시 주의점

  1. 데이터 타입 확인
    • 전달된 데이터의 타입이 맞는지 확인해야 런타임 에러를 방지할 수 있습니다.
  2. 예외 처리
    • 데이터를 받지 못했을 때를 대비해 기본값을 설정하거나 에러를 처리하는 코드를 작성하세요.
  3. 데이터 구조 관리
    • 데이터 구조를 명확히 정의하고, 필요할 경우 JSON 또는 Map 형태로 데이터를 처리합니다.

Flutter의 데이터 전달 활용 사례

사례 1: 로그인 상태 전달

사용자가 로그인한 정보를 다음 화면으로 전달하여 개인화된 UI를 구성할 수 있습니다.

사례 2: 검색 조건 전달

검색 화면에서 설정한 필터 조건을 결과 화면으로 전달해 검색 결과를 필터링합니다.

사례 3: 다단계 폼 데이터 전달

다단계 폼에서 각 단계의 데이터를 다음 화면으로 전달해 최종적으로 데이터를 종합합니다.


결론

Flutter에서 Navigator와 arguments를 활용하면 화면 간 데이터 전달을 쉽게 처리할 수 있습니다. 이를 통해 애플리케이션의 유지보수성과 확장성을 높일 수 있으며, 복잡한 네비게이션 로직도 효율적으로 관리할 수 있습니다. 위에서 소개한 가이드를 참고해 Flutter 앱 개발에 적용해 보세요!

 

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      routes: {
        ExtractArgumentsScreen.routeName: (context) =>
            const ExtractArgumentsScreen(),
      },
      // Provide a function to handle named routes.
      // Use this function to identify the named
      // route being pushed, and create the correct
      // Screen.
      onGenerateRoute: (settings) {
        // If you push the PassArguments route
        if (settings.name == PassArgumentsScreen.routeName) {
          // Cast the arguments to the correct
          // type: ScreenArguments.
          final args = settings.arguments as ScreenArguments;

          // Then, extract the required data from
          // the arguments and pass the data to the
          // correct screen.
          return MaterialPageRoute(
            builder: (context) {
              return PassArgumentsScreen(
                title: args.title,
                message: args.message,
              );
            },
          );
        }
        // The code only supports
        // PassArgumentsScreen.routeName right now.
        // Other values need to be implemented if we
        // add them. The assertion here will help remind
        // us of that higher up in the call stack, since
        // this assertion would otherwise fire somewhere
        // in the framework.
        assert(false, 'Need to implement ${settings.name}');
        return null;
      },
      title: 'Navigation with Arguments',
      home: const HomeScreen(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home Screen'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // A button that navigates to a named route.
            // The named route extracts the arguments
            // by itself.
            ElevatedButton(
              onPressed: () {
                // When the user taps the button,
                // navigate to a named route and
                // provide the arguments as an optional
                // parameter.
                Navigator.pushNamed(
                  context,
                  ExtractArgumentsScreen.routeName,
                  arguments: ScreenArguments(
                    'Extract Arguments Screen',
                    'This message is extracted in the build method.',
                  ),
                );
              },
              child: const Text('Navigate to screen that extracts arguments'),
            ),
            // A button that navigates to a named route.
            // For this route, extract the arguments in
            // the onGenerateRoute function and pass them
            // to the screen.
            ElevatedButton(
              onPressed: () {
                // When the user taps the button, navigate
                // to a named route and provide the arguments
                // as an optional parameter.
                Navigator.pushNamed(
                  context,
                  PassArgumentsScreen.routeName,
                  arguments: ScreenArguments(
                    'Accept Arguments Screen',
                    'This message is extracted in the onGenerateRoute '
                        'function.',
                  ),
                );
              },
              child: const Text('Navigate to a named that accepts arguments'),
            ),
          ],
        ),
      ),
    );
  }
}

// A Widget that extracts the necessary arguments from
// the ModalRoute.
class ExtractArgumentsScreen extends StatelessWidget {
  const ExtractArgumentsScreen({super.key});

  static const routeName = '/extractArguments';

  @override
  Widget build(BuildContext context) {
    // Extract the arguments from the current ModalRoute
    // settings and cast them as ScreenArguments.
    final args = ModalRoute.of(context)!.settings.arguments as ScreenArguments;

    return Scaffold(
      appBar: AppBar(
        title: Text(args.title),
      ),
      body: Center(
        child: Text(args.message),
      ),
    );
  }
}

// A Widget that accepts the necessary arguments via the
// constructor.
class PassArgumentsScreen extends StatelessWidget {
  static const routeName = '/passArguments';

  final String title;
  final String message;

  // This Widget accepts the arguments as constructor
  // parameters. It does not extract the arguments from
  // the ModalRoute.
  //
  // The arguments are extracted by the onGenerateRoute
  // function provided to the MaterialApp widget.
  const PassArgumentsScreen({
    super.key,
    required this.title,
    required this.message,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Text(message),
      ),
    );
  }
}

// You can pass any object to the arguments parameter.
// In this example, create a class that contains both
// a customizable title and message.
class ScreenArguments {
  final String title;
  final String message;

  ScreenArguments(this.title, this.message);
}