기본 콘텐츠로 건너뛰기

[Flutter] 플러터 새로운 화면(route)으로 데이터 보내고 받기

플러터에서 화면(screen)/라우트(route) 간에 데이터를 보내고, 받는 방법을 설명한다. 전달하는 방식에 따라 직접 전달, 간접 전달 방식이 있다. 전달하는 데이터 타입(고정, 가변)에 따라 전달 방식이 구분되기도 한다. 안드로이드 네이티브 코드의 인텐트(Intent), iOS 네이티브 코드의 세그(Segue)의 개념을 플러터에서는 어떻게 다루는지 살펴보자.

statistics_icon_official

1. 개요

이전 포스팅에서 페이지(route) 간 화면 이동에 대해서 설명하였다. (참고) 오늘은 페이지 간 화면 이동 시 데이터(값)을 전달 하는 개념에 대해서 설명하고자 한다.

참고로 안드로이드 네이티브에서는 이 개념을 인텐트(Intent), iOS 네이티브의 세그(Segue)의 개념으로 사용하고 있다.

대부분의 앱에서는 화면 이동의 이벤트를 처리하는 프로시저/함수가 있다고 이전 포스팅에서 설명을 했다. (플러터에서는 Navigator.push()와 Navigator.pop()이 역할을 한다.) 이 과정에서 화면 간에 필요한 파라미터 값을 전달하고 전달 받는 경우가 대부분의 케이스에서 발생한다. 다시 말해, 액티비티와 인텐트가 별개의 동작 및 구성요소가 아니며, 이는 플러터에서도 예외가 아닐 것으로 본다. 참고로 아래 그림에서 value로 표현한 개념이 안드로이드 네이티브의 인텐트와, iOS 네이티브의 세그의 개념과 동일하다고 보면 된다.

Deliver value between screens overview
Deliver value between screens overview

2. 전달하는 데이터 유형

2.1. 고정 값 전달 예시

위의 프로시저/함수에서 정해진 값 자체를 함수 내에 전달하는 방식이 있을 수 있다. 예를 들어, 아래의 코드가 있을 수 있다. 아래 예에서는 Class FirstRoute에서 버튼이 클릭되면 페이지 이동과 동시에 ‘SecondRoute_Delivered’라는 고정된 스트링 값을 전달하고, Class SecondRoute에서 전달 받은 스트링 값을 appBar의 타이틀(title)값으로 사용하는 예제를 볼 수 있다. 이런 고정 값을 전달 하는 경우는 정해진 메시지를 대화상자(Dialog)로 출력하는 경우 등에서 주로 사용될 수 있을 것이다. 주로 시스템 메시지의 출력에 사용되는 경우가 해당될 것이다.

import 'package:flutter/material.Dart';

void main() {
  runApp(MaterialApp(
    title: 'Flutter',
    home: FirstRoute(),
  ));
}

class FirstRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Route Page'),
      ),
      body: Center(
        child: RaisedButton(
          child: Text('Go to the Second Route'),
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => SecondRoute(appbarTitle:'SecondRoute_Delivered')), //버튼이 눌리는 이벤트 발생 시, 다음 페이지에서 전달 받을 string 변수와 value('SecondRoute_Delivered')값을 직접 전달
            );
          },
        ),
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {

  final String appbarTitle;//FirstRoute에서 전달 받은 변수를 사용하기 위해 변수 선언

  SecondRoute({Key key, @required this.appbarTitle}) : super(key: key);//변수 선언 후 초기화 및 입력

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(appbarTitle),//입력받은 값을 사용하여 AppBar 타이틀 값에 적용
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () {

            Navigator.pop(context);

          },
          child: Text('Go back!'),
        ),
      ),
      backgroundColor: Colors.orange,
    );
  }
}

1.2. 선택된 고정값 전달 예시

TodosScreen route의 ListView에서 선택된 아이템의 값을 DetailScreen route로 전달하는 예시이다. 값이 선택되는 것에 따라서 전달되는 값은 변경될 수 있다. 아래의 예시에서는 변수로 List(배열)이 정의되었고, 이 값들이 시스템에 의해서 자동으로 생성된다. (20개의 title, description 데이터 쌍이 배열로 생성되는 구조)

이후 해당 List 값은 TodosScreen 클래스에서 todos로 상속을 받아 ListView로 출력이 된다. ListView에 출력된 값을 선택하면 선택된 값(선택된 인덱스의 title,description 데이터 쌍)이 TodosScreen route에서 이동하게 될 DetailScreen route으로 전달된다.

전달 받을 DetailScreen route에서는 전달 받은 값을 초기화하고, 클래스 내에서 사용할 값으로 할당하고(key:key 코드부), 이후 정보에 사용한다.

이 외에 메인함수/void main()에서 List 값을 반복하여 생성하는 것과, TodosScreen route와 같은 출력화면에서의 처리구조와 관련해서는 변수 관련 포스팅의 List 파트에서 별도로 다루도록 하겠다.

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

class Todo {
  final String title;
  final String description;

  Todo(this.title, this.description);
}

void main() {
  runApp(MaterialApp(
    title: 'Passing Data',
    home: TodosScreen(
      todos: List.generate(
        20, //20개의 리스트를 만든다는 의미
        //이때, Todo 객체를 title description 배열로 20개를 순환하여 만든다는 것을 의미
            (i) => Todo(
          'Todo $i', //Todo 클래스에서 정의한 것처럼 첫번째 정의된 값은 title로 정의
          'A description of what needs to be done for Todo $i', //Todo 클래스에서 정의한 것처럼 두번째 정의된 값은 description으로 정의
        ),
      ),
    ),
  ));
}

class TodosScreen extends StatelessWidget {
  final List<Todo> todos;

  TodosScreen({Key key, @required this.todos}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Todos'),
      ),
      body: ListView.builder(
        itemCount: todos.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(todos[index].title),
            // 사용자가 ListTile을 선택하면, DetailScreen으로 이동
            // DetailScreen을 생성할 뿐만 아니라, 현재 todo를 같이 전달해야 한다는 것을 명심할 것
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  // DeatilScreen route에서 전달받을 list 변수를 todo를 정의하고, TodosScreen 클래스에서 정의된 todos 객체의 객체의 인덱스 값을 전달값으로 정의 
                  // todos는 Todo 클래스로부터 상속 받은 것임을 명심할 것
                builder: (context) => DetailScreen(todo: todos[index]),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

class DetailScreen extends StatelessWidget {
  // TodosScreen에서 전달한 todo를 들고 있을 필드 선언
  // TodosScreen의 todo 변수는 Todo클래스로 상속받은 todos 객체값을 할당된 것이기 때문에 Todo클래스로 선언함에 유의
  final Todo todo;

  // 생성자를 통해 전달받은 변수 todo 변수 값을 세팅 (TodosScreen에서 선택된 인덱스의 배열값이 todo변수 값이 세팅되는 것을 의미함. 선택된 아이템의 title description값이 배열로 전달)
  DetailScreen({Key key, @required this.todo}) : super(key: key); 

  @override
  Widget build(BuildContext context) {
    // UI를 그리기 위해 Todo를 사용합니다.
    return Scaffold(
      appBar: AppBar(
        title: Text(todo.title), //선택된 아이템의 title값을 AppBar 타이틀에 출력)
      ),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Text(todo.description), //선택된 아이템의 desctiption값을 body text로 출력)
      ),
    );
  }
}

1.3. 사용자 입력값 전달 예시

해당 예제는 사용자가 입력한 입력값을 다른 route로 전달하는 예시이다. 사용자가 입력하는 입력값은 임의값인 textfield에 입력한 값이 있을 수도 있으며, 출력된 대화상자(Dialog)에서 선택한 선택값을 원래 화면으로 돌려보내는 것이 될 수도 있다.

이 경우 다시 동작 시나리오에 따라서 다른 코드 설계가 필요한데, 단순히 현재 route에서 다음 route로 데이터를 전달하는 구조와 현재 화면에서 대화상자(Dialog)가 출력된 상태에서 사용자가 대화상자 내 값을 선택(예: 여러 개의 라디오 버튼이 출력된 대화상자에서 특정 값을 선택하는 상황)하면 현재 화면의 값을 변경하는 케이스는 다르게 처리되어야 한다.

1.3.1. 서로 다른 route로 값 전달하기

아래와 같이 서로 다른 route로 값을 전달하는 방식은 아래와 같은 구조로 설계가 된다. (아래 코드 내 주석항목 참조)

이 경우 위의 케이스와 가장 다른 점은 클래스가 StatelessWidget이 아니라 StatefulWidget 타입으로 정의된다는 점이다.

1번. 클래스를 생성하기 위한 클래스를 정의하는 코드부

2번. 상속받은 클래스에서 text 컨트롤을 위한 TextEditingController 선언과 TextField 컨트롤러로 TextEditingController 매핑부

3번. 전달할 route에서 전달받을 route로 전달하기 위한 변수 정의와 이동을 위한 별도 함수 정의부

4번. 전달받을 route에서 변수 정의와 key 매핑 및 값 출력부

* 참고로 Dart 언어에서는 식별자 이름 앞에 _(underbar)를 붙이면 Private으로 자동적용된다.

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    title: 'Flutter',
    home: FirstScreen(),
  ));
}

//1번-FirstScreen 클래스의 상태를 생성하는 _FirstScreenState를 정의하고, 리턴값으로 반환 (매우 중요★★)
class FirstScreen extends StatefulWidget {
  @override
  _FirstScreenState createState() {
    return _FirstScreenState();
  }
}

//2번-FirstScreenState 클래스를 FirstScreen으로부터 상속처리 (매우 중요★★)
class _FirstScreenState extends State<FirstScreen> {

  // TextEditingController 타입의 textFieldController를 선언하여 textfield의 텍스트를 제어할 수 있도록 처리 (매우 중요★★)
  TextEditingController textFieldController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('First screen')),
      //body 영역을 Column 위젯을 지정하여 TextField와 RaisedButton을 구성
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [

          Padding(
            padding: const EdgeInsets.all(32.0),
            //첫번째 Column의 위젯으로 TextField를 정의
            child: TextField(
              //컨트롤러로 textFieldController를 매핑 (매우 중요★★)
              controller: textFieldController,
              style: TextStyle(
                fontSize: 24,
                color: Colors.black,
              ),
            ),
          ),

          RaisedButton(
            child: Text(
              'Go to second screen',
              style: TextStyle(fontSize: 24),
            ),
            // onPressed 이벤트 발생 시, TextField의 데이터 전달 처리를 위한 별도의 함수 _sendDataToSecondScreen() 지정 (매우 중요★★)
            onPressed: () {
              _sendDataToSecondScreen(context);
            },
          )

        ],
      ),
    );
  }

  //3번-위의 RaisedButton의 onPressed 이벤트 처리를 위한 함수 정의부 (매우 중요★★)
  void _sendDataToSecondScreen(BuildContext context) {
    //별도의 변수로 textToSend를 정의하고, textFieldController.text를 통해 값 전달
    String textToSend = textFieldController.text;
    //Navigator.push 함수는 여기서 실행이 되며, 이동할 route인 SecondScreen을 정의하고, 전달할 변수 textToSend를 지정
    Navigator.push(
        context,
        MaterialPageRoute(
          builder: (context) => SecondScreen(text: textToSend,),
        ));
  }
}

class SecondScreen extends StatelessWidget {
  //4번-전달 받을 textToSend를 받기 위한 변수 text를 정의
  final String text;

  //키값으로 text와 textToSend값을 매핑처리
  SecondScreen({Key key, @required this.text}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Second screen')),
      body: Center(
        //해당 클래스에서 정의한 text 변수 값을 body 부분에 출력하도록 Text 위젯의 값으로 text 변수를 지정
        child: Text(
          text,
          style: TextStyle(fontSize: 24),
        ),
      ),
    );
  }
}

1.3.2. 현재 route에서 사용자 입력 대기 후, 입력된 값을 현재 route에 갱신하기

다른 화면(route)으로부터 입력받은 값을 전달받아 현재 화면(route)에 값을 갱신하는 방식을 비동기 데이터 처리라고 한다. 비동기 데이터 처리에 대한 부분은 플러터에서 제공하는 방식 외에 Dart 언어에서 처리하는 Stream 방식이 있을 수 있다.

1.3.2.1. 플러터 자체처리 방식

1번. 클래스를 생성하기 위한 클래스를 정의하는 코드부

2번. 위의 케이스와 달리 TextEditingController 선언하지 않음. (중요)

3번. 전달받을 route에서 값을 넘겨받기 위한 함수 처리 async, await, setState 처리 구조. 넘겨 받은 데이터를 기존 변수에 Override 처리하기 위한 별도 함수 정의부

4번. 클래스를 생성하기 위한 클래스를 정의하는 코드부 (값을 넘겨주기 위한 route에서 위 예제의 1번이 진행되는 구조)

5번. 상속받은 클래스에서 text 컨트롤을 위한 TextEditingController 선언과 TextField 컨트롤러로 TextEditingController 매핑부 (값을 넘겨주기 위한 route에서 위 예제의 2번이 진행되는 구조)

6번. 전달할 route에서 전달받을 route로 전달하기 위한 변수 정의와 이동을 위한 별도 함수 정의부 (값을 넘겨주기 위한 route에서 위 예제의 3번이 진행되는 구조)

* 위의 케이스에서와 달리 이전 route에서 변수를 전달 받아서 단순히 출력하는 행위를 하는 과정이 없기 때문에 key값을 통한 변수 값 전달하는 부분이 없다. 반대로 전달을 받을 route에서 이미 변수로 지정된 값을 할당하고, 전달할 route에서 값이 넘어오기를 대기하기 때문에 key값을 통한 변수 값 전달이 아니라 이미 선언한 변수에 직접 할당하는 개념만 있는 케이스에 해당된다.

* 다시 말해, 단순히 값을 전달 받아 출력을 할 때는 key값을 통한 변수 처리를 하지만 전달받을 값을 대기하고 있다가 업데이트를 하는 경우에는 key값을 통한 처리가 아니라 직접 변수에 매핑하는 과정으로 처리가 된다는 것을 의미한다.

void main() {
  runApp(MaterialApp(
    title: 'Flutter',
    home: FirstScreen(),
  ));
}

//1번-FirstScreen 클래스의 상태를 생성하는 _FirstScreenState를 정의하고, 리턴값으로 반환 (매우 중요★★)
class FirstScreen extends StatefulWidget {
  @override
  _FirstScreenState createState() {
    return _FirstScreenState();
  }
}

//2번-_FirstScreenState 클래스를 FirstScreen으로부터 상속처리 (매우 중요★★)
class _FirstScreenState extends State<FirstScreen> {

  //위의 예제와 달리   TextEditingController 타입의 textFieldController를 선언하여 textfield의 텍스트 제어를 여기서 하지 않음. 단순히 text 변수에 'Text'를 값으로 할당 (매우 중요)
  String text = 'Text';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('First screen')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [

            Padding(
              padding: const EdgeInsets.all(32.0),
              child: Text(
                //Text 위젯에 text 변수를 할당함. 첫번째 화면에서 text 변수의 'Text'값이 할당된 채로 출력됨. (중요)
                text,
                style: TextStyle(fontSize: 24),
              ),
            ),

            RaisedButton(
              child: Text(
                'Go to second screen',
                style: TextStyle(fontSize: 24),
              ),
              // onPressed 이벤트 발생 시, TextField의 데이터 전달 처리를 위한 별도의 함수 _awaitReturnValueFromSecondScreen() 지정 (매우 중요★★)
              onPressed: () {
                _awaitReturnValueFromSecondScreen(context);
              },
            )

          ],
        ),
      ),
    );
  }
  //3번-_awaitReturnValueFromSecondScreen 함수 선언 async 타입으로 동기화가 가능한 형태로 선언 (매우 중요★★)
  void _awaitReturnValueFromSecondScreen(BuildContext context) async {

    // result 변수를 선언하고, Navigator.push()의 결과값을 result에 할당. 동기화가 가능하도록 await 타입으로 Navigator.push() 함수를 선언 (매우 중요)
    final result = await Navigator.push(
        context,
        // 위의 케이스와 달리 전달 값이 없이 이동할 route인 SecondScreen만 정의하고, 전달값을 넘겨주지 않음. (매우 중요)
        MaterialPageRoute(
          builder: (context) => SecondScreen(),
        ));

    // 위의 SecondScreen route으로 부터 입력 받은 전달 값을 setState() 함수를 통해서 text에 result값을 덮어쓰기 처리 (매우 중요★★)
    setState(() {
      text = result;
    });
  }
}

class SecondScreen extends StatefulWidget {
  @override
  //4번-생성자를 통해서 SecondScreen을 상속받는 _SecondScreenState를 생성 처리 (매우 중요★★) 
  _SecondScreenState createState() {
    return _SecondScreenState();
  }
}

//5번-상속받은 _SecondScreenState 클래스에서 textFieldController를 선언하여 해당 클래스 내의 위젯 내 text값을 제어처리 (매우 중요★★)
class _SecondScreenState extends State<SecondScreen> {
  // this allows us to access the TextField text
  TextEditingController textFieldController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Second screen')),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [

          Padding(
            padding: const EdgeInsets.all(32.0),
            child: TextField(
              // TextField의 Controller를 선언한 textFieldController로 정의 (매우 중요★★)
              controller: textFieldController,
              style: TextStyle(
                fontSize: 24,
                color: Colors.black,
              ),
            ),
          ),

          RaisedButton(
            child: Text(
              'Send text back',
              style: TextStyle(fontSize: 24),
            ),
            //onPressed 처리를 위한 별도 함수 _sendDataBack 호출 (매우 중요★★)
            onPressed: () {
              _sendDataBack(context);
            },
          )

        ],
      ),
    );
  }

  //6번-해당 route를 호출한 route인 FirstScreen으로 값을 반환하고, 이동처리 Navigator.pop하기 위한 함수 정의
  void _sendDataBack(BuildContext context) {
    //반환하기 위한 변수 textToSendBack을 정의하고, 위의 TextField의 Controller로 정의된 텍스트 값 .text를 textToSendBack에 매핑 (매우 중요)
    String textToSendBack = textFieldController.text;
    //Navigator.pop 함수에 textToSendBack 변수 전달 처리 (매우 중요)
    Navigator.pop(context, textToSendBack);
  }
}

1.3.2.2. Dart Stream 처리 방식

Dart의 스트림(Strema)을 통한 비동기 데이터 처리 방식에 해당 내용에 대해서는 향후 별도 포스팅을 통해서 다루며, 해당 포스팅에서 추가로 다루지는 않는 것으로 한다. 참고로 해당 방식을 간단히 정리하면 데이터 관리를 위해서는 스트림, 스트림 컨트롤러, 스트림 빌더 등의 요소들을 활용해서 구현하는 방식이다. 해당 방식은 Reactive 프로그램밍 방식의 코드 구현방식이며, 스트림 방식으로 코드 구현이 되면 UI와 데이터 관련 코드가 섞여서 복잡도가 증가하게 되는데, 이때 Bloc 방식으로 설계하면 UI 코드와 데이터 관련 코드를 분리하여 설계가 가능해진다.

참조: 원문1원문2

3. 참고사항

플러터에는 안드로이드 네이티브 코드에서처럼 액티비티 간 값 전달을 하기 위한 개념인 인텐트의 개념은 없으며, 서로 다른 앱(예: 네이티브와 플러터 코드)에서 값 전달을 위해서는 별도의 설계가 필요하다. (별도 설계를 위한 방식은 아래 원문 참조)

참조: 원문

댓글

인기 게시글

[오류해결] KMS 인증(Activation) 오류(error) 0xC004F017 문제 원인 및 해결 방법

[오류해결] 카카오톡 PC 버전 접속 오류(일시적인 장애이거나 네트워크 문제일 수 있습니다. 잠시 후 다시 이용해 주세요. 오류코드 70101, 11002, LL)와 다음(daum.net), 티스토리(tistory.com) 접속 오류(오류코드 DNS_PROBE_FINISHED_NXDOMAIN) 문제