Flutter - 다른 Stateful 위젯의 함수 사용하기

3 분 소요


관련 스택오버플로우


자식 위젯이 부모 위젯의 함수 call

// 부모 위젯에서 위젯 생성 시 함수 넘겨 주기
SomeSfW(foo: foo));

// 자식 위젯에서 함수 사용
widget.foo();

함수를 그냥 그대로 넘겨주면 된다. 쉬우니까 패스


부모 위젯이 자식 위젯의 함수 call

부모 위젯이 자식 위젯의 함수를 부르는 것은 기본적으로 권장되지는 않는다고 합니다.

방법 1(X)

class SomeSfW extends StatefulWidget {

  final SomeSfWState _someSfWState = SomeSfWState();

  SomeSfW({
    Key? key,
  }) : super(key: key);

  @override
  SomeSfWState createState() => _someSfWState;

  foo() => SomeSfWState.foo();

}

class SomeSfWState extends State<SomeSfW> {

  void foo(){
    /*
    some jobs...
    */
  }

  @override
  Widget build(BuildContext context) {
    return Text(" !! ");
  }

}

처음엔 이 방식으로 어느 정도는 잘 쓰다가, 문제가 생겼다. 예를 들어 List<SomeSfW> list에 동적으로 SomeSfW를 만들거나 지우는 등의 처리를 하자 에러가 생겼다.

에러

state._element == null
is not true

assertion failed로 위와 같은 경고가 나왔다.

(flutter 설치 경로)/packages/flutter/lib/src/widgets/framework.dart에서 문제가 생겼다 하여 찾아 봤는데,

/// An [Element] that uses a [StatefulWidget] as its configuration.
class StatefulElement extends ComponentElement {
  /// Creates an element that uses the given widget as its configuration.
  StatefulElement(StatefulWidget widget)
      : _state = widget.createState(),
        super(widget) {
    assert(() {
      if (!state._debugTypesAreRight(widget)) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('StatefulWidget.createState must return a subtype of State<${widget.runtimeType}>'),
          ErrorDescription(
            'The createState function for ${widget.runtimeType} returned a state '
            'of type ${state.runtimeType}, which is not a subtype of '
            'State<${widget.runtimeType}>, violating the contract for createState.',
          ),
        ]);
      }
      return true;
    }());
    assert(state._element == null);
    state._element = this;
    assert(
      state._widget == null,
      'The createState function for $widget returned an old or invalid state '
      'instance: ${state._widget}, which is not null, violating the contract '
      'for createState.',
    );
    state._widget = widget;
    assert(state._debugLifecycleState == _StateLifecycle.created);
  }

여기서 assert(state._element == null); 전에 state._element를 출력해 보니 null이 아닌 state를 가진 상태의 위젯이 나오더라.

즉 위 방식처럼 위젯을 만들 때 final로 스테이트를 만들어 그걸 반환하는 식으로 하면,
스테이트를 동적으로 생성하거나 삭제하고 조작할 경우, 그 스테이트는 실제 플러터의 트리에 속하는 스테이트와는 다른 녀석일 수 있어 오동작을 하는 문제점이 있다.

위에서 설명한 에러는 아니지만, 에러가 나는 간단한 예제를 추가한다.


예제 코드 보기

부모 위젯에서 버튼을 눌러 자식인 카운터 위젯들의 카운터를 일괄적으로 늘리고, 큐 방식으로 뒤에 push하고 앞을 pop했을 때다.

부모: MyApp
자식: MyHomePage

Wrong

import 'package:flutter/material.dart';

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

class MyApp extends StatefulWidget {
  MyApp({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() => MyAppState();
}

class MyAppState extends State<MyApp> {

  List<MyHomePage> list = List.generate(5, (index) => MyHomePage(title: index.toString()));
  int i = 5;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text("test"),
        ),
        body: ListView.builder(itemCount: list.length,itemBuilder: (constet, i) => list[i]),
        floatingActionButton:
        IconButton(onPressed: (){
          for(MyHomePage h in list) h.incrementCounter();
          list.add(MyHomePage(title: (i++).toString()));
          list.removeAt(0);
          setState(() {

          });
        }, icon: Icon(Icons.details)),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {

  final MyHomePageState _someSfWState = MyHomePageState();

  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _someSfWState;

  void incrementCounter() => _someSfWState.incrementCounter();
}

class MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text(
          '${widget.title} You have pushed the button this many times:  ',
        ),
        Text(
          '$_counter',
          style: Theme.of(context).textTheme.headline4,
        ),
      ],
    );
  }
}
import 'package:flutter/material.dart';

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

class MyApp extends StatefulWidget {
  MyApp({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() => MyAppState();
}

class MyAppState extends State<MyApp> {

  List<MyHomePage> list = [];
  List<GlobalKey<MyHomePageState>> klist = [];
  int i = 5;

  @override
  void initState(){
    super.initState();

    klist = List.generate(5, (index) => GlobalKey());
    list = List.generate(5, (index) => MyHomePage(key: klist[index],title: index.toString()));
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text("test"),
        ),
        body: ListView.builder(itemCount: list.length,itemBuilder: (constet, i) => list[i]),
        floatingActionButton:
        IconButton(onPressed: (){
          for(GlobalKey<MyHomePageState> k in klist) k.currentState?.incrementCounter();
          klist.add(GlobalKey());
          list.add(MyHomePage(key: klist.last,title: (i++).toString()));
          list.removeAt(0);
          setState(() {

          });
        }, icon: Icon(Icons.details)),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {

  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => MyHomePageState();

}

class MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text(
          '${widget.title} You have pushed the button this many times:  ',
        ),
        Text(
          '$_counter',
          style: Theme.of(context).textTheme.headline4,
        ),
      ],
    );
  }
}


방법 2(O)

class SomeSfW extends StatefulWidget {

  SomeSfW({
    Key? key,
  }) : super(key: key);

  @override
  SomeSfWState createState() => SomeSfWState();

}

class SomeSfWState extends State<SomeSfW> {

  void foo(){
    /*
    some jobs...
    */
  }

  @override
  Widget build(BuildContext context) {
    return Text(" !! ");
  }

}

자식 위젯은 기본적인 형태와 같다.

// 부모 위젯에서 위젯 생성 시 키 넘겨 주기
GlobalKey<SomeSfWState> someSfWStateKey = GlobalKey();
SomeSfW(key: someSfWStateKey);

// 함수 사용
someSfWStateKey.currentState?.foo();

GlobalKey를 사용해 자식 위젯에 직접 접근할 수 있다. 이 GlobalKey는 스테이트마다 고유하므로 트리에서 꼬일 일도 없다.

참고

글로벌 키를 통해 someSfWStateKey.currentContext?.size 등 현재 스테이트의 컨텍스트도 접근할 수 있다.



굿

댓글남기기