Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
05-16 04:51
관리 메뉴

nomad-programmer

[Flutter] Reactive Programming 본문

Programming/Flutter

[Flutter] Reactive Programming

scii 2020. 10. 17. 23:19

Flutter에서의 Reactive Programming - Stream, Provider 패턴, BloC 패턴

요즘의 프로그래밍 패러다임은 반응형 프로그래밍(Reactive Programming) 이다. 반응형 프로그래밍은 비동기 데이터를 효율적으로 처리하기 위해 만들어졌다. 

비동기 처리 : 언제 도착할 지 모르는 데이터인 http 호출, UI 클릭, 데이터 저장, 에러 처리 등을 말한다.

StreamBuilder

플러터에서도 리액티브 프로그래밍을 할 수 있다. 보통은 Stream이나 RxDart를 사용한다. 그렇다면 Flutter에서 Stream은 어떠한 방식으로 사용할까? 

Flutter에서 Stream은 "StreamBuilder" 를 사용하여 스트림 데이터 처리를 한다. StreamBuilder를 사용하면 setState() 메소드를 사용하지 않고도 UI를 업데이트 할 수 있다. 그리고 setState() 메소드는 UI 전체를 다시 그리지만 StreamBuilder로 사용하면 StreamBuilder로 둘러쌓인 Widget만 다시 그리게되므로 더욱 효율적이다. 또한 스트림을 사용하면 항상 최신값을 가져오니 최신값을 확인할 필요도 없다.

StreamBuilder를 사용한 기본 카운터앱 수정한 모습

import 'dart:async';

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  // StreamController 생성.
  final StreamController<int> _streamCtrlCount = new StreamController<int>();
  // 0.2초마다 데이터가 생성되는 스트림
  final Stream<int> _streamAutoIncrement =
      new Stream.periodic(new Duration(milliseconds: 200), (int x) => x);

  @override
  void dispose() {
    // 스트림 닫기
    _streamCtrlCount.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Column(
              children: [
                // 스트림빌더 생성
                StreamBuilder<int>(
                    // 어느 스트림을 사용할지 (자동으로 숫자가 올라가는 스트림 사용)
                    stream: _streamAutoIncrement,
                    builder:
                        // snapshot은 스트림의 스탭샷을 나타낸다. 그래서
                        // snapshot.data는 스트림의 최신값이 나온다.
                        (BuildContext context, AsyncSnapshot<int> snapshot) {
                      return Text(
                        'Auto increment: ${snapshot.data}',
                        style: TextStyle(fontSize: 20),
                      );
                    }),
                Container(height: 30),
                Text(
                  'You have pushed the button this many times:',
                ),
              ],
            ),
            // 스트림빌더 생성
            StreamBuilder<int>(
                // 버튼 클릭 시, 숫자가 올라가는 스트림 사용
                stream: _streamCtrlCount.stream,
                // 초기값 설정. 옵션 값이다.
                initialData: _counter,
                builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
                  return Text(
                    '${snapshot.data}',
                    style: Theme.of(context).textTheme.headline4,
                  );
                }),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // 이벤트 발생
          _streamCtrlCount.sink.add(++_counter);
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

이번에는 서버에서 데이터를 가져와 데이터 가공 후 StreamBuilder로 UI를 갱신하는 예제를 보도록하자.

jsonplaceholder.typicode.com/todos

위의 데이터를 사용해서 TodoList를 생성하는 예제이다.

Todo 리스트 완성된 모습

import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: TodoList(),
    );
  }
}

class TodoList extends StatefulWidget {
  @override
  _TodoListState createState() => _TodoListState();
}

class _TodoListState extends State<TodoList> {
  final StreamController<List<Todo>> _streamController =
      new StreamController<List<Todo>>();

  @override
  void dispose() {
    _streamController.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('StreamBuilder 테스트'),
      ),
      body: Column(
        children: <Widget>[
          Flexible(
            child: StreamBuilder<List<Todo>>(
                stream: _streamController.stream,
                builder:
                    (BuildContext context, AsyncSnapshot<List<Todo>> snapshot) {
                  // 스탭샷에 데이터가 존재하지 않으면 텍스트 반환
                  if (!snapshot.hasData) {
                    return Text('No data');
                  }
                  return ListView.builder(
                    itemBuilder: (context, index) =>
                        _buildListTile(snapshot, index),
                    // 스냅샷의 데이터 크기만큼 뷰를 그린다.
                    itemCount: snapshot.data.length,
                  );
                }),
          ),
          Center(
            child: RaisedButton(
              // 버튼을 누르면 서버에서 데이터를 가져온다.
              onPressed: () async =>
                  // 스트림 싱크에 데이터를 추가한다.
                  _streamController.sink.add(await getTodoList()),
              color: Colors.lightBlueAccent,
              child: Text('Data Load'),
            ),
          ),
        ],
      ),
    );
  }

  // 리스트 뷰에 들어갈 타일 위젯
  Widget _buildListTile(AsyncSnapshot<List<Todo>> snapshot, int index) {
    return ListTile(
      leading: Text('${snapshot.data[index].id}'),
      title: Text('${snapshot.data[index].title}'),
      subtitle: Text(
        'completed',
        style: TextStyle(
            color: snapshot.data[index].completed ? Colors.blue : Colors.red),
      ),
    );
  }

  Future<List<Todo>> getTodoList() async {
    List<Todo> todoList = <Todo>[];
    final String _url = 'https://jsonplaceholder.typicode.com/todos';

    /* 아래의 코드와 동일한 결과를 얻는 코드이다.
    await http
        .get(_url)
        .then((value) => value.body)
        .then(json.decode)
        .then((value) => value.forEach((x) => todoList.add(Todo.fromJson(x))));
     */

    http.Response getData = await http.get(_url);
    List<dynamic> jsonData = await json.decode(getData.body);
    jsonData.forEach((element) => todoList.add(Todo.fromJson(element)));

    return todoList;
  }
}

class Todo {
  final int _userId;
  final int _id;
  final String _title;
  final bool _completed;

  Todo(this._userId, this._id, this._title, this._completed);

  Todo.fromJson(Map jsonData)
      : _userId = jsonData['userId'],
        _id = jsonData['id'],
        _title = jsonData['title'],
        _completed = jsonData['completed'];

  int get userId => _userId;
  int get id => _id;
  String get title => _title;
  bool get completed => _completed;
}

StreamBuilder는 stream 속성과 builder 속성으로 구성되어 있다. stream 속성은 StreamBuilder에 사용할 stream을 정하는 일이고, builder 속성은 stream 데이터를 받아 UI를 그리는 일은 한다.

이렇듯, Stream을 사용하면 비동기 데이터를 다루는 일이 한결 수월해진다. 하지만 코드의 규모가 커질수록 코딩이 점점 힘들어지는 것을 느낄 것이다. 
이유는, Stream과 StreamBuilder가 앱의 코드에 추가되다 보니 UI 코드와 데이터 코드가 섞이기때문이다. 이러한 문제를 해결하는 방법은 UI 코드와 데이터 코드를 분리하는 것이다.

UI 코드와 데이터 처리 코드를 분하는 방법은 MVC 패턴으로 하면 된다. Flutter에서 MVC 패턴처럼 코딩할 수 있도록 도와주는 것이 Provider 패턴과 Bloc 패턴이다. 

Provider는 중규모 앱을 만들 때 사용하면 유용하고, Bloc 패턴은 대규모 앱을 만들 때 사용하면 유용하다.


Provider 패턴

Provider 패턴은 아래의 글을 참고하면 된다.

2020/10/12 - [Programming/Flutter] - [Flutter] Provider Pattern

 

[Flutter] Provider Pattern

Flutter Provider란? Provider는 2019년 구글 IO에서 추천되며 큰 주목을 받았다. 원래는 플러터 커뮤니티에서 만든 플러그인이었으나, 구글에서 공식적으로 추천할 정도로 편리함을 가져다준다. 2018년 ��

nomad-programmer.tistory.com

2020/10/11 - [Programming/Flutter] - [Flutter] Provider 패턴으로 만든 StopWatch APP

 

[Flutter] Provider 패턴으로 만든 StopWatch APP

해당 스탑워치는 0.01초까지 측정 가능하며 Timer 클래스를 활용하여 0.01초마다 화면을 갱신한다. main.dart 파일 코드 // main.dart import 'package:flutter/material.dart'; import 'package:provider/provid..

nomad-programmer.tistory.com


Bloc 패턴

Bloc은 비지니스 로직 컴포넌트(Business Logic Component)의 약자이다. Bloc 패턴을 사용하는 이유는 다음과 같다.

비지니스 로직은 데이터베이스 조회나 서버와의 통신 등 데이터를 처리하는 부분을 말한다.
  • UI와 비지니스 로직을 분리해 개발을 원활히할 수 있다.
  • UI와 비지니스 로직을 분리하면, UI를 수정하는 일이 비지니스 로직에 영향을 미치지 않고, 비지니스 로직의 변경이 UI에 영향을 미치지 않게 된다.
  • 테스트가 용이해지고 UI의 변경이 손쉬워진다. 
  • Widget Build 횟수가 줄어들게 되므로 성능면에서도 이점이 있다.
  • 기존 MVC 패턴와 유사하다.

Bloc 패턴은 아래와 같은 흐름으로 사용된다.

  1. Bloc 생성 후 로직 구현
  2. BlocProvider 생성 후 로직(Bloc)을 UI에 건내준다.
  3. BlocProvider를 통해 건내받은 Bloc을 StreamBuilder와 연결한다.

pub.dev/packages/bloc

 

bloc | Dart Package

A predictable state management library that helps implement the BLoC (Business Logic Component) design pattern.

pub.dev


www.didierboelens.com/2018/08/reactive-programming-streams-bloc/

 

Flutter - Reactive Programming - Streams - BLoC

Flutter - Introduction to the notions of Streams, Bloc and Reactive Programming. Theory and practical examples. Difficulty: Intermediate

www.didierboelens.com

Comments