공식문서

예제

class MyCounter extends StatefulWidget {
  const MyCounter({super.key});

  
  State<MyCounter> createState() => _MyCounterState();
}

class _MyCounterState extends State<MyCounter> {
  int count = 0;

  
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $count'),
        TextButton(
          onPressed: () {
            setState(() {
              count++;
            });
          },
          child: Text('Increment'),
        )
      ],
    );
  }
}

Using a StatefulWidget

  • Encapsulation
    MyCounter 위젯은 count 변수에 접근하거나 변경할 수 없다.

  • Object lifecycle
    _MyCounterState object와 count 변수는 MyCounter 위젯이 생성될 때 같이 생성된다.
    그리고 MyCounter 위젯이 제거되기 전까지 존재한다.

Sharing state between widgets

우리는 아래 2가지에 대해서만 알면 된다.

  • 공유 중인 state를 업데이트하고, 사용하는 위젯에게 알려준다.
  • 위젯이 state 변화를 감지하고, UI를 리빌드한다.

Using widget constructors

class MyCounter extends StatelessWidget {
  final int count;
  const MyCounter({super.key, required this.count});

  
  Widget build(BuildContext context) {
    return Text('$count');
  }
}
Column(
  children: [
    MyCounter(
      count: count,
    ),
    MyCounter(
      count: count,
    ),
    TextButton(
      child: Text('Increment'),
      onPressed: () {
        setState(() {
          count++;
        });
      },
    )
  ],
)

공유 중인 데이터를 위젯 constructor로 전달하는 방법이다.
이러한 디자인 패턴을 Dependency Injection이라고 부른다.

Using InheritedWidget

class Test extends InheritedWidget {
  const Test({
    super.key,
    required this.data,
    required super.child,
  });

  final String data;

  static MyState of(BuildContext context) {
    // This method looks for the nearest `Test` widget ancestor.
    final result = context.dependOnInheritedWidgetOfExactType<Test>();

    assert(result != null, 'No Test found in context');

    return result!;
  }

  
  // This method should return true if the old widget's data is different
  // from this widget's data. If true, any widgets that depend on this widget
  // by calling `of()` will be re-built.
  bool updateShouldNotify(MyState oldWidget) => data != oldWidget.data;
}
class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  
  Widget build(BuildContext context) {
    var data = Test.of(context).data;
    return Scaffold(
      body: Center(
        child: Text(data),
      ),
    );
  }
}

InheritedWidget을 사용하면 중간 위젯을 거치지 않고, 바로 접근할 수 있다.
즉, 순차적으로 데이터를 넘길 필요가 없다.

사용하기 위해서는 InheritedWidget을 상속 받고, 정적 메서드 of를 만들어야 한다.
of 메서드는 dependOnInheritedWidgetOfExactType를 통해 가장 가까운 Test를 위젯 트리에서 찾는다.

또한, updateShouldNotify로 이전 데이터와 새로운 데이터를 비교해 다르면 true를 반환한다.
true가 반환되면 InheritedWidget을 상속하는 모든 위젯이 리빌드된다.

Using callbacks

class MyCounter extends StatefulWidget {
  const MyCounter({super.key, required this.onChanged});

  final ValueChanged<int> onChanged;

  
  State<MyCounter> createState() => _MyCounterState();
}
TextButton(
  onPressed: () {
    widget.onChanged(count++);
  },
),

콜백을 통해서 state의 변화를 알릴 수도 있다.

Using listenables

이제 공유하고 있는 state가 바뀔 때, UI를 리빌드하는 방법에 대해 알아보자.

ListenableBuilderValueListenableBuilder로 state가 바뀐 것을 알아챌 수 있다.

ChangeNotifier

class CounterNotifier extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}
Column(
  children: [
    ListenableBuilder(
      listenable: counterNotifier,
      builder: (context, child) {
        return Text('counter: ${counterNotifier.count}');
      },
    ),
    TextButton(
      child: Text('Increment'),
      onPressed: () {
        counterNotifier.increment();
      },
    ),
  ],
)

notifyListeners를 사용하면 ListenableBuilder에서 변화를 감지하고, 리빌드한다.

ValueNotifier

ValueNotifier<int> counterNotifier = ValueNotifier(0);
Column(
  children: [
    ValueListenableBuilder(
      valueListenable: counterNotifier,
      builder: (context, value, child) {
        return Text('counter: $value');
      },
    ),
    TextButton(
      child: Text('Increment'),
      onPressed: () {
        counterNotifier.value++;
      },
    ),
  ],
)

ValueNotifier는 ChangeNotifier의 간소화 버전이다.
ValueListenableBuilder 말고 ListenableBuilder를 사용할 수도 있다.

Using MVVM for your application's architecture

Defining the Model

import 'package:http/http.dart';

class CounterData {
  CounterData(this.count);

  final int count;
}

class CounterModel {
  Future<CounterData> loadCountFromServer() async {
    final uri = Uri.parse('https://myfluttercounterapp.net/count');
    final response = await get(uri);

    if (response.statusCode != 200) {
      throw ('Failed to update resource');
    }

    return CounterData(int.parse(response.body));
  }

  Future<CounterData> updateCountOnServer(int newCount) async {
    // ...
  }
}

MVVM에서 가장 밑단에 존재하는 구조로, HTTP client를 사용해 데이터를 fetch하고 있다.
Mock 또는 Fake 데이터를 생성해 unit test를 할 수도 있고, 각 layer 간에 경계를 명확히 나눌 수 있다.
받아온 데이터를 CounterData라는 자료구조로 변환해 전달하고 있다.

data_source, dto, repository_impl을 합쳤다고 볼 수 있다.

Defining the ViewModel

import 'package:flutter/foundation.dart';

class CounterViewModel extends ChangeNotifier {
  final CounterModel model;
  int? count;
  String? errorMessage;
  CounterViewModel(this.model);

  Future<void> init() async {
    try {
      count = (await model.loadCountFromServer()).count;
    } catch (e) {
      errorMessage = 'Could not initialize counter';
    }
    notifyListeners();
  }

  Future<void> increment() async {
    var count = this.count;
    if (count == null) {
      throw('Not initialized');
    }
    try {
      await model.updateCountOnServer(count + 1);
      count++;
    } catch(e) {
      errorMessage = 'Count not update count';
    }
    notifyListeners();
  }
}

model과 view가 직접적으로 연결되는 것을 막아준다.
그리고 model로부터 데이터 흐름이 시작된다는 것도 보장해준다.

비즈니스 로직이 포함된 곳으로, usecase, state가 합쳐졌다고 볼 수 있다.

Defining the View

ListenableBuilder(
  listenable: viewModel,
  builder: (context, child) {
    return Column(
      children: [
        if (viewModel.errorMessage != null)
          Text(
            'Error: ${viewModel.errorMessage}',
            style: Theme.of(context)
                .textTheme
                .labelSmall
                ?.apply(color: Colors.red),
          ),
        Text('Count: ${viewModel.count}'),
        TextButton(
          onPressed: () {
            viewModel.increment();
          },
          child: Text('Increment'),
        ),
      ],
    );
  },
)

state의 변화를 감지하기 위해 ListenableBuilder을 사용하고 있다.

profile
Software Engineer

0개의 댓글