이 글에서 Widget, State, BuildContext, InheritedWidget 에 대해 알아 봅니다.
특히 중요하지만 문서화가 덜된 InheritedWidget에 집중해 봅시다.
플러터에서 위젯, 상태, 빌드컨텍스트는 개발자들이 이해야할 중요한 개념들입니다.
그러나 문서에는는 방대하고 이런 것들을 명확히 설명해주지 않습니다.
이 개념을 나만의 방법으로 설명 할 것입니다. 마음에 들지 않는 사람도 있겠지만
이 글의 실제 목적은 다음 주제를 명확히하기위한 것입니다.
-.stateful과 stateless위젯의 차이
-.BuildContext 란
-.State란 무엇이고 어떻게 사요하는지.
-.BuildContext와 State 사이의 관계
-.InheritedWidget과 위젯트리안에서 정보를 전달하는 방법
-.리빌드의 개념
파트1 : 컨셉들
위젯
In Flutter, almost everything is a Widget.
위젯을 시각적 구성요소라고 생각하세요.
레이아웃과 관련한 것을 만들고 있다면, 위젯을 사용하고 있는 것입니다.
위젯트리
위젯은 트리 구조로 구성됩니다.
다른 위젯을 포함하는 위젯을 상위 위젯 (또는 위젯 컨테이너)이라고합니다.
상위 위젯에 포함 된 위젯을 하위 위젯이라고합니다.
빌드컨텍스트
BuildContext는 빌드 된 모든 위젯 트리 구조 내의 위젯 위치에 대한 참조입니다.
간단히 말해 빌드컨텍스를 위젯트리의 일부라고 생각하세요.
빌드컨텍스트는 한 위젯에만 속합니다.
만약 위젯 A가 하위위젯을 가지고있다면, 위젯 A의 빌드컨텍스트는 자식 빌드컨텍스트의 상위 빌드컨텍스트가 됩니다.
빌드컨텍스트들은 연결되어 있고 빌드컨텍스트 트리(부모자식 관계)를 구성합니다.
빌드컨텍스트 가시성
어떤 빌드컨텍스트는 자신의 빌드컨텍스트나 상위 빌드컨텍스트에서만 보입니다.
예를 들어 Scaffold > Center > Column > Text 에서
context.ancestorWidgetOfExactType(Scaffold)는
Text 컨텍스트에서 트리 구조로 이동하여 첫 번째 Scaffold를 반환합니다.
상위 빌드컨텍스트에서 descendat 위젯을 찾을 수도 있지만 그렇게 하지 않느 것이 좋습니다.
위젯의 타입
위젯은 두가지 타입이 있습니다.
Stateless Widget
빌드타이밍에 부모로 부터 받은 정보로에만 의존하는 컴포넌트입니다.
즉, 한 번 빌드되면 신경쓰지 않아도 된다는 말입니다.
Text나 Column, Container등등은 빌드할 때 파라미터를 단순히 전달합니다.
파라미터는 데코레션 디멘션등등이 될수있습니다. 어쨌든 이런 것들은 한번 적용되면 다음 빌드까지 변하지 않습니다.
StatelessWidget은 로드/빌드될때 한번 그려집니다. 이말은 이벤트나 사용자 동작에의해 그려지지 않는다는 뜻입니다.
StatelessWidget 의 라이프사이클
다음은 Stateless Widget과 관련된 코드의 일반적인 구조입니다.
보시다시피 몇 가지 매개 변수를 생성자에 전달합니다.
그러나 이 매개 변수는 이후에 변경되지 않습니다. 그래서 그대로 사용해야합니다.
오버라이드할 수있는 메소드가 있어도 거의 오버라이드 하지 않습니다.
빌드메소만 오버라드하면됩니다.
Stateless Widget의 라이프사이클은 단순합니다.
초기화
렌더링(build())
StatefulWidget
또라은 위젯은 위젯이 살아있는동안 내부데이터를 다룹니다.
따라서 데이터는 위젯이 살아 있는 동안 동적으로 변합니다.
이 데이터의 집합을 State라고합니다.
내부에 State를 가지고 있는 위젯을 Stateful 위젯이라고 부릅니다.
위젯의 예로는 사용자가 선택할 수있는 체크 박스나 조건에 따라 비활성화되는 버튼 등이 있습니다.
State
State는 StatefulWidget 인스턴스의 "행동"부분을 정의합니다.
State는 위젯의 동작과 레이아웃을 위한 정보를 가지고 있습니다.
State가 변경되면 위젯은 리빌드 됩니다.
State와 BuildContext와의 관계
StatefultWidget의 경우 State는 Buildcontext와 연관되어 있습니다.
이 연결은 영구적이며 State 개체는 결코 BuildContext를 변경하지 않습니다.
위젯 BuildContext가 트리 구조 주위를 이동할 수있는 경우에도 State는 해당 BuildContext와 연결된 채로 유지됩니다.
State가 BuildContext와 연결되면 State는 마운트된 것으로 간주됩니다.
State 객체는 BuildContext와 연관되어 있기 때문에 State 객체가 다른 BuildContext를 통해 (직접적으로) 액세스 할 수 없다는 것을 의미합니다!
StatefulWidget 라이프사이클
기본적인것은 설명했고 좀더 깊게 살펴 봅시다.
StatefulWidet과 관련된 전형적인 코드 구조 입니다.
이 글의 중요 주제는 State의 개념을 설명하는 것입니다.
didUpdateWidget, deactivate, reassemble 메소드들을 오버라이드 할 수 있습니다.
아래 다이어 그램은 StatefulWidget이 생성되는 동안의 일련의 동작과 호출을 보여줍니다.
오른쪽에는 State 오브젝트의 상태를 보여줍니다.
그리고 context가 state와 연관되고, 그래서 사용할 수 있게되는 것도 볼 수 있습니다.
initState()
initState() 메소드는 State객체가 생성될 때 처음에 한 번 호출 되는 메소드입니다.
이 메소드는 추가적인 초기화를 실행할 필요가 있을 때 (에니메이션이나 컨트롤러등을 사용할 때 등등) 오버라이드하면 됩니다.
이 메소드를 오버라이드한다면 super.initState()를 가장먼저 실행해야합니다.
initState 메소드가 완료되면 State 객체는 초기화 되었고 Context를 사용할 수있습니다.
이메소드는 이 State 객체가 살아있는 동안 더이상 호출되지 않습니다.
didChangeDependencies()
didChangeDependencies()는 두번째로 호출되는 메소드 입니다.
이단계에서 context는 사용가능해서 사용해도됩니다.
이 메소드는 일반적으로 위젯이 InheritedWidget에 링크되어 있거나
BuildContext를 기반의 일부 리스너를 초기화해야하는 경우에 오버라이드 됩니다.
위젯이 InheritedWidget에 링크되어 있는 경우, 이 메소드는 위젯이 재빌드 될 때마다 호출 됩니다.
이 메소드를 오버라드 할 때 super.didChangeDependencies()를 먼저 호출 하세요.
build()
build(BuildContext context) 메소드는 didChangeDependencies (그리고 didUpdateWidget) 이후에 호출 됩니다.
여기는 위젯을 만드는 곳입니다.
이 메소드는 State 객체가 변경될 때마다 (또는 InheritedWidget이 등록된 위젯에게 통지할 때마다) 호출됩니다.
리빌드를 해야한다면 setState({...}) 메소드를 호출 하세요.
dispose()
dispose() 메소드는 위젯이 폐기될 때 호출 됩니다.
리스너나 컨트롤러등 과 같은 것들을 정리할 필요가 있을 때 호출합니다.
super.dispose()를 먼저호출해 주세요.
Stateless or Stateful Widget?
어떤위젯을 쓸는 아래 질문에 답을 해세요.
위젯이 살아있는 동안 변경 될 변수를 고려해야하고
변경 될 경우 위젯이 강제로 리빌드 되어야합니까?
만약 예 라면 StatefultWidget이 필요한 것 이고, 아니면 StatelessWidget이 필요한 것입니다.
StatefultWidget 위젯은 두 파트로 만들어집니다.
The Widget main definition
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({
Key key,
this.color,
}): super(key: key);
final Color color;
@override
_MyStatefulWidgetState createState() => new _MyStatefulWidgetState();
}
첫번 째 파트 MyStatefulWidget은 보통 위젯의 퍼블릭파트입니다.
위젯을 위젯트리에 추가하려고할 때 이 부분을 인스턴스화 합니다.
이 부분은 위젯의 수명 동안 변하지 않습니다.
그리고 이 부분에서 해당 State에서 사용할 수 있는 파라미터들을 받습니다.
위젯의 첫 번째 부분의 레벨에서 정의 된 모든 변수는 위젯이 살아 있는 동안 변경되지 않습니다.
The Widget State definition
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
...
@override
Widget build(BuildContext context){
...
}
}
두 번 째 파트인 _MyStatefulWidgetState는 위젯이 존속하는 동안 변경되고,
변경 사항이 적용될 때마다 위젯을 강제로 다시 빌드하는 부분입니다.
변수이름의 '_'문자는 변수를 private으로 만듭니다.
_MyStatefulWidgetState 클래스는 widget.{변수의 이름}을 사용하여 MyStatefulWidget에 저장된
모든 변수에 액세스 할 수 있습니다.
이 예제에서 : widget.color
Widget unique identity — Key
Flutter에서는 각 위젯이 고유하게 식별됩니다. 이 고유 ID는 빌드 / 렌더링시 프레임 워크에 의해 정의됩니다.
이 고유 ID는 선택적 Key 매개 변수에 해당합니다.
생략하면 Flutter가 생성합니다.
경우에 따라 키를 강제해야 할 수도 있습니다.
그렇게 되면 키로 위젯에 접근할 수 있습니다.
이렇게하려면, 다음과 같은 헬퍼 중 하나를 사용해야합니다.
GlobalKey <T>, LocalKey, UniqueKey , ObjectKe
GlobalKey는 어플리케이션 전체에서 유일한 키를 가져옵니다.
Part 2: How to access the State?
앞서 State는 BuildContext에 연결되고, BuildContext는 위젯 객체에 연결된다고 설명했습니다.
1. the Widget itself
이론적으로 State에 액세스 할 수있는 것은 Widget State 자체뿐입니다.
이런 경우 위젯 State클래스의 변수에 접근하는 것은 어렵지 않습니다.
2. 자식 위젯
가끔 특정 작업을 하기위해서 부모 위젯은 자식위젯의 상태에 접근할 필요가 있습니다.
이런 경우에 자식 State에 접근하기위해서는 자식을 알야합니다.
가장 쉬운 방법은 이름을 통해서 호출 하는 것입니다. 플러터에서는 각 위젯은 빌드타임에 플레임워크에의해서 결정되는 유니크 아이디를 가지고 있습니다.
이전에 본 것처럼 key 파라미터를 통해서 이 아이디를 정의할 수도 있습니다.
GrobalKey<MyStatefulWidgetState> myWidgetStateKey = new GrobalKey<MyStatefulWidgetState>();
...
@override
Widget build(BuildContext context){
return MyStatefulWidget(
key:myWidgetStateKey,
color:Colors.blue,
)
}
이렇게 정의해 놓으면 부모 위젯에서는 myWidgetStateKey.currentState 를 통해 접근할 수있습니다.
사용자가 버튼을 눌렀을 때 SnackBar를 보여주는 기본 예제를 살펴 보겠습니다.
SnackBar는 Scaffold의 자식 위젯입니다. 그래서 Scaffold의 body에 있는 다른 자손 위젯에서 접근할 수 없습니다.
그러므로 Scaffold를 통해 접근하는 유일한 방법은 SnackBar를 표시하기위한 퍼블릭 메소드를 만드는 것입니다.
3.선조위젯(Ancestor Widget)
다음 다이어그램과 같이 다른 위젯의 하위 트리에 속한 위젯이 있다고 가정합니다.
이를 가능하게하는 3 가지 조건이 충족되어야합니다.
1. “Widget with State” (빨간색)는 자신의 State를 노출해야 합니다.
State를 노출하기 위해서는 다음 처럼 생성하는 순간에 기록해야합니다.
2. “Widget with State”는 getter/setter를 노출해야합니다.
어떤 위젯이 State의 속성에 set/get하기 위해서는 public property (not recommended)나 getter/setter를 통해 접근할 수있게 해야합니다.
3. "Widget interested in getting the State"(파랑색 위젯) 위젯은 State의 참조를 얻어야합니다.
(Use ?. (Conditional member access) instead of .(Member access) to avoid an exception when the leftmost operand is null:)
이 방법은 구현하기 쉽습니다. 하지만 후손 위젯은 언제 리빌드해야할지 알 수 없습니다. 후손 위젯의 Container가 리빌드 하기를 기다려야합니다. 이점은 매우 불편합니다.
다음 섹션에서는이 문제에 대한 해결책을 제공하는 Inherited Widget 개념을 다룹니다.
InheritedWidget
간결하고 간단한 단어로, InheritedWidget은 정보를 위젯 트리로 효율적으로 전파 (공유) 할 수있게 해줍니다.
InheritedWidget은 위젯 트리에 다른 하위 트리의 부모로 넣어주는 특별한 위젯입니다. 해당 하위 트리의 모든 위젯 부분은 해당 InheritedWidget에 의해 노출 된 데이터와 상호 작용할 수 있어야합니다.
기본
이것을 설명하기 위해서 다음 코드를 보세요
class MyInheritedWidget extends InhertitedWidget{
MyInhertitedWidget({
Key key,
@required
})
}
이 코드에서 자손트리 전체 위젯에 데이터를 공유하는 것을 목적으로하는 MyInheritedWidget라는 위젯을 정의합니다.
앞에서 설명한 것 처럼 InheritedWidget은 데이터를 전파하고 공유하기 위해서는 위젯트리 최상단에 위치해야합니다.
이 코드에서 InheritedWidet의 기본 생성자의 파라미터로 넘겨주는 @required Widget child는 이를 위한 것입니다.
static MyInheritedWidget of(BuildContext context) 메소드는 모든 자손위젯에게 컨텍스트를 가지는 가장 가까운 MyInheritedWidget 객체를 얻게해줍니다.
마지막으로 오버라이드한 updateShouldNotify()는 데이터가 변경됐을 때 알림이 모든 자식에게 전달할 것인지를 InhertiedWidget에게 말해주는 데 사용됩니다.
따라서 다음과 같이 트리 노드 레벨에 배치해야합니다.
class MyParentWidget ... {
...
@override
Widget build(BuildContext context){
return MyInheritedWidget(
data : counter,
child:Row(
Children : <Widget>[...]
)
)
}
}
어떻게 자식이 InheritedWidget의 데이터에 접근 할 수 있습니까?
자식을 빌드할 때, 다음과 같이 InheritedWidget에 대한 참조를 얻습니다.
class MyChildWidget ... {
...
@override
Widget bild(BuildContext context){
final MyInheritedWidget inheritedwidget = MyInheritedWidget.of(context);
return Container(
color : inheritedWiget.data.color
)
}
}
위젯 간의 상호 작용을 만드는 방법은 무엇입니까?
아래 그림과 같은 위젯 트리 구조를 생각해보자
다음을 가정해 보겠습니다.
-. 위젯 A는 쇼핑카트에 아이템을 추가하는 Button입니다.
-. 위젯 B는 쇼핑카트의 아이템의 갯수를 보여주는 Text 입니다.
-. 위젯 C는 위젯B 옆있으면 어떤 텍스트를 가지는 Text 입니다.
-. 우리는 위젯 A가 눌려졌을 때 쇼핑카트에 있는 아이템의 갯수를 자동으로 표시되길 원하는데 C는 리빌드 되지 않기를 원합니다.
InheritedWidget은 이때 사용하기 좋은 위젯입니다.
예제코드
소스먼저보고 설명하겠습니다.
설명
이것은 매우 간단합니다.
_MyInherited는 InheritedWidget입니다. 위젯A의 버튼을 클릭할 때마다 재생성 됩니다.
MyInheritedWidget은 아이템의 리스트를 포함하는 State를 갖는 StatefulWidget 입니다.
static MyInheritedWidgetState of(BuildContext context)을 통해 이 State로 접근할 수 있습니다.
MyInheritedWidgetState 는 getter 하나와 addItem() 메소드를 노출합니다. 이 메소드들은 자손 위젯트리의 위젯에서 사용가능합니다.
아이템을 State에 추가할 때마다 MyInheritedWidgetState는 리빌드 합니다.
MyTree는 MyInheritedWidget을 부모로하는 위젯트리를 만듭니다.
위젯A는 RaisedButton 입니다. 이 버튼을 누르면 가장가까운 InheritedWidget에 addItem 메소드를 실행시킵니다.
위젯B는 Text 입니다. 가장가까운 MyInheritedWidget의 레벨에서 아이템의 갯수를 표시합니다
이 모든게 어떻게 작동합니까?
이후 알림을위한 위젯 등록
자손 위젯에서 MyInheritedWidget.of(context)를 실행시키면, 자신의 BuildContext를 전달하면서 MyInheritedWidget의 다음 메소드를 호출합니다.
static MyInheritedWidgetState of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
}
내부적으로 MyInheritedWidgetState의 객체를 리턴하는 것 외에도 소비자위젯(consumer widget)을 변경 사항 알림에 등록합니다.
이 정적 메서드 호출은 실제로 두 가지를 수행합니다.
소비자위젯(consumer widget)은 자동으로 구독자 목록에 추가됩니다.
그 구독자들은 InheritedWidget이 변경되면 리빌드됩니다.
_MyInherited 위젯 (일명 MyInheritedWidgetState)에서 참조 된 데이터가 소비자위젯(consumer widget)에게 반환됩니다.
흐름(flow)
위젯A와 위젯B가 InheritedWidget을 구독하고 있고, 위젯A의 RaisedButton을 클릭해서 _MyInherited가 변경되면 동작과정은 다음과 같습니다.
1.MyInheritedWidgetState.addItem가 호출됩니다.
2.MyInheritedWidgetState.addItem 메소드는 List<Item>에 새로운 Item을 추가합니다.
3.MyInheritedWidget를 리빌드하기위해 setState를 호출합니다.
4.새로운 List<Item>을 가지는 새로운 _MyInherited 객체가 생성됩니다.
5._MyInherited는 data 마라미터로 넘어온 새로운 State를 기록합니다.
6._MyInherited는 오버라이드된 updateShouldNotify()에서 소비자들(위젯A와 위젯B)에게 알림을 보낼지 말지 체크합니다. (여기서는 true)
7._MyInherited는 소비자들(위젯A와 위젯B)에게 리빌드를 요청합니다.
8.위젯C는 소비자가 아니므로 리빌드 되지 않습니다.
위젯A와 위젯B 둘 다 리빌드 되는 데 위젯A는 바뀌는 것이 없기 때문에 리빌하는 것은 불필요합니다.
어떻게 하면 이것을 방지 할 수 있을 까요?
InheritedWidget에 액세스하면서도 위젯이 리빌드되는 것을 막기
위젯A가 MyInheritedWidgetState에 액세스하기 때문에 리빌드 되는 것입니다.
context.inheritFromWidgetOfExactType() 메소드를 호츨하는 것은 자동으로 위젯을 소비자가 되게 합니다.
위젯A가 MyInheritedWidgetState 위젯에 계속 접근하면서도 자동적인 구독을 막는 방법은 MyInheritedWidget 의 정적메소드를 아래와 같은 변경하는 것입니다.
static MyInheritedWidgetState of ([BuildContext context, bool rebuild = true]){
return (rebuild ? context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited
: context.ancestorWidgetOfExactType(_MyInherited) as _MyInherited).data;
}
bool rebuild 파라미터를 추가해서
rebuild 파라미터가 true 이면(디폴트) 이전처럼 위젯은 소비자에 목록에 추가됩니다.
reubild 파라미터가 false이면 data에 접근하지만 InheritedWidget의 내부구현에는 접근하지 못합니다.
그래서 이것을 적용하고 위젯A의 리빌드를 막기위해서는 위젯A의 코드를 아래처럼 약간 수정하는 것이 좋습니다.
class WidgetA extends StatelessWidget{
@override
Widget build(Context context){
final MyInheritedWidgetState = MyInheritedWidget.of(context, false);
return Container(
child:RaisedButton(
child:Text("Add Item"),
onPress : (){
state.addItem("new Item");
}
)
)
}
}
이렇게하면 위젯A는 리빌드 되지 않을 것입니다.
Route, Dialog, BuildContext 들은 Application에 묶여 있습니다.
즉, 화면 A 내부에서 다른 화면 B를 표시하도록 요청한 경우에도 2 개의 화면 중 하나에서 자신의 컨텍스트와 관련된 "쉬운 방법"이 없음을 의미합니다.
스크린B에서 스크린A의 컨텍스트에대해 알 수있는 유일한 방법은 스크린A에서 Navigator.of(context).push(….)의 파라미터로 넘겨주는 방법입니다.
끗..
의문점.
1)
_MyInherited과 MyInheritedWidget을 분리한게 좀 오묘하군...무슨 이유가 있나?
List<Item>을 state로 가지고 있고 싶어서 그런가.??
2)
그리고 MyInheritedWidget가 child를 파라미터로 _yInherited에 넘겨주기 때문에 _MyInherited 가 리빌드 되면 child들(Scaffold 밑에 잇는 위젯들)까지도 리빌드 될 것같은데 왜 리빌드 안되는 것 처럼 설명하지? 위젯트리에 있다고 다 리빌드하는게 아니라 필요한 위젯들, 그러니깐 updateShouldNotify()의 리턴값이 true인 위젯들만 해주는게 InhertiedWidget의 기능인가?
https://flutter.dev/docs/development/tools/inspector
https://github.com/bizz84/coding-with-flutter-login-demo
https://medium.com/flutter-community/widget-state-buildcontext-inheritedwidget-898d671b7956
'flutter' 카테고리의 다른 글
여러가지 화면크기와 화면방향으로 개발하기 (0) | 2019.04.23 |
---|---|
bloc todo (0) | 2019.04.19 |
플러터용 파이어 베이스 1부 (0) | 2019.04.11 |
Dart에서 stream 만들기 (0) | 2019.04.04 |
dart (0) | 2019.04.03 |