<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dc143c_dev.log</title>
        <link>https://velog.io/</link>
        <description>크로스플랫폼 앱 개발자</description>
        <lastBuildDate>Fri, 27 Jan 2023 16:00:38 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>dc143c_dev.log</title>
            <url>https://velog.velcdn.com/images/dc143c_dev/profile/667b3535-aa75-4281-86a7-369103d17e20/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. dc143c_dev.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dc143c_dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Flutter][GetX] 만보기 앱 개발일지 - 3 | GetConnect로 http 호출하고 받아온 데이터 UI에 구현해보기]]></title>
            <link>https://velog.io/@dc143c_dev/FlutterGetX-%EB%A7%8C%EB%B3%B4%EA%B8%B0-%EC%95%B1-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-3-GetConnect%EB%A1%9C-http-%ED%98%B8%EC%B6%9C%ED%95%98%EA%B3%A0-UI%EC%97%90-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@dc143c_dev/FlutterGetX-%EB%A7%8C%EB%B3%B4%EA%B8%B0-%EC%95%B1-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-3-GetConnect%EB%A1%9C-http-%ED%98%B8%EC%B6%9C%ED%95%98%EA%B3%A0-UI%EC%97%90-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Fri, 27 Jan 2023 16:00:38 GMT</pubDate>
            <description><![CDATA[<h2 id="0">0.</h2>
<p>이번 게시글에서는 getX 패턴을 사용하여 http를 호출하고, api 서버에서 받아온 데이터를 데이터 모델과 UI 모델에 전달하여 상품 페이지를 구현해보는 과정을 기술해본다.</p>
<p>플러터에서 기본적으로 제공하는 http 호출 방식이 있으나, 그건 블로그 첫번째 게시물에 이미 소개한 바가 있다.</p>
<p>더불어 나는 프로젝트 앱을 모두 getX 패턴으로 작성할 계획이라, getX 라이브러리에서 제공해주는 GetConnnect 패키지를 사용하여 http를 호출해보려 한다.</p>
<p>해당 게시글의 내용은 기능 구현을 위한 정석적인 방법이 아닐수 있으며, 구현 과정이 미숙하거나 정리되지 않은 코드가 포함될수도 있다.</p>
<p>그럼에도 플러터를 학습하는 누군가와, 얼마 못가 이 내용을 까먹을 미래의 나를 위해 이 게시글을 작성해본다.</p>
<h2 id="1-사전-준비">1. 사전 준비</h2>
<p>우선 api 서버 백엔드가 마련되어 있어야 한다.</p>
<p>이 내용은 이미 <strong><a href="https://velog.io/@dc143c_dev/Flutter-node.jsexpressmysql-%EB%B0%B1%EC%97%94%EB%93%9C%EC%99%80-%EC%97%B0%EB%8F%99%ED%95%98%EC%97%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-UI%EC%97%90-%EC%B6%9C%EB%A0%A5%ED%95%B4%EB%B3%B4%EA%B8%B0">첫 게시글</a></strong> 에 기술했으나 짧게 요약하자면, 필자는 로컬 환경에 <strong>Node.js express 웹 서버</strong>와 <strong>mySQL DB 서버</strong>를 연결하여 데이터 CRUD 기능을 맡아줄 간단한 api 서버를 올려놓은 상태다.</p>
<p>플러터를 학습하기 전에 이미 백엔드 지식을 경험해본 분들이라면 해당 서버의 구현 방식은 필자보다도 훨씬 더 잘 알고계실 터다.</p>
<p>그럼에도 약간의 설명을 덧붙이자면,</p>
<pre><code class="language-js">//node.js express server
//server.js
app.get(&#39;/getStbCoffee&#39;, function(req, res){
  var sql = &#39;select * from product_stb&#39;;
  con.query(sql, function(err, id, fields){
    var product_id = req.params.id;
    if(id){
      var sql=&#39;select * from product_stb&#39;
      con.query(sql, function(err, id, fields){
        if(err){
          console.log(err);
        }else{
          res.json(id);
        }
      })
    }
  })
})</code></pre>
<p>데이터 테이블에서 데이터를 받아오는 기능을 하는 메소드는 이러하고,</p>
<p><img src="https://velog.velcdn.com/images/dc143c_dev/post/6b2f85ee-4f68-471c-8c0d-98a9ef20c615/image.png" alt=""></p>
<p>우리가 사용할 데이터 테이블은 대강 이런 구조다.</p>
<p>프라이머리 키로 product_id를 가지고, 이름, 브랜드, 가격, 이미지 패스를 갖고 있다.</p>
<p>다른 브랜드의 상품 데이터 테이블도 동일한 구조를 가지고 있으며, <strong>이는 flutter에서 같은 구조의 테이블을 하나의 데이터 모델로 묶어서 처리하기 위함이다.</strong> 이는 후술.</p>
<p>조금 더 여유가 있거나 정식 서비스를 할 예정이라면 클라우드 서비스를 이용해 서버를 호스팅하는것도 좋을 듯하다.</p>
<h2 id="2-provider">2. Provider</h2>
<p>프로바이더의 사전적 정의는 공급자다.</p>
<p>동명의 상태관리 라이브러리와 혼동을 유의하라. 어떻게 보면 하는 역할은 비슷하긴 하다.</p>
<pre><code class="language-dart">//사이클.
//api 를 호출하는 프로바이더를 선언 -&gt;
//각 UI 의 컨트롤러에서 프로바이더의 원하는 프로바이더를 호출 -&gt;
//프로바이더는 productFromJson 으로 데이터 모델에 데이터를 넘겨
//flutter UI 에서 활용할수 있도록 바꿔줌 -&gt;
//최종 유저가 사용할 UI 에서 빌더를 사용해 UI 모델에 데이터 값을 하나씩 담아줌. -&gt;
//유저가 시인할 UI에 빌드.
class ProductProvider extends GetConnect implements GetxService {
  //api get 호출 메소드.
  Future&lt;List&lt;CoffeeProductModel&gt;?&gt; getStbProductData() async {
    final response = await get(&#39;http://localhost:8000/getStbCoffee&#39;);
    print(response.body);
    if (response.status.hasError) {
      return Future.error({response.statusText});
    } else {
      return productFromJson(response.body);
    }
  }

  Future&lt;List&lt;CoffeeProductModel&gt;?&gt; getTwsProductData() async {
    final response = await get(&#39;http://localhost:8000/getTwsCoffee&#39;);
    print(response.body);
    if (response.status.hasError) {
      return Future.error({response.statusText});
    } else {
      return productFromJson(response.body);
    }
  }

  Future&lt;List&lt;CoffeeProductModel&gt;?&gt; getYdyProductData() async {
    final response = await get(&#39;http://localhost:8000/getYdyCoffee&#39;);
    print(response.body);
    if (response.status.hasError) {
      return Future.error({response.statusText});
    } else {
      return productFromJson(response.body);
    }
  }

  @override
  void onInit() {
    super.onInit();
  }
}</code></pre>
<p>ProductProvider는 문자 그대로 상품 데이터를 공급하는 역할이다.</p>
<p>getX 에서 제공하는 http 통신 라이브러리인 <strong>GetConnet</strong>를 상속하여 getX 패턴의 http 통신 기능을 쓸수 있도록 한다.</p>
<p>필자는 response 변수에 데이터를 받아오게끔 했다.</p>
<p>if문을 거쳐 데이터를 받아오는데 성공했다면, <strong>productFromJson</strong>에 응답 값(response.body)를 넘기도록 한다.</p>
<h2 id="3-productfromjson데이터-모델">3. productFromJson(데이터 모델)</h2>
<pre><code class="language-dart">List&lt;CoffeeProductModel&gt; productFromJson(dynamic str) =&gt;
    List&lt;CoffeeProductModel&gt;.from(
        (str).map((x) =&gt; CoffeeProductModel.fromJson(x)));

//데이터 모델.
//데이터베이스의 구조에 맞추어 변수에 데이터베이스에서 가져온 데이터를 담아줌.
//동일한 구조를 사용하는 테이블은 하나로 묶어 하나의 데이터 모델을 거쳐감.
class CoffeeProductModel {
  int? productId;
  String? productName;
  String? productBrand;
  String? productPrice;
  String? productImagePath;

  CoffeeProductModel(this.productId, this.productName, this.productBrand,
      this.productPrice, this.productImagePath);

  CoffeeProductModel.fromJson(Map&lt;String, dynamic&gt; json) {
    productId = json[&#39;product_id&#39;];
    productName = json[&#39;name&#39;];
    productBrand = json[&#39;brand&#39;];
    productPrice = json[&#39;price&#39;];
    productImagePath = json[&#39;image_path&#39;];
  }

  Map&lt;String, dynamic&gt; toJson() =&gt; {
        &quot;product_id&quot;: productId.toString(),
        &quot;name&quot;: productName,
        &quot;brand&quot;: productBrand,
        &quot;price&quot;: productPrice,
        &quot;image_path&quot;: productImagePath
      };
}</code></pre>
<p><strong>productFromJson</strong> 은 모델링을 거쳐 flutter의 UI에 쓸수 있도록 변수의 담긴 데이터를 다시 받아오는 리스트 인자값을 가진 메소드다.</p>
<p><strong>CoffeeProductModel</strong> 으로 명명한 데이터 모델이 호출받은 api 의 json 값을 각각 받아와 모델에 선언된 변수에 담아주도록 한다.</p>
<p>업데이트나 인서트를 위해 데이터를 다시 넘길 경우에는 <strong>toJson</strong> 을 통해 그 반대의 기능을 한다.</p>
<p><strong>위에서 테이블을 같은 구조로 작성한 이유가 바로 여기에 있다.</strong> 세 브랜드 상품 테이블 모두 동일한 json 구조를 가지고 있으니, 하나의 모델로 세 개의 테이블의 데이터를 변수에 담아줄수 있다.</p>
<blockquote>
<p>일상의 언어로 비유하자면, 우리는 <strong>네모난 모양의 그릇 하나(Product model)</strong> 만 가지고 있는 상태이고, <strong>세 가지 다른 맛의 식빵을(세 개의 브랜드 상품 데이터 테이블)</strong> 그릇의 모양으로 잘 잘라내어 하나의 그릇으로도 모두 담을수 있도록 하는 것이다.
각자 맞는 모양의 그릇을 세 개 다 따로 준비할 수도 있겠다만, <strong>어차피 식빵은 네모낳고(데이터 테이블 구조가 다 같음)</strong>, 반복되는 일은 귀찮지 않는가?</p>
</blockquote>
<h2 id="4-flutter-ui에-출력하기">4. Flutter UI에 출력하기</h2>
<p>데이터를 호출했고, 그걸 데이터 모델로 받아주었다. 그럼 이제 이걸 UI에 활용해보자.</p>
<p><strong>Contorller</strong></p>
<pre><code class="language-dart">class CoffeeController extends GetxController
    with StateMixin&lt;List&lt;CoffeeProductModel&gt;&gt; {
  //페이지 인덱스
  final RxInt selectedIndex = 0.obs;

  //프로바이더 선언. 구조를 공유하는 테이블들은 같은 프로바이더 사용.
  ProductProvider productProvider = ProductProvider();

  void changeIndex(int index) {
    selectedIndex(index);
  }

  //브랜드 버튼을 순서대로 나열, 브랜드 버튼을 누르면 선택 인덱스의 값이 바뀌며 페이지 전환.
  //동시에 프로바이더의 json 호출 메소드를 호출하여 데이터를 호출함.
  isBrand0IndexClicked() {
    selectedIndex.value = 0;
    print(selectedIndex.value);
    productProvider.getStbProductData().then((response) {
      change(response, status: RxStatus.success());
      print(response);
    }, onError: (e) {
      change(null, status: RxStatus.error(e.toString()));
    });
  }

  isBrand1IndexClicked() {
    selectedIndex.value = 1;
    print(selectedIndex.value);
    productProvider.getTwsProductData().then((response) {
      change(response, status: RxStatus.success());
      print(response);
    }, onError: (e) {
      change(null, status: RxStatus.error(e.toString()));
    });
  }

  isBrand2IndexClicked() {
    selectedIndex.value = 2;
    print(selectedIndex.value);
    productProvider.getYdyProductData().then((response) {
      change(response, status: RxStatus.success());
      print(response);
    }, onError: (e) {
      change(null, status: RxStatus.error(e.toString()));
    });
  }

  isBrand3IndexClicked() {
    selectedIndex.value = 3;
    print(selectedIndex.value);
    productProvider.getStbProductData().then((response) {
      change(response, status: RxStatus.success());
      print(response);
    }, onError: (e) {
      change(null, status: RxStatus.error(e.toString()));
    });
  }

  @override
  void onInit() {
    super.onInit();
    productProvider.getStbProductData().then((response) {
      change(response, status: RxStatus.success());
      print(response);
    }, onError: (e) {
      change(null, status: RxStatus.error(e.toString()));
    });
  }

  @override
  void onReady() {
    super.onReady();
  }

  @override
  void onClose() {
    super.onClose();
  }
}</code></pre>
<p><strong>View</strong></p>
<pre><code class="language-dart">class CoffeeView extends GetView&lt;CoffeeController&gt; {
  const CoffeeView({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    Get.put(CoffeeController());

    final List&lt;Widget&gt; coffeePages = [
      CoffeeBrandStbView(),
      CoffeeBrandTwsView(),
      CoffeeBrandYdyView(),
    ];

    return Scaffold(
      backgroundColor: bgColor,
      appBar: AppBar(
        iconTheme: IconThemeData(color: textDark),
        title: Text(
          &#39;Coffee&#39;,
          style: TextStyle(
              fontFamily: &#39;LS&#39;,
              fontSize: 30,
              fontWeight: FontWeight.w700,
              color: accentYellow),
        ),
        centerTitle: false,
        elevation: 0,
        backgroundColor: bgColor,
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Expanded(
            flex: 1,
            child: ProfileNCash(),
          ),
          SizedBox(
            height: 2,
            width: double.infinity,
            child: Container(
              color: Colors.grey.shade300,
            ),
          ),
          Expanded(
            flex: 2,
            child: Padding(
              padding: const EdgeInsets.all(18.0),
              child: ListView(
                scrollDirection: Axis.horizontal,
                children: [
                  BrandTile(
                    brandChild: CircleAvatar(
                      backgroundColor: Colors.green,
                      radius: 20,
                    ),
                    brandName: &#39;STB&#39;,
                    onTap: () =&gt; controller.isBrand0IndexClicked(),
                  ),
                  BrandTile(
                    brandChild: CircleAvatar(
                      backgroundColor: Colors.red,
                      radius: 20,
                    ),
                    brandName: &#39;TWS&#39;,
                    onTap: () =&gt; controller.isBrand1IndexClicked(),
                  ),
                  BrandTile(
                    brandChild: CircleAvatar(
                      backgroundColor: Colors.purple,
                      radius: 20,
                    ),
                    brandName: &#39;YDY&#39;,
                    onTap: () =&gt; controller.isBrand2IndexClicked(),
                  ),
                ],
              ),
            ),
          ),
          Expanded(
            flex: 11,
            child: Obx(
              () =&gt; SafeArea(
                child: coffeePages[controller.selectedIndex.value],
              ),
            ),
          ),
        ],
      ),
    );
  }
}</code></pre>
<p><strong>UI</strong>
<img src="https://velog.velcdn.com/images/dc143c_dev/post/6e292bbc-5c74-4cab-8b48-bc75c6e2e738/image.png" alt=""></p>
<p><strong><a href="https://velog.io/@dc143c_dev/Flutter-%EB%A7%8C%EB%B3%B4%EA%B8%B0-%EC%95%B1-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-1-%EC%BB%A4%EC%8A%A4%ED%85%80-%EB%A7%8C%EB%B3%B4%EA%B8%B0-UI-%EB%A7%8C%EB%93%A4%EA%B8%B0-PageView-Controller-getX-get-storage-color-picker-%ED%99%9C%EC%9A%A9">첫 게시글의 사전 준비 목차</a></strong> 에서 보여주었던 getX 패턴을 사용한 바텀 네비게이션 바와 비슷하게 페이지를 구성했다.</p>
<blockquote>
<p>뷰에서는 브랜드를 선택하는 UI와 그걸 인덱스로 받아주는 컨트롤러로만 구성하고, 화면 중심에 표시되는 페이지는 따로따로 위젯을 만들어 페이지 리스트에 넣는 방식.</p>
</blockquote>
<p>컨트롤러 현재 페이지를 감지할 인덱스 값과, 뷰의 구성이 List로 정의된 세 개의 브랜드 상품 페이지와 그걸 받아주는 SafeArea로 구성된 이유도 이것 때문.</p>
<p>여하튼 중요한 건, 컨트롤러 부분의 <strong>isBrandNIndexClicked</strong>이다.</p>
<pre><code class="language-dart">//동일한 소스 잘라서 보기.
isBrand0IndexClicked() {
    selectedIndex.value = 0;
    print(selectedIndex.value);
    productProvider.getStbProductData().then((response) {
      change(response, status: RxStatus.success());
      print(response);
    }, onError: (e) {
      change(null, status: RxStatus.error(e.toString()));
    });
  }</code></pre>
<p>UI 상단의 브랜드 버튼을 클릭하게 되면, 컨트롤러는 각 순번에 맞는 <strong>isBrandNIndexClicked</strong>를 호출하여 페이지 인덱스를 바꾸고, 동시에 해당 브랜드 상품 데이터를 받아오게 된다.</p>
<pre><code class="language-dart">//ProductProvider의 일부.
Future&lt;List&lt;CoffeeProductModel&gt;?&gt; getStbProductData() async {
    final response = await get(&#39;http://localhost:8000/getStbCoffee&#39;);
    print(response.body);
    if (response.status.hasError) {
      return Future.error({response.statusText});
    } else {
      return productFromJson(response.body);
    }
  }</code></pre>
<p>1번 목차의 프로바이더에서 떼어놓은 <strong>getStbProductData</strong>.</p>
<p>Stb 브랜드의 상품 데이터를 호출하고, 이 데이터를 <strong>productFromJson</strong>으로 넘겨준다.</p>
<p><strong>브랜드 상품 페이지 UI</strong></p>
<pre><code class="language-dart">class CoffeeBrandStbView extends GetView&lt;CoffeeController&gt; {
  const CoffeeBrandStbView({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: bgColor,
      body: SafeArea(
        child: Column(
          children: [
            SizedBox(
              width: double.infinity,
              height: 20,
              child: Container(
                color: Colors.grey.shade300,
                child: Padding(
                  padding: const EdgeInsets.fromLTRB(20, 0, 0, 0),
                  child: Text(
                    &#39;STB&#39;,
                    style:
                        TextStyle(color: textDark, fontWeight: FontWeight.w300),
                  ),
                ),
              ),
            ),
            Expanded(
              child: controller.obx(
                (data) =&gt; ListView.builder(
                  itemCount: data?.length,
                  itemBuilder: (context, index) {
                    //UI 모델에 데이터 세부값을 인덱스로 넘기기
                    var details = data?[index];
                    //디테일 값은 받은 User UI 모델이 아이템 빌더로 빌드됨.
                    return StbUIModel(
                      model: details,
                    );
                  },
                ),
                // onError: (err) =&gt; Text(&#39;e&#39;),
              ),
            ),
          ],
        ),
      ),
    );
  }
}</code></pre>
<p>그 뒤에는 해당 페이지(이 경우에는 Stb 브랜드의 페이지)UI의 빌더 위젯을 controller.obx로 감싸 데이터를 넘겨준다.</p>
<p>빌더 위젯 안에 details 변수를 선언하여 인덱스에 맞는 데이터를 만들어주고, 최종적으로 이를 <strong>UI 모델에 넘겨준다.</strong></p>
<p>그러면 리턴받은 <strong>StbUIModel</strong>의 모양대로 갯수에 맞게 리스트뷰 빌더로 객체가 빌드된다.</p>
<p><strong>UI 모델</strong></p>
<pre><code class="language-dart">class StbUIModel extends StatelessWidget {
  const StbUIModel({Key? key, this.model}) : super(key: key);

  final CoffeeProductModel? model;

  @override
  Widget build(BuildContext context) {
    String baseUrl = &#39;&#39;;

    return Container(
      height: 130,
      width: double.infinity,
      color: bgColor,
      child: Padding(
        padding: EdgeInsets.all(10),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            SizedBox(
              width: 10,
            ),
            Expanded(
              flex: 3,
              child: SizedBox(
                width: 60,
                height: 90,
                child: Container(
                  child: Image.network(
                    &#39;http://localhost:8000/${model!.productImagePath!}&#39;,
                    fit: BoxFit.fill,
                  ),
                ),
              ),
            ),
            SizedBox(
              width: 13,
            ),
            Expanded(
              flex: 7,
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  SizedBox(
                    height: 10,
                  ),
                  Text(
                    model!.productBrand!,
                    style: TextStyle(color: Colors.grey.shade600),
                  ),
                  Text(model!.productName!),
                  SizedBox(
                    height: 3,
                  ),
                  Row(
                    children: [
                      CircleAvatar(
                        radius: 9,
                        backgroundColor: accentYellow,
                        child: Text(
                          &#39;C&#39;,
                          style: TextStyle(
                              fontFamily: &#39;LS&#39;,
                              fontWeight: FontWeight.w700,
                              color: bgColor),
                        ),
                      ),
                      SizedBox(
                        width: 5,
                      ),
                      Text(model!.productPrice!),
                    ],
                  )
                ],
              ),
            ),
            SizedBox(
              width: 20,
            ),
          ],
        ),
      ),
    );
  }
}</code></pre>
<p>리스트뷰에 빌드되는 UI.</p>
<p>이건 정답이 없다. 원하는 디자인으로 데이터가 들어가야 할 곳에 model 변수를 사용하여 데이터를 삽입하자.</p>
<h2 id="5-마무리">5. 마무리</h2>
<p><img src="https://velog.velcdn.com/images/dc143c_dev/post/9f953daa-0ff2-40e6-8596-6decdfc61dc7/image.gif" alt=""></p>
<p>각 브랜드 페이지별로 테이블 데이터가 잘 출력되는 모습.</p>
<p>이번 게시글의 내용은 꽤나 복잡했기 때문에, <del>무엇보다 내가 외우기 힘들기 때문에...</del> 추가로 기능 구현 사이클을 정리해보려 한다.</p>
<p>이 게시글 자체가 우선 내가 메모하고 이해하기 위한 용도인지라, 정리 역시도 나만의 언어로 기술되어서 정석적인 해석과는 거리가 멀다. 유의 바람.</p>
<p><strong>-기능 이름으로 사이클 정리-</strong></p>
<p>해당 기능 구현은 총 다섯가지 파트로 구성되어있다.</p>
<p><strong>ProductProvider</strong> : api 호출 기능을 지님.</p>
<p><strong>ProductModel</strong> : 데이터 모델. json 데이터를 flutter에서 사용할수 있도록 변수로 담아줌. <strong>즉, 백엔드의 데이터를 받아주는 프론트엔드의 그릇.</strong></p>
<p><strong>productFromJson</strong> : 그 그릇에 연결하기 위해 ProductModel을 인자로 받는 리스트 함수. 내 소스 코드에서는 같은 파일에 존재함. <strong>연결고리.</strong></p>
<p><strong>UIModel</strong> : View에 빌드되기 위해 존재하는 재료. UI 디자인을 갖고있고, <strong>ProductModel</strong>의 데이터를 model 변수로 받아와 각 데이터가 필요한 UI에 삽입.</p>
<p><strong>컨트롤러와 뷰로 구성된 UI</strong> : 컨트롤러는 <strong>ProductProvider</strong>를 호출하여 데이터를 받아오고, 뷰는 컨트롤러의.obx로 받아온 데이터를 빌더 위젯에 넘겨서 UI를 구성함.</p>
<p><strong>-유저 입장에서의 사이클-</strong></p>
<p>유저가 페이지 진입 -&gt; </p>
<p>유저가 UI의 페이지를 바꾸면 컨트롤러가 이를 감지, 해당 페이지가 받아야 할 productProvider를 호출 -&gt;</p>
<p>해당 브랜드의 productProvider가 api를 호출. json 데이터로 받아옴 -&gt;</p>
<p>응답받은 json 데이터의 body를 productFromJson을 호출하는 방식으로 데이터 모델에 넘김-&gt;</p>
<p>이 데이터 모델을 변수의 형태로 UI 모델이 받아옴 -&gt;</p>
<p>뷰 UI에서 빌더를 통해 UI모델을 객체로 하여 빌드. 이를 유저가 시인. 완성.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] [GetX] 만보기 앱 개발일지 - 2 | getX 패턴을 사용하여 애니메이션 조작이 필요한 위젯들 만들기]]></title>
            <link>https://velog.io/@dc143c_dev/Flutter-GetX-%EB%A7%8C%EB%B3%B4%EA%B8%B0-%EC%95%B1-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-2.-getX-%ED%8C%A8%ED%84%B4%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EC%A1%B0%EC%9E%91%EC%9D%B4-%ED%95%84%EC%9A%94%ED%95%9C-%EC%9C%84%EC%A0%AF%EB%93%A4-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@dc143c_dev/Flutter-GetX-%EB%A7%8C%EB%B3%B4%EA%B8%B0-%EC%95%B1-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-2.-getX-%ED%8C%A8%ED%84%B4%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EC%A1%B0%EC%9E%91%EC%9D%B4-%ED%95%84%EC%9A%94%ED%95%9C-%EC%9C%84%EC%A0%AF%EB%93%A4-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Tue, 24 Jan 2023 07:41:18 GMT</pubDate>
            <description><![CDATA[<h2 id="0">0.</h2>
<p>getX 패턴, 그리고 getX 라이브러리를 사용하면서 flutter 앱을 만들면 여러가지 장점이 있다.</p>
<p>기존보다 편리한 navigation. 
Get 클래스로 사전 정의된 UI 위젯들(SnackBar, Dialog 등등).
그 밖에도 언어 전환이나, 테마 설정 등의 기능들...</p>
<p>하지만 대다수의 getX 라이브러리를 사용하는 유저들이 가장 학습하기 어려워하고, 또 동시에 만족스러워하면서 가장 필요로 하는 기능은 상태관리 기능이다.</p>
<p>앱의 성능을 향상시키기 위해서 꼭 알아두어야 할 개념 상태관리. </p>
<p>나 역시도 이번 프로젝트 앱의 모든 코드를 getX 패턴을 사용하며 작성하는 중으로, 이번 게시글은 getX 패턴으로 애니메이션의 state를 조작해야 하는 위젯들을 구현해보면서 얻은 경험들을 기술해본다.</p>
<p>해당 게시글의 내용은 기능 구현을 위한 정석적인 방법이 아닐수 있으며, 구현 과정이 미숙하거나 정리되지 않은 코드가 포함될수도 있다.</p>
<p>그럼에도 플러터를 학습하는 누군가와, 얼마 못가 이 내용을 까먹을 미래의 나를 위해 이 게시글을 작성해본다.</p>
<h2 id="1-사전-준비">1. 사전 준비</h2>
<p>우선 당연하게도 getX 라이브러리가 설치되어있어야 한다.</p>
<p>그리고 필자는 UI 구현을 위해 추가로 <strong>flutter_speed_dial</strong> 패키지를 설치해주었다.</p>
<p><img src="https://velog.velcdn.com/images/dc143c_dev/post/91528c17-392d-482d-b98b-aecf24821878/image.gif" alt=""></p>
<p>speed dial 패키지는 위와 같이 기존의 floating action button에 해당 UI를 애니메이션 아이콘을 동반한 메뉴 버튼으로 쓸수 있게 만들어주는 패키지다.</p>
<h2 id="2-tabbar-구현">2. tabBar 구현</h2>
<p>첫번째로 구현해볼 UI는 tabBar.</p>
<p><img src="https://velog.velcdn.com/images/dc143c_dev/post/18490705-7e11-4a33-a9ea-3d914380f73b/image.gif" alt=""></p>
<p>tabBar는 기존의 navigation bar와 비슷하다고 생각하면 쉽다.</p>
<p>다만 필자는 getX 패턴을 적용하여 앱을 작성하는 중으로, 탭바의 state가 변경될때마다 이를 감지하고 UI에 표시해주는 컨트롤러를 따로 만들어주어야 한다.</p>
<pre><code class="language-dart">//탭바를 감싸고있는 friedns view의 UI.
class FriendsView extends GetView&lt;FriendsController&gt; {
  const FriendsView({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    Get.put(FriendsController());
    return Scaffold(
      backgroundColor: bgColor,
      appBar: AppBar(
        bottom: TabBar(
          indicatorColor: accentBrown,
          dividerColor: accentBrown,
          controller: controller.tabController,
          tabs: controller.myTabs,
        ),
        title: Text(
          &#39;Walk&#39;,
          style: TextStyle(
              fontFamily: &#39;LS&#39;,
              fontSize: 30,
              fontWeight: FontWeight.w700,
              color: accentYellow),
        ),
        centerTitle: false,
        elevation: 0,
        backgroundColor: bgColor,
      ),
      body: TabBarView(
        controller: controller.tabController,
        children: [
          FriendsRankingView(),
          FriendsInviteView(),
          FriendsAcceptView(),
        ],
      ),
    );
  }
}</code></pre>
<p>위의 코드는 탭바를 감싸고있는 friedns view의 UI로, 앱바의 bottom 부분에 tabBar의 디자인적 요소와 할당할 컨트롤러를, Scaffold의 body 부분에 TabBarView를 넣어 구현되었다.</p>
<p>TabBarView의 자식들로 들어가있는 View들은 위의 gif에서 보이는 페이지들이다.</p>
<pre><code class="language-dart">class FriendsController extends GetxController
    with GetSingleTickerProviderStateMixin {
  //탭바.

  //탭 클릭 감지시 표시될 탭바 UI.
  final List&lt;Tab&gt; myTabs = &lt;Tab&gt;[
    Tab(
      child: Text(
        &#39;랭킹&#39;,
        style: TextStyle(
            color: textDark,
            fontFamily: &#39;IBMKR&#39;,
            fontSize: 15,
            fontWeight: FontWeight.w700),
      ),
    ),
    Tab(
      child: Text(
        &#39;친구 초대&#39;,
        style: TextStyle(
            color: textDark,
            fontFamily: &#39;IBMKR&#39;,
            fontSize: 15,
            fontWeight: FontWeight.w700),
      ),
    ),
    Tab(
      child: Text(
        &#39;받은 요청&#39;,
        style: TextStyle(
            color: textDark,
            fontFamily: &#39;IBMKR&#39;,
            fontSize: 15,
            fontWeight: FontWeight.w700),
      ),
    ),
  ];

  //탭 컨트롤러 선언
  late TabController tabController;</code></pre>
<p>Friends View에 연결되어있는 Friends Controller의 코드.</p>
<p>FriendsView Appbar의 bottom 부분을 차지하고 있는 TabBar의 controller와 tabs를 이 부분이 담당한다.</p>
<p>List로 선언된 myTabs가 tabBar의 UI 객체가 되고, 탭 컨트롤러를 선언하여 외부에서 사용할수 있도록 한다.</p>
<p>다만 이 컨트롤러는 주의사항이 있는데, <strong>GetXController를 상속하면서 동시에 GetSingleTickerProviderStateMixin를 같이 추가해야만 애니메이션의 동작을 제어할수 있다.</strong></p>
<p>GetSingleTickerProviderStateMixin은 기존의 SingleTickerProviderStateMixin와 비슷한 역할을 한다.</p>
<h2 id="3-speed-dial-구현">3. speed dial 구현</h2>
<p>이제 맨 위에서 보았던 speed dial을 구현해볼 차례다.</p>
<pre><code class="language-dart">class FriendsView extends GetView&lt;FriendsController&gt; {
  const FriendsView({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    Get.put(FriendsController());
    Get.lazyPut(() =&gt; FriendsDialController());

    FriendsDialController friendsDialController = FriendsDialController();

    return Scaffold(
      backgroundColor: bgColor,
      floatingActionButton: SpeedDial(
        openCloseDial: friendsDialController.isDialOpen,
        onOpen: () {},
        onClose: () {
          friendsDialController.isDialBtnClosed();
        },
        onPress: () {
          friendsDialController.isDialBtnClicked();
        },
        child: AnimatedIcon(
          icon: AnimatedIcons.menu_close,
          progress: friendsDialController.animationController,
        ),
        backgroundColor: accentYellow,
        //dial item 들.
        //클릭시 해당 함수에 해당하는 디알로그 출력.
        children: [
          SpeedDialChild(
              child: Icon(
                Icons.add,
                color: accentBrown,
              ),
              label: &#39;친구 추가하기&#39;,
              onTap: () {
                friendsDialController.isAddBtnClicked();
              }),
          SpeedDialChild(
              child: Icon(
                Icons.search_rounded,
                color: accentBrown,
              ),
              label: &#39;친구 검색&#39;,
              onTap: () {
                friendsDialController.isSearchBtnClicked();
              }),
          SpeedDialChild(
              child: Icon(
                Icons.people_rounded,
                color: accentBrown,
              ),
              label: &#39;친구 관리&#39;,
              onTap: () {
                friendsDialController.isControlBtnClicked();
              }),
          SpeedDialChild(
              child: Icon(
                Icons.share,
                color: accentBrown,
              ),
              label: &#39;SNS 연동&#39;,
              onTap: () {
                friendsDialController.isSNSBtnClicked();
              }),
        ],
      ),
      appBar: AppBar(
        bottom: TabBar(
          indicatorColor: accentBrown,
          dividerColor: accentBrown,
          controller: controller.tabController,
          tabs: controller.myTabs,
        ),
        title: Text(
          &#39;Walk&#39;,
          style: TextStyle(
              fontFamily: &#39;LS&#39;,
              fontSize: 30,
              fontWeight: FontWeight.w700,
              color: accentYellow),
        ),
        centerTitle: false,
        elevation: 0,
        backgroundColor: bgColor,
      ),
      body: TabBarView(
        controller: controller.tabController,
        children: [
          FriendsRankingView(),
          FriendsInviteView(),
          FriendsAcceptView(),
        ],
      ),
    );
  }
}</code></pre>
<p>이 FriendsView는 위의 TabBar 구현 방법을 설명할때 보았던 것과 같은 소스다.</p>
<p>다만 다른 점은 Scaffold 의 floating action button이 있어야 할 자리에 Speed dial이 사용되었다.</p>
<p>Get.lazyPut으로 speed dial을 조작할 FriendsDialController를 추가로 삽입했으며, 컨트롤러를 사용하기 쉽도록 friendsDialController라는 이름으로 다시 선언해주었다.</p>
<p>즉, FriendsView는 두개의 컨트롤러를 사용한다.</p>
<p>후술할 friendsDialController는 다이얼이 오픈되었을 때, 닫혔을 때, 클릭되었을때의 기능들, 그리고 애니메이션의 progress를 담고있다.</p>
<p>speed dial의 자식들로 들어가있는 SpeedDialChild는 다이얼을 눌렀을때 위에 표시되는 메뉴 버튼들이며, UI 정보와 onTap 실행할 메소드를 담고있다.</p>
<p>onTap시 실행될 메소드들은 friendsDialController에 선언되어있다.</p>
<pre><code class="language-dart">class FriendsDialController extends GetxController
    with GetSingleTickerProviderStateMixin {
  //초기값이 없어 발생하는 빌드 오류를 막기 위해 초기값을 미리 선언.
  //왜인지 onInit 에 넣었을때는 작동하지 않았음. 확인 필요.
  late AnimationController animationController =
      AnimationController(vsync: this, duration: Duration(seconds: 1));

  //speed dial 버튼 클릭시 감지 노티파이어.
  //애니메이션과 무관. 다이알 올라오는 기능 감지용.
  ValueNotifier&lt;bool&gt; isDialOpen = ValueNotifier(false);

  var isPlaying = false.obs;

  //다이알 버튼 클릭시 선언된 애니메이션 컨트롤러 작동. 동시에 다이알 올라옴.
  isDialBtnClicked() {
    isDialOpen.value = true;
    animationController.forward();
  }

  //다이알 버튼 종료시 애니메이션 닫힘. 동시에 다이알 내려옴.
  isDialBtnClosed() {
    isDialOpen.value = false;
    animationController.reverse();
  }

  isAddBtnClicked() {
    Get.dialog(
      CustomDialog(
        titleText: &#39;  친구 추가하기&#39;,
        dialogContent: Column(
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                Expanded(
                  flex: 8,
                  child: TextField(
                    keyboardType: TextInputType.number,
                    decoration: InputDecoration(
                      focusedBorder: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(30),
                          borderSide: BorderSide(color: accentYellow)),
                      focusColor: accentYellow,
                      fillColor: Colors.grey,
                      hintText: &#39;추천코드로 친구를 찾아보세요&#39;,
                      hintStyle: TextStyle(
                        fontSize: 16,
                      ),
                      border: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(30),
                      ),
                    ),
                  ),
                ),
                SizedBox(
                  width: 5,
                ),
                Expanded(
                  flex: 2,
                  child: SizedBox(
                    height: 55,
                    width: 50,
                    child: ElevatedButton(
                      style: ElevatedButton.styleFrom(
                        backgroundColor: accentYellow,
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(30.0),
                        ),
                      ),
                      onPressed: () {},
                      child: Icon(Icons.search),
                    ),
                  ),
                ),
              ],
            ),
            SizedBox(
              height: 50,
            ),
            Text(
              &#39;친구하고 싶은 사용자를 추천코드로 검색합니다.\n            이미 친구인 사용자를 검색하고\n        싶다면 아래의 버튼을 선택해주세요&#39;,
              style: TextStyle(fontSize: 13, color: Colors.grey.shade700),
            ),
            SizedBox(
              height: 50,
            ),
            CustomButtonYellow(
              btnText: &#39;친구 검색하러 가기&#39;,
              onPressed: () {},
            ),
            SizedBox(
              height: 250,
            ),
            SizedBox(
              width: 250,
              child: CustomButtonBrown(
                btnText: &#39;확인&#39;,
                onPressed: () {
                  Get.back();
                },
              ),
            ),
          ],
        ),
      ),
    );
  }

  isSearchBtnClicked() {
    Get.dialog(
      CustomDialog(
        titleText: &#39;친구 검색하기&#39;,
        dialogContent: Column(
          children: [],
        ),
      ),
    );
  }

  isControlBtnClicked() {
    Get.dialog(
      CustomDialog(
        titleText: &#39;친구 관리하기&#39;,
        dialogContent: Column(
          children: [],
        ),
      ),
    );
  }

  isSNSBtnClicked() {
    Get.dialog(
      CustomDialog(
        titleText: &#39;SNS 연동하기&#39;,
        dialogContent: Column(
          children: [],
        ),
      ),
    );
  }
}</code></pre>
<p>다이얼의 메뉴 버튼들을 눌렀을때 나타나는 UI는 Get.dialog를 통해 만들었다.</p>
<p>CustomDialog는 필자가 AlertDialog를 리턴하여 만든 커스텀 UI이며, 이는 추후에 따로 서술하겠다. </p>
<p>지금은 그냥 미리 UI가 만들어져있는 AlertDialog로 생각해도 좋다.</p>
<p>중요하게 봐야할 포인트는 <strong>AnimationController</strong>인데, 코드의 최상단에 animationController의 기본값들을 미리 선언해둔다.</p>
<p>나는 애니메이션의 듀레이션을 1초로 설정해두었다.</p>
<p>다이얼이 오픈되거나 닫힐때 호출되는 메소드에 animation 컨트롤러를 호출하면서, 오픈되었을때는 <strong>forward</strong>로 애니메이션을 재생하며, 닫힐때는 <strong>reverse</strong>로 애니메이션을 역재생한다.</p>
<h2 id="4-결과물">4. 결과물</h2>
<p><img src="https://velog.velcdn.com/images/dc143c_dev/post/dc7db4d7-9d84-4439-93a6-c6d62cc183da/image.gif" alt=""></p>
<p>speed dial의 애니메이션 아이콘이 잘 작동하는 모습.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] [GetX] 만보기 앱 개발일지 - 1 | 커스텀 만보기 UI 만들기]]></title>
            <link>https://velog.io/@dc143c_dev/Flutter-%EB%A7%8C%EB%B3%B4%EA%B8%B0-%EC%95%B1-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-1-%EC%BB%A4%EC%8A%A4%ED%85%80-%EB%A7%8C%EB%B3%B4%EA%B8%B0-UI-%EB%A7%8C%EB%93%A4%EA%B8%B0-PageView-Controller-getX-get-storage-color-picker-%ED%99%9C%EC%9A%A9</link>
            <guid>https://velog.io/@dc143c_dev/Flutter-%EB%A7%8C%EB%B3%B4%EA%B8%B0-%EC%95%B1-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-1-%EC%BB%A4%EC%8A%A4%ED%85%80-%EB%A7%8C%EB%B3%B4%EA%B8%B0-UI-%EB%A7%8C%EB%93%A4%EA%B8%B0-PageView-Controller-getX-get-storage-color-picker-%ED%99%9C%EC%9A%A9</guid>
            <pubDate>Wed, 18 Jan 2023 15:17:44 GMT</pubDate>
            <description><![CDATA[<h2 id="0">0.</h2>
<p>포트폴리오용 만보기 앱을 만들다가 구현하고픈 아이디어가 떠올랐다.</p>
<p>대부분의 만보기 앱은 유저가 가장 자주 보게 될 홈 화면에 유저가 걸어온 총 걸음의 수와 목표 걸음 수 등을 표시하곤 한다.</p>
<p>경우에 따라 프로필 사진 또는 인증샷을 첨부할수 있게 하거나, 진행도를 그래프로 보여주는 UI를 추가하는 경우도 더러 있다.</p>
<p><img src="https://velog.velcdn.com/images/dc143c_dev/post/8609a0bd-588f-4e91-a058-1eafbb9f5fe1/image.png" alt=""></p>
<blockquote>
<p><strong>이번에 직접 구현해본 UI.
 총 걸음 수 인디케이터, 목표 걸음 수 인디케이터, 표시용 텍스트로 구성되어있다.</strong></p>
</blockquote>
<p>구현하고픈 아이디어라는건 저 인디케이터 UI의 색상을 유저가 마음대로 커스텀할수 있게 만드는 것.</p>
<p>정확한 목표는 여러가지 디자인의 인디케이터 UI를 미리 구현해두고, 유저가 커스텀 페이지에서 걸음 수, 포인트 인디케이터와 텍스트의 색상을 설정하면 UI에 즉각적으로 적용되도록 하는 것이다. </p>
<p>더불어 유저가 커스텀한 색상들의 값을 로컬에 저장하고, 추후에는 이 데이터를 백엔드와도 연동해보려 한다.</p>
<p><strong>구현 과정에서는 getX 패턴과 get storage를 활용한다.</strong></p>
<p>이 글은 그것을 구현하는 과정으로서 지식 공유의 목적이라기보단, 필자의 개발 기록 보관용으로 작성되었다.</p>
<p>당연히 기능 구현을 위한 정석적인 방법이 아니며, 미숙한 코드가 많을수도 있다. </p>
<p>그럼에도 구현에 필요한 기능들을 함께 기술하여, 이 글이 flutter를 학습하는 누군가와, 얼마 못가 오늘 공부한 내용을 까먹고 벨로그를 뒤적거릴 몇주 뒤의 나에게 도움이 되길 바래본다.</p>
<p><strong>참고로 이 결과물에선 pedometer 패키지를 사용하지 않았다. 에뮬레이터 관련 이슈를 당장 해결할수 없고(실물 기기와 개발자용 계정 없음...), 테스트의 용이성 때문. 그래서 당장의 만보기 기능은 유저가 1초마다 한번씩 걷는 것으로 상정하여, 걸음 수를 Timer의 초당 값이 변하는 걸로 대체한다.</strong></p>
<h2 id="1-사전-준비">1. 사전 준비</h2>
<p>우선 당연하게도 get, get storage 패키지가 설치되어있어야 한다.</p>
<p>필자는 추가로 UI 구현을 위해 step_progress_indicator 패키지도 함께 설치했다.</p>
<p>step_progress_indicator는 좀더 다양한 디자인의 progress indicator를 사용할수 있게 해주는 패키지다.</p>
<pre><code class="language-dart">///main.dart
Future main() async {
  await GetStorage.init();
  await GetStorage().read(&#39;mainPageImageIndex&#39;);
  await GetStorage().read(&#39;totalColor&#39;);
  await GetStorage().read(&#39;walkColor&#39;);
  await GetStorage().read(&#39;textColor&#39;);
  print(GetStorage().read(&#39;totalColor&#39;).toString());
  print(GetStorage().read(&#39;test&#39;));
  runApp(
    GetMaterialApp(
      debugShowCheckedModeBanner: false,
      title: &quot;Application&quot;,
      initialRoute: AppPages.INITIAL,
      getPages: AppPages.routes,
    ),
  );
}</code></pre>
<p>우선 앱이 시작되면 호출될 main. GetStorage를 이용하여 각 키값에 저장된 value를 불러온다.</p>
<p>이는 비동기 방식으로 처리되며, value를 불러와야만 main이 실행되는 구조.</p>
<p>우리는 getX 패턴을 사용하므로, getX의 다양한 기능을 사용하기 위해 <strong>MaterialApp이 아닌 GetMaterialApp을 사용한다.</strong></p>
<p>이후 AppPages.에 작성된 INITIAL 변수에 해당하는 페이지로 라우팅된다.</p>
<pre><code class="language-dart">//../routes/app_pages.dart
class AppPages {
  AppPages._();

  static const INITIAL = Routes.HOME;

  static final routes = [
    GetPage(
      name: _Paths.HOME,
      page: () =&gt; const HomeView(),
      binding: HomeBinding(),
    ),
    GetPage(
      name: _Paths.WALK,
      page: () =&gt; const WalkView(),
      binding: WalkBinding(),
    ),
    GetPage(
      name: _Paths.FRIENDS,
      page: () =&gt; const FriendsView(),
      binding: FriendsBinding(),
    ),
    GetPage(
      name: _Paths.HEALTH,
      page: () =&gt; const HealthView(),
      binding: HealthBinding(),
    ),
    GetPage(
      name: _Paths.COMMUNITY,
      page: () =&gt; const CommunityView(),
      binding: CommunityBinding(),
    ),
    GetPage(
      name: _Paths.NEWS,
      page: () =&gt; const NewsView(),
      binding: NewsBinding(),
    ),
    GetPage(
      name: _Paths.MORE,
      page: () =&gt; const MoreView(),
      binding: MoreBinding(),
    ),
    GetPage(
      name: _Paths.MY_BOTTOM_NAV_BAR,
      page: () =&gt; const MyBottomNavBarView(),
      binding: MyBottomNavBarBinding(),
    ),
    GetPage(
      name: _Paths.CAMERA,
      page: () =&gt; CameraView(),
      binding: CameraBinding(),
    ),
  ];
}</code></pre>
<p>GetPage를 활용한 라우트 관리.</p>
<p>필자는 get_cli를 활용하여 자동적으로 생성했다. get_cli에 관한 내용은 추후에 따로 작성할 예정.</p>
<p>해당 소스의 INITAL page는 home_view 로 연결된다.</p>
<pre><code class="language-dart">// ./lib/app/modules/home/views/home_view/dart
class HomeView extends GetView&lt;HomeController&gt; {
  const HomeView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    Get.put(MyBottomNavBarController());
    final List&lt;Widget&gt; pages = [
      WalkView(),
      FriendsView(),
      HealthView(),
      CommunityView(),
      NewsView(),
    ];

    return Scaffold(
      body: Obx(
        () =&gt; SafeArea(
            child: pages[MyBottomNavBarController.to.selectedIndex.value]),
      ),
      bottomNavigationBar: MyBottomNavBarView(),
    );
  }
}</code></pre>
<p>이니셜 라우트로 연결된 홈뷰.</p>
<p>사실상 이름만 homeView이고, 실질적인 기능은 바텀 네비게이션 바의 역할을 한다.</p>
<p>따라서 별도의 앱바와 UI가 없고, pages 리스트에 네비게이션 바로 표시할 페이지들을 넣어놓고, homeView의 UI로는 바텀 네비게이션 바만 표시한다.
<img src="https://velog.velcdn.com/images/dc143c_dev/post/f446d771-802a-4344-ad58-7d9cf985c61f/image.png" alt=""></p>
<p>바로 이 부분.</p>
<pre><code class="language-dart">// ./lib/app/modules/my_bottom_nav_bar/views/my_bottom_nav_bar_view
class MyBottomNavBarView extends GetView&lt;MyBottomNavBarController&gt; {
  const MyBottomNavBarView({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Obx(
      () =&gt; BottomNavigationBar(
        currentIndex: controller.selectedIndex.value,
        onTap: controller.changeIndex,
        selectedItemColor: accentYellow,
        unselectedItemColor: mainGrey,
        unselectedLabelStyle: TextStyle(fontSize: 10),
        selectedLabelStyle:
            TextStyle(fontFamily: &#39;LS&#39;, fontWeight: FontWeight.w700),
        backgroundColor: bgColor,
        items: &lt;BottomNavigationBarItem&gt;[
          BottomNavigationBarItem(
              icon: controller.selectedIndex.value == 0
                  ? Icon(
                      Icons.home,
                      color: accentYellow,
                    )
                  : Icon(
                      Icons.home,
                      color: mainGrey,
                    ),
              label: &quot;Home&quot;),
          BottomNavigationBarItem(
              icon: controller.selectedIndex.value == 1
                  ? Icon(
                      Icons.people,
                    )
                  : Icon(
                      Icons.people,
                    ),
              label: &quot;Friends&quot;),
          BottomNavigationBarItem(
              icon: controller.selectedIndex.value == 2
                  ? Icon(
                      Icons.health_and_safety,
                      color: accentYellow,
                    )
                  : Icon(
                      Icons.health_and_safety,
                      color: mainGrey,
                    ),
              label: &quot;Health&quot;),
          BottomNavigationBarItem(
              icon: controller.selectedIndex.value == 3
                  ? Icon(
                      Icons.comment_rounded,
                      color: accentYellow,
                    )
                  : Icon(
                      Icons.comment_rounded,
                      color: mainGrey,
                    ),
              label: &quot;Community&quot;),
          BottomNavigationBarItem(
              icon: controller.selectedIndex.value == 4
                  ? Icon(
                      Icons.newspaper,
                      color: accentYellow,
                    )
                  : Icon(
                      Icons.newspaper,
                      color: mainGrey,
                    ),
              label: &quot;News&quot;),
        ],
      ),
    );
  }
}</code></pre>
<p>바텀 네비게이션 바의 UI 부분.</p>
<p>BottomNavigationBarItem으로 아이템들을 표시하며, getX contoller를 사용하여 선택했을 때와 선택하지 않았을 때의 아이콘을 조정할수 있도록 만들었다.</p>
<pre><code class="language-dart">// ./lib/app/modules/my_bottom_nav_bar/controllers/my_bottom_nav_bar_controller
class MyBottomNavBarController extends GetxController {
  static MyBottomNavBarController get to =&gt; Get.find();

  final RxInt selectedIndex = 0.obs;

  void changeIndex(int index) {
    selectedIndex(index);
  }

  @override
  void onInit() {
    super.onInit();
  }

  @override
  void onReady() {
    super.onReady();
  }

  @override
  void onClose() {
    super.onClose();
  }
}</code></pre>
<p>selectedIndex 변수를 .obs 형태로 만들어, 값이 변할때마다 감지할수 있도록 컨트롤러에 미리 작성해두었다.</p>
<h2 id="2-기능-구현">2. 기능 구현</h2>
<p>이제 기본적인 UI를 담을 틀은 만들어졌으니, 상단에 올렸던 화면처럼 커스텀 인디케이터의 기능과 UI를 구현해보자.</p>
<p><strong>디자인적 요소나 미구현된 기능은 생략하고, 코드의 기능들을 각 코드블럭의 주석에 서술한다.</strong></p>
<h3 id="walk_viewdart">walk_view.dart</h3>
<p>이번 게시글에서 주로 다룰 파트이자, 바텀 네비게이션 바의 첫번째 아이템이다.</p>
<p>UI상에는 home으로 표기되지만, 소스는 이것이다. 사실상 walk_view가 홈 화면이다.</p>
<pre><code class="language-dart">// ./lib/app/modules/walk/views/walk_view
class WalkView extends GetView&lt;WalkController&gt; {
  const WalkView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    Get.put(WalkController());
    Get.put(CameraController());

    CameraController cameraController = CameraController();
    return Scaffold(
      backgroundColor: bgColor,
      appBar: AppBar(
        title: Text(
          &#39;Walk&#39;,
          style: TextStyle(
              fontFamily: &#39;LS&#39;,
              fontSize: 30,
              fontWeight: FontWeight.w700,
              color: accentYellow),
        ),
        centerTitle: false,
        elevation: 0,
        backgroundColor: bgColor,
        actions: [
          IconButton(
            onPressed: () {
              //아직 미구현
            },
            icon: Icon(
              Icons.add_alert,
              color: accentYellow,
            ),
          ),
          IconButton(
            onPressed: () {
              //아직 미구현
            },
            icon: Icon(
              Icons.settings,
              color: accentYellow,
            ),
          ),
        ],
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [
            SizedBox(
              height: 15,
            ),
            Container(
              child: Stack(
                children: [
                  Obx(
                    () =&gt; Container(
                      margin: EdgeInsets.all(10),
                      decoration: BoxDecoration(
                          image: DecorationImage(
                              fit: BoxFit.fill,
                              //카메라 컨트롤러의 selectedImagePath 스트링 값을 가져와 에셋이미지 패스 연결.
                              image: AssetImage(
                                  &#39;assets/images/${controller.imagePath}.png&#39;)),
                          color: bgColor,
                          shape: BoxShape.circle,
                          boxShadow: [
                            BoxShadow(
                                color: mainGrey,
                                offset: Offset(0.0, 1.0),
                                blurRadius: 6.0),
                          ]),
                      height: 350,
                      width: 500,
                      child: PageView(
                        controller: controller.indicatorController,
                        onPageChanged: controller.onIndicatorPageChanged,
                        children: [
                          //Indicator
                          IndicatorCircularView(),
                          IndicatorStepView(),
                          IndicatorCircularBView(),
                          IndicatorStepBView(),
                        ],
                      ),
                    ),
                  ),
                  Row(
                    children: [
                      SizedBox(
                        width: 15,
                      ),
                      FloatingActionButton.small(
                        heroTag: &#39;goToCamera&#39;,
                        backgroundColor: accentBrown,
                        onPressed: () {
                          controller.CameraBtnClicked();
                        },
                        child: Icon(Icons.camera),
                      ),
                      SizedBox(
                        width: 265,
                      ),
                      FloatingActionButton.small(
                        backgroundColor: accentBrown,
                        onPressed: () {
                          controller.shareBtnClicked();
                        },
                        child: Icon(Icons.share),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}</code></pre>
<h3 id="walk_controllerdart">walk_controller.dart</h3>
<p>walkView의 변화하는 변수들과 기능들을 담당할 walkController.</p>
<pre><code class="language-dart">// ./lib/app/modules/walk/controllers/walk_controller
//--사이클--
//Controller 빌드 시 onInit 실행=&gt;
//onInit 으로 initPageValue 함수, startWalk 함수 호출=&gt;
//if 문으로 storage 값이 존재할시 그대로 적용하고, null 이라면 기본 색상 및 기본 인덱스 적용=&gt;
//동시에 startWalk 실행됨=&gt;
//초마다 포인트 상승. 초마다 storage 의 value 를 listenKey 로 감지=&gt;
//CameraController 에서 storage 의 value 를 수정하면 즉각 적용.
class WalkController extends GetxController {
  static WalkController get to =&gt; Get.find();
  //get storage 사용하기 쉽게 미리 선언.
  final storage = GetStorage();

  //camera controller 쪽에서 넘길 인덱스를 받아줄 그릇.
  RxString imagePath = &#39;0&#39;.obs;

  //camera controller 쪽에서 넘길 세가지 색상 값을 받아줄 그릇들.
  //아래 타이머 위젯으로 1초마다 감지하여 실시간으로 변함.
  //storage 에 저장될 값은 Color() 가 아닌 int 이기에 우선 0으로 지정.
  var currentTotalColor = 0.obs;
  var currentWalkColor = 0.obs;
  var currentTextColor = 0.obs;

  //순서대로 블루 핑크 옐로우.
  //아래 initPage 에서 null 값을 체크하여 true 일 경우 반한될 기본 색상.
  //current~ 컬러들이 int 값이기에, 값을 받아주려면 똑같이 int 여야함. 따라서 int color 값.
  var initTotalColor = 0xFf4169e1.obs;
  var initWalkColor = 0xFFff69b4.obs;
  var initTextColor = 0xffffffff.obs;

  //100걸음과 총 걸음수 최대값.
  //목업이기에 사용하는 것이지, 실제로는 유저가 맥스값을 설정할수 있게 해야함.
  final walk100maxSecond = 100;
  final walkTotalMax = 1000;

  //인디케이터 index
  var indicatorIndex = 0.obs;
  var currentIndicator = 0.obs;

  final walk100maxSecondSplit5 = 20;
  final walkTotalMaxSplit5 = 30;

  final indicatorController = PageController(initialPage: 0, keepPage: true);

  //아래 타이머로 변화될 값들. 이 값들이 circular indicator 로 전해져서 ui의 애니메이션처럼 표현.
  RxInt walk100 = 0.obs;
  RxInt walkTotal = 0.obs;
  RxInt pointCount = 0.obs;
  // /5
  RxInt walk100s5 = 0.obs;
  RxInt walkTotals5 = 0.obs;

  @override
  void onInit() {
    super.onInit();
    //위젯 시작시 페이지 이미지 및 색상 데이터 가져옴.
    initPageValue();
    storage.read(&#39;test&#39;);
    //동시에 두가지 만보기 타이머 시작.
    startWalk();
    startPoint();
    // /5
    startSplit5();
    startSplitTotal5();
    startSplit5Cut();
  }

  @override
  void onReady() {
    super.onReady();
  }

  @override
  void onClose() {
    super.onClose();
  }

  //위에서 호출한 메소드. 이미지 인덱스를 get storage 에서 읽어옴.
  //camera controller 에서 get storage 에 저장한 인덱스 값.
  //storage 의 값들을 체크하여 값이 null 일 경우 기본 값으로 저장한 값들로 변경.
  initPageValue() {
    imagePath.value = storage.read(&#39;mainPageImageIndex&#39;);

    var getTotalColor = storage.read(&#39;totalColor&#39;);

    if (getTotalColor == null) {
      currentTotalColor.value = initTotalColor.value;
    } else {
      currentTotalColor.value = getTotalColor;
    }

    var getWalkColor = storage.read(&#39;walkColor&#39;);

    if (getWalkColor == null) {
      currentWalkColor.value = initWalkColor.value;
    } else {
      currentWalkColor.value = getWalkColor;
    }

    var getTextColor = storage.read(&#39;textColor&#39;);

    if (getTextColor == null) {
      currentTextColor.value = initTextColor.value;
    } else {
      currentTextColor.value = getTextColor;
    }
    print(&#39;init&#39;);
  }

  onIndicatorPageChanged(int page) {
    print(&#39;page num:&#39; + page.toString());
    int currentIndicator = page.toInt();
    //페이지가 바뀔때마다 호출. 즉, galleryPageIndex 가 현재 페이지를 담게 됨.
    indicatorIndex.value = currentIndicator;
    print(currentIndicator);
  }

  //공유 버튼 클릭시 호출.
  shareBtnClicked() {
    Get.bottomSheet(
      Padding(
        padding: const EdgeInsets.all(10.0),
        child: Column(
          children: [
            Center(
              child: Text(
                &#39;Bottom Sheet&#39;,
                style: TextStyle(fontSize: 18),
              ),
            ),
            OutlinedButton(
              onPressed: () {
                Get.back();
              },
              child: const Text(&#39;Close&#39;),
            ),
          ],
        ),
      ),
    );
  }

  //카메라 버튼 클릭시 호출. 네비게이션.
  CameraBtnClicked() {
    Get.toNamed(&#39;/camera&#39;);
  }

  //타이머.
  late Timer _timer;

  //컨트롤러 init 시 작동되는 함수들.
  //Duration 주기(1초)마다 총 걸음수와 100걸음 걸음수를 1씩 증가시키고, storage 값을 한번씩 읽음.
  void startWalk() {
    _timer = Timer.periodic(Duration(seconds: 1), (Timer timer) {
      walkTotal.value++;
      walk100.value++;

      storage.write(&#39;indicatorIndex&#39;, indicatorIndex.value);

      storage.listenKey(&#39;mainPageImageIndex&#39;, (value) {
        imagePath.value = value;
      });

      storage.listenKey(&#39;totalColor&#39;, (value) {
        currentTotalColor.value = value;
      });

      storage.listenKey(&#39;walkColor&#39;, (value) {
        currentWalkColor.value = value;
      });

      storage.listenKey(&#39;textColor&#39;, (value) {
        currentTextColor.value = value;
      });
    });
  }

  void startSplit5() {
    _timer = Timer.periodic(Duration(seconds: 5), (Timer timer) {
      walk100s5.value++;
    });
  }

  //1000/1 / 30
  void startSplitTotal5() {
    _timer = Timer.periodic(Duration(seconds: 33), (Timer timer) {
      walkTotals5.value++;
    });
  }

  // /5
  void startSplit5Cut() {
    _timer = Timer.periodic(Duration(seconds: 100), (Timer timer) {
      walk100s5.value -= 20;
    });
  }

  //init 시 호출되는 또 다른 메소드.
  // 사실 100만큼 올라갈때마다 그걸 감지해야 하는데, 목업이기에 임의로 100초 간격으로 업데이트.
  void startPoint() {
    _timer = Timer.periodic(Duration(seconds: 100), (Timer timer) {
      walk100.value -= 100;
      pointCount.value++;
    });
  }
}
</code></pre>
<h3 id="camera_view">camera_view</h3>
<p><img src="https://velog.velcdn.com/images/dc143c_dev/post/92bfbe2f-f982-4749-bc13-3c515b602b18/image.png" alt=""></p>
<p>직접 구현한 갤러리 기능과, color_picker 패키지로 구현한 색상 선택 기능을 담은 camera_view UI.</p>
<p>홈 화면의 왼쪽 상단 floatingActionButton을 클릭하면 나타난다.</p>
<pre><code class="language-dart">// ./lib/app/modules/camera/views/camera_view.dart
class CameraView extends GetView&lt;CameraController&gt; {
  CameraView({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: bgColor,
      appBar: AppBar(
        elevation: 0,
        backgroundColor: bgColor,
        title: const Text(
          &#39;갤러리&#39;,
          style: TextStyle(
              color: Colors.black,
              fontFamily: &#39;IBMKR&#39;,
              fontSize: 18,
              fontWeight: FontWeight.w600),
        ),
        centerTitle: true,
        actions: [
          Center(
            child: Obx(
              () =&gt; Text(
                controller.galleryPageIndexPlus.value.toString() + &#39;/5&#39;,
                style: TextStyle(
                    color: Colors.black,
                    fontFamily: &#39;IBMKR&#39;,
                    fontWeight: FontWeight.w700,
                    fontSize: 18),
              ),
            ),
          ),
          SizedBox(
            width: 40,
          ),
        ],
      ),
      body: Center(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Container(
              margin: EdgeInsets.all(10),
              decoration: BoxDecoration(
                  color: bgColor,
                  shape: BoxShape.circle,
                  boxShadow: [
                    BoxShadow(
                        color: mainGrey,
                        offset: Offset(0.0, 1.0),
                        blurRadius: 6.0),
                  ]),
              height: 350,
              width: 500,
              child: PageView(
                controller: controller.pageController,
                //페이지 변경시 onGalleryPageChanged 호출
                onPageChanged: controller.onGalleryPageChanged,
                children: [
                  //galleryPageUnit 커스텀 위젯으로 코드 단축. 모듈화.
                  PageUnit(
                    assetImage: AssetImage(&#39;assets/images/0.png&#39;),
                  ),
                  PageUnit(
                    assetImage: AssetImage(&#39;assets/images/1.png&#39;),
                  ),
                  PageUnit(
                    assetImage: AssetImage(&#39;assets/images/2.png&#39;),
                  ),
                  PageUnit(
                    assetImage: AssetImage(&#39;assets/images/3.png&#39;),
                  ),
                  PageUnit(
                    assetImage: AssetImage(&#39;assets/images/4.png&#39;),
                  ),
                ],
              ),
            ),
            SizedBox(
              height: 40,
            ),
            Row(
              children: [
                SizedBox(
                  width: 20,
                ),
                GestureDetector(
                  onTap: () {
                    controller.totalColorChangeBtnClicked();
                  },
                  child: Container(
                    height: 85,
                    width: 165,
                    decoration: BoxDecoration(
                      color: bgColor,
                      boxShadow: [
                        BoxShadow(
                          color: Colors.grey,
                          blurRadius: 5.0,
                          spreadRadius: 0.0,
                          offset: Offset(0, 7),
                        ),
                      ],
                      borderRadius: BorderRadius.all(
                        Radius.circular(10),
                      ),
                    ),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      crossAxisAlignment: CrossAxisAlignment.center,
                      children: [
                        SizedBox(
                          height: 30,
                          width: 30,
                          child: CircularProgressIndicator(
                            color: Color(controller.storage.read(&#39;totalColor&#39;)),
                            backgroundColor: bgColor,
                            strokeWidth: 3,
                            value: 70 / 100,
                          ),
                        ),
                        SizedBox(
                          width: 20,
                        ),
                        Text(
                          &#39;총 걸음 표시\n색상 선택하기&#39;,
                          style: TextStyle(
                              fontWeight: FontWeight.w700,
                              fontSize: 12,
                              fontFamily: &#39;IBMKR&#39;),
                        ),
                      ],
                    ),
                  ),
                ),
                SizedBox(
                  width: 25,
                ),
                GestureDetector(
                  onTap: () {
                    controller.walkColorChangeBtnClicked();
                  },
                  child: Container(
                    height: 85,
                    width: 165,
                    decoration: BoxDecoration(
                      color: bgColor,
                      boxShadow: [
                        BoxShadow(
                          color: Colors.grey,
                          blurRadius: 5.0,
                          spreadRadius: 0.0,
                          offset: Offset(0, 7),
                        ),
                      ],
                      borderRadius: BorderRadius.all(
                        Radius.circular(10),
                      ),
                    ),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      crossAxisAlignment: CrossAxisAlignment.center,
                      children: [
                        SizedBox(
                          height: 25,
                          width: 25,
                          child: CircularProgressIndicator(
                            color: Color(controller.storage.read(&#39;walkColor&#39;)),
                            backgroundColor: bgColor,
                            strokeWidth: 3,
                            value: 90 / 100,
                          ),
                        ),
                        SizedBox(
                          width: 20,
                        ),
                        Text(
                          &#39;목표 걸음 표시\n색상 선택하기&#39;,
                          style: TextStyle(
                              fontWeight: FontWeight.w700,
                              fontSize: 12,
                              fontFamily: &#39;IBMKR&#39;),
                        ),
                      ],
                    ),
                  ),
                ),
                SizedBox(
                  width: 10,
                ),
              ],
            ),
            SizedBox(
              height: 30,
            ),
            GestureDetector(
              onTap: () {
                controller.textColorChangeBtnClicked();
              },
              child: Container(
                height: 85,
                width: 355,
                decoration: BoxDecoration(
                  color: bgColor,
                  boxShadow: [
                    BoxShadow(
                      color: Colors.grey,
                      blurRadius: 5.0,
                      spreadRadius: 0.0,
                      offset: Offset(0, 7),
                    ),
                  ],
                  borderRadius: BorderRadius.all(
                    Radius.circular(10),
                  ),
                ),
                child: Row(
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: [
                    SizedBox(
                      width: 30,
                    ),
                    Text(
                      &#39;T&#39;,
                      style: TextStyle(
                          fontFamily: &#39;LS&#39;,
                          fontSize: 30,
                          fontWeight: FontWeight.w700,
                          color: Color(controller.storage.read(&#39;textColor&#39;))),
                    ),
                    SizedBox(
                      width: 20,
                    ),
                    Text(
                      &#39;텍스트\n색상 선택하기&#39;,
                      style: TextStyle(
                          fontWeight: FontWeight.w700,
                          fontSize: 12,
                          fontFamily: &#39;IBMKR&#39;),
                    ),
                  ],
                ),
              ),
            ),
            SizedBox(
              height: 30,
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                SizedBox(
                  width: 360,
                  child: CustomButtonBrown(
                    btnText: &#39;적용하기&#39;,
                    onPressed: () {
                      controller.applyBtnClicked();
                      Get.back();
                    },
                  ),
                ),
              ],
            ),
            SizedBox(
              height: 40,
            ),
          ],
        ),
      ),
    );
  }
}</code></pre>
<h3 id="camera_controller">camera_controller</h3>
<pre><code class="language-dart">// ./lib/app/modules/camera/controllers/camera_controller.dart
//--사이클--
//각 UI 에 할당된 세 가지 color picker 로 색상 변경=&gt;
//color picker 의 onColorChanged 로 각 값에 할당된 selectedColor 를 int color 값으로 반환함=&gt;
//UI 에서 적용하기 버튼을 누르면 applyBtnClicked 함수가 호출되어 storage 에 세 가지 int color 값 저장=&gt;
class CameraController extends GetxController {
  WalkController walkController = WalkController();
  //get storage 사용하기 쉽게 미리 선언.
  GetStorage storage = GetStorage();

  var textInt = 0.obs;

  //블루 핑크 옐로우. 0으로 해도 상관은 없음. storage 에 저장될 색상.
  //int 값이어야 storage 에 저장할수 있기에, Color 위젯이 아닌 int 로 표기함.
  var totalColor = 0xFf4169e1.obs;
  var walkColor = 0xFFff69b4.obs;
  var textColor = 0xffffffff.obs;

  //color picker 가 감지할 색상.
  var selectedTotalColor = Color(0).obs;
  var selectedWalkColor = Color(0).obs;
  var selectedTextColor = Color(0).obs;

  //현재 선택된 이미지 번호. 에셋 이미지로 0~4까지 다섯개.
  var selectedImagePath = &#39;0&#39;.obs;

  //갤러리의 pageView controller 가 감지할 현재 갤러리 번호.
  var galleryPageIndex = 0.obs;
  //갤러리 표시용. 위 값에서 +1.
  var galleryPageIndexPlus = 0.obs;

  //시작 페이지 0번. 노란 이미지.
  final pageController = PageController(initialPage: 0);

  //갤러리 페이지 변화를 감지. 변화된 페이지는 currentPage 라는 내부 변수로 담음.
  //이것을 galleryPageIndex 로 다시 담아주기.
  //print 는 디버그용.
  onGalleryPageChanged(int page) {
    print(&#39;page num:&#39; + page.toString());
    int currentPage = page.toInt();
    //페이지가 바뀔때마다 호출. 즉, galleryPageIndex 가 현재 페이지를 담게 됨.
    galleryPageIndex.value = currentPage;
    galleryPageIndexPlus.value = currentPage += 1;
    print(&#39;main page image path:&#39; + selectedImagePath.value.toString());
    print(&#39;current page:&#39; + galleryPageIndex.value.toString());
  }

  //적용 버튼 클릭시 호출.
  //walk controller 로 보낼 selectedImagePath 를 스트링으로 바꿔줌.
  //그리고 저장해야할 값들(페이지 인덱스 스트링 값, 세가지 컬러 값)을 get storage 에 저장.
  applyBtnClicked() {
    selectedImagePath.value = galleryPageIndex.value.toString();
    //storage 에 페이지 인덱스와 컬러 값들 저장. Color 값은 받을수 없으므로 int 로 변환하여 저장.
    storage.write(&#39;mainPageImageIndex&#39;, selectedImagePath.value);
    storage.write(&#39;totalColor&#39;, totalColor.value);
    storage.write(&#39;walkColor&#39;, walkColor.value);
    storage.write(&#39;textColor&#39;, textColor.value);

    print(&#39;write:&#39; + storage.read(&#39;mainPageImageIndex&#39;));
    print(&#39;write total color:&#39; + storage.read(&#39;totalColor&#39;).toString());
    print(&#39;write walk color:&#39; + storage.read(&#39;walkColor&#39;).toString());
    print(&#39;write text color:&#39; + storage.read(&#39;textColor&#39;).toString());
  }

  count() {
    textInt.value++;
    print(textInt.value);
    storage.listenKey(&#39;test&#39;, (value) =&gt; print(&#39;new key is $value&#39;));
  }

  read() {
    storage.read(&#39;test&#39;);
    print(textInt.value);
  }

  debugBtn() {
    storage.write(&#39;test&#39;, &#39;vvv&#39;);
    print(textInt.value);
    print(textColor);
    print(&#39;write:&#39; + storage.read(&#39;mainPageImageIndex&#39;));
    print(&#39;write total color:&#39; + storage.read(&#39;totalColor&#39;).toString());
    print(&#39;write walk color:&#39; + storage.read(&#39;walkColor&#39;).toString());
    print(&#39;write text color:&#39; + storage.read(&#39;textColor&#39;).toString());
  }

  //표시 색상 선택시 호출.
  //color picker 패키를 사용하여 색상 정해줌. 기본 컬러는 총 걸음 표시 색상. 바뀌면 함께 바뀜.
  //onColorChanged 로 색상이 바뀔때마다 Color 형태로 값을 저장. 아래 두 메소드도 작동방식 동일.
  //color 인자의 Color.value 로 int 색상 값을 추출.
  //인자값으로 존재하는 color.value 를 활용하여 변수에 컬러 값을 int 로 받아옴.
  totalColorChangeBtnClicked() {
    Get.dialog(
      AlertDialog(
        shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.all(Radius.circular(15.0))),
        content: Column(
          children: [
            SizedBox(
              height: 15,
            ),
            ColorPicker(
              pickerColor: Color(storage.read(&#39;totalColor&#39;)),
              onColorChanged: (Color color) {
                selectedTotalColor.value = color;
                print(selectedTotalColor.value);
                totalColor.value = color.value;
              },
              pickerAreaHeightPercent: 0.9,
              enableAlpha: true,
              paletteType: PaletteType.hsvWithHue,
            ),
            SizedBox(
              height: 70,
            ),
            CustomButtonYellow(
              btnText: &#39;적용하기&#39;,
              onPressed: () {
                Get.back();
              },
            ),
          ],
        ),
      ),
    );
  }

  walkColorChangeBtnClicked() {
    Get.dialog(
      AlertDialog(
        shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.all(Radius.circular(15.0))),
        content: Column(
          children: [
            SizedBox(
              height: 15,
            ),
            ColorPicker(
              pickerColor: Color(storage.read(&#39;walkColor&#39;)),
              onColorChanged: (Color color) {
                selectedWalkColor.value = color;
                print(color.value);
                walkColor.value = color.value;
              },
              pickerAreaHeightPercent: 0.9,
              enableAlpha: true,
              paletteType: PaletteType.hsvWithHue,
            ),
            SizedBox(
              height: 70,
            ),
            CustomButtonYellow(
              btnText: &#39;적용하기&#39;,
              onPressed: () {
                Get.back();
              },
            ),
          ],
        ),
      ),
    );
  }

  textColorChangeBtnClicked() {
    Get.dialog(
      AlertDialog(
        shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.all(Radius.circular(15.0))),
        content: Column(
          children: [
            SizedBox(
              height: 15,
            ),
            ColorPicker(
              pickerColor: Color(storage.read(&#39;textColor&#39;)),
              onColorChanged: (Color color) {
                selectedTextColor.value = color;
                print(selectedTextColor.value);
                textColor.value = color.value;
              },
              pickerAreaHeightPercent: 0.9,
              enableAlpha: true,
              paletteType: PaletteType.hsvWithHue,
            ),
            SizedBox(
              height: 70,
            ),
            CustomButtonYellow(
              btnText: &#39;적용하기&#39;,
              onPressed: () {
                Get.back();
              },
            ),
          ],
        ),
      ),
    );
  }

  @override
  void onInit() {
    super.onInit();
  }

  @override
  void onReady() {
    super.onReady();
  }

  @override
  void onClose() {
    super.onClose();
  }
}</code></pre>
<h2 id="25-인디케이터-ui">2.5: 인디케이터 UI</h2>
<p><img src="https://velog.velcdn.com/images/dc143c_dev/post/bd684327-9dbe-4e8f-a3b6-cba556544f0e/image.gif" alt=""></p>
<p>구현 화면. 현재는 네가지밖에 없으나, 추후에 더 고퀄리티의 UI 추가해볼 예정이다.</p>
<p>상단에 밝힌 포부대로 내 목표는 여러가지 인디케이터 UI를 유저가 고를수 있도록 하는 것이었다.</p>
<pre><code class="language-dart">// ./lib/app/modules/walk/views/walk_view
//walk_view의 일부.

PageView(
                        controller: controller.indicatorController,
                        onPageChanged: controller.onIndicatorPageChanged,
                        children: [
                          //Indicator
                          IndicatorCircularView(),
                          IndicatorStepView(),
                          IndicatorCircularBView(),
                          IndicatorStepBView(),
                        ],
                      ),</code></pre>
<p>단순한 디자인이라도 유저가 여러가지 디자인의 인디케이터를 사용할수 있도록, 프로필 사진을 담을 컨테이너를 Stack으로 감싸고, 인디케이터들을 담을 PageView를 그 위에 덧붙혔다. </p>
<p>stepIndicator들은 상단에 서술한 패키지를 사용했다.</p>
<h3 id="indicator_circular">indicator_circular</h3>
<pre><code class="language-dart">//첫번째 인디케이터.
class IndicatorCircularView extends GetView&lt;WalkController&gt; {
  const IndicatorCircularView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Obx(
      () =&gt; Stack(
        children: [
          Center(
            child: SizedBox(
              width: 250,
              height: 250,
              child: CircularProgressIndicator(
                strokeWidth: 10,
                backgroundColor: bgColor,
                color: Color(controller.currentWalkColor.value),
                value: controller.walk100.value.toDouble() /
                    controller.walk100maxSecond,
              ),
            ),
          ),
          Center(
            child: SizedBox(
              width: 300,
              height: 300,
              child: CircularProgressIndicator(
                strokeWidth: 12,
                backgroundColor: bgColor,
                color: Color(controller.currentTotalColor.value),
                value: controller.walkTotal.value.toDouble() /
                    controller.walkTotalMax.toInt(),
              ),
            ),
          ),
          Container(
            child: Center(
              child: SizedBox(
                height: 300,
                child: Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: [
                      Text(
                        &#39;   ${controller.walkTotal}\n 걸음&#39;,
                        style: TextStyle(
                          fontFamily: &#39;IBMKR&#39;,
                          fontSize: 18,
                          fontWeight: FontWeight.w700,
                          color: Color(controller.currentTextColor.value),
                        ),
                      ),
                      SizedBox(
                        height: 10,
                      ),
                      Text(
                        &#39;         ${controller.walk100}/100\n 다음 포인트까지&#39;,
                        style: TextStyle(
                          fontFamily: &#39;IBMKR&#39;,
                          fontSize: 18,
                          fontWeight: FontWeight.w700,
                          color: Color(controller.currentTextColor.value),
                        ),
                      ),
                      SizedBox(
                        height: 20,
                      ),
                      Text(
                        &#39;${controller.pointCount} Cash&#39;,
                        style: TextStyle(
                          fontFamily: &#39;IBMKR&#39;,
                          fontSize: 18,
                          fontWeight: FontWeight.w700,
                          color: Color(controller.currentTextColor.value),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}</code></pre>
<h3 id="indicator_step">indicator_step</h3>
<pre><code class="language-dart">//두번째 인디케이터
class IndicatorStepView extends GetView&lt;WalkController&gt; {
  const IndicatorStepView({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Obx(
      () =&gt; Stack(
        children: [
          Center(
            child: SizedBox(
              width: 300,
              height: 300,
              child: CircularStepProgressIndicator(
                stepSize: 35,
                selectedStepSize: 40,
                selectedColor: Color(controller.currentTotalColor.value),
                unselectedColor: bgColor,
                totalSteps: controller.walkTotalMaxSplit5,
                currentStep: controller.walkTotals5.value,
              ),
            ),
          ),
          Center(
            child: SizedBox(
              width: 250,
              height: 250,
              child: CircularStepProgressIndicator(
                stepSize: 15,
                selectedStepSize: 20,
                selectedColor: Color(controller.currentWalkColor.value),
                unselectedColor: bgColor,
                totalSteps: controller.walk100maxSecondSplit5,
                currentStep: controller.walk100s5.value,
              ),
            ),
          ),
          Container(
            child: Center(
              child: SizedBox(
                height: 300,
                child: Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: [
                      Text(
                        &#39;   ${controller.walkTotal}\n 걸음&#39;,
                        style: TextStyle(
                          fontFamily: &#39;IBMKR&#39;,
                          fontSize: 18,
                          fontWeight: FontWeight.w700,
                          color: Color(controller.currentTextColor.value),
                        ),
                      ),
                      SizedBox(
                        height: 10,
                      ),
                      Text(
                        &#39;         ${controller.walk100}/100\n 다음 포인트까지&#39;,
                        style: TextStyle(
                          fontFamily: &#39;IBMKR&#39;,
                          fontSize: 18,
                          fontWeight: FontWeight.w700,
                          color: Color(controller.currentTextColor.value),
                        ),
                      ),
                      SizedBox(
                        height: 20,
                      ),
                      Text(
                        &#39;${controller.pointCount} Cash&#39;,
                        style: TextStyle(
                          fontFamily: &#39;IBMKR&#39;,
                          fontSize: 18,
                          fontWeight: FontWeight.w700,
                          color: Color(controller.currentTextColor.value),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}</code></pre>
<h3 id="indicator_circuler_b">indicator_circuler_b</h3>
<pre><code class="language-dart">//세번째 인디케이터
class IndicatorCircularBView extends GetView&lt;WalkController&gt; {
  const IndicatorCircularBView({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Obx(
      () =&gt; Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              SizedBox(
                width: 120,
                height: 120,
                child: Center(
                  child: Stack(
                    children: [
                      SizedBox(
                        width: 150,
                        height: 150,
                        child: CircularProgressIndicator(
                          strokeWidth: 12,
                          backgroundColor: bgColor,
                          color: Color(controller.currentTotalColor.value),
                          value: controller.walkTotal.value.toDouble() /
                              controller.walkTotalMax.toInt(),
                        ),
                      ),
                      Center(
                        child: Text(
                          &#39; ${controller.walkTotal}\n  걸음&#39;,
                          style: TextStyle(
                            fontFamily: &#39;IBMKR&#39;,
                            fontSize: 18,
                            fontWeight: FontWeight.w700,
                            color: Color(controller.currentTextColor.value),
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
              SizedBox(
                width: 40,
              ),
              SizedBox(
                width: 120,
                height: 120,
                child: Center(
                  child: Stack(
                    children: [
                      SizedBox(
                        width: 150,
                        height: 150,
                        child: CircularProgressIndicator(
                          strokeWidth: 12,
                          backgroundColor: bgColor,
                          color: Color(controller.currentWalkColor.value),
                          value: controller.walk100.value.toDouble() /
                              controller.walk100maxSecond.toInt(),
                        ),
                      ),
                      Center(
                        child: Text(
                          &#39;        ${controller.walk100}/100\n 다음 포인트까지&#39;,
                          style: TextStyle(
                            fontFamily: &#39;IBMKR&#39;,
                            fontSize: 14,
                            fontWeight: FontWeight.w700,
                            color: Color(controller.currentTextColor.value),
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
          SizedBox(
            height: 30,
          ),
          Text(
            &#39;${controller.pointCount} Cash&#39;,
            style: TextStyle(
              fontFamily: &#39;IBMKR&#39;,
              fontSize: 18,
              fontWeight: FontWeight.w700,
              color: Color(controller.currentTextColor.value),
            ),
          ),
        ],
      ),
    );
  }
}</code></pre>
<h3 id="indicator_step_b">indicator_step_b</h3>
<pre><code class="language-dart">//네번째 인디케이터
class IndicatorStepBView extends GetView&lt;WalkController&gt; {
  const IndicatorStepBView({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Obx(
      () =&gt; Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          SizedBox(
            height: 10,
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              SizedBox(
                width: 190,
                height: 190,
                child: Center(
                  child: Stack(
                    children: [
                      SizedBox(
                        width: 190,
                        height: 190,
                        child: CircularStepProgressIndicator(
                          stepSize: 35,
                          selectedStepSize: 40,
                          width: 200,
                          selectedColor:
                              Color(controller.currentTotalColor.value),
                          unselectedColor: bgColor,
                          totalSteps: controller.walkTotalMaxSplit5,
                          currentStep: controller.walkTotals5.value,
                        ),
                      ),
                      Center(
                        child: Text(
                          &#39; ${controller.walkTotal}\n  걸음&#39;,
                          style: TextStyle(
                            fontFamily: &#39;IBMKR&#39;,
                            fontSize: 18,
                            fontWeight: FontWeight.w700,
                            color: Color(controller.currentTextColor.value),
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
              SizedBox(
                width: 40,
              )
            ],
          ),
          Row(
            children: [
              SizedBox(
                width: 110,
              ),
              Text(
                &#39;${controller.pointCount} Cash&#39;,
                style: TextStyle(
                  fontFamily: &#39;IBMKR&#39;,
                  fontSize: 18,
                  fontWeight: FontWeight.w700,
                  color: Color(controller.currentTextColor.value),
                ),
              ),
              SizedBox(
                width: 20,
              ),
              SizedBox(
                width: 120,
                height: 120,
                child: Center(
                  child: Stack(
                    children: [
                      CircularStepProgressIndicator(
                        stepSize: 15,
                        selectedStepSize: 17,
                        selectedColor: Color(controller.currentWalkColor.value),
                        unselectedColor: bgColor,
                        totalSteps: controller.walk100maxSecondSplit5,
                        currentStep: controller.walk100s5.value,
                      ),
                      Center(
                        child: Text(
                          &#39;        ${controller.walk100}/100\n 다음 포인트까지&#39;,
                          style: TextStyle(
                            fontFamily: &#39;IBMKR&#39;,
                            fontSize: 12,
                            fontWeight: FontWeight.w700,
                            color: Color(controller.currentTextColor.value),
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
          SizedBox(
            height: 30,
          ),
        ],
      ),
    );
  }
}</code></pre>
<h2 id="3-결과물">3: 결과물</h2>
<p><img src="https://velog.velcdn.com/images/dc143c_dev/post/61c473a3-2ece-42df-9274-9aeb6ac46908/image.gif" alt=""></p>
<p><strong>색상 선택 후 UI에 적용되는 화면</strong></p>
<p><img src="https://velog.velcdn.com/images/dc143c_dev/post/58fff8fc-369f-48a4-a31c-970242b5377e/image.gif" alt=""></p>
<p><strong>앱을 재시작해도 storage에 저장된 데이터를 읽어 색상과 사진이 유지되는 화면</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] node.js/express+mysql DB api 서버와 연동하여 데이터를 UI에 출력해보기]]></title>
            <link>https://velog.io/@dc143c_dev/Flutter-node.jsexpressmysql-%EB%B0%B1%EC%97%94%EB%93%9C%EC%99%80-%EC%97%B0%EB%8F%99%ED%95%98%EC%97%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-UI%EC%97%90-%EC%B6%9C%EB%A0%A5%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@dc143c_dev/Flutter-node.jsexpressmysql-%EB%B0%B1%EC%97%94%EB%93%9C%EC%99%80-%EC%97%B0%EB%8F%99%ED%95%98%EC%97%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-UI%EC%97%90-%EC%B6%9C%EB%A0%A5%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Tue, 20 Dec 2022 13:32:40 GMT</pubDate>
            <description><![CDATA[<p>결과물 미리보기</p>
<p><strong>0.</strong> 
플러터를 학습하면서 firestore database가 아닌 관계형 db(MySQL)와 UI를 연동하는 방법에 흥미가 생겼다.</p>
<p>마침 팀 프로젝트로 개발중인 플랫폼 서비스의 어드민 페이지 파트 제작을 내가 맡게 되었기에, 프로젝트의 MySQL과 연동된 express 웹서버에 데이터 테이블을 가져오는 간단한 api를 작성하고 이를 플러터 UI와 연동해보기로 했다.</p>
<p>해당 블로그 게시물은 작성자의 학습과정을 정리하기 위해 작성되었다. 
절대 정답을 기술하는것이 아니며, 기능 구현을 위한 코드가 난해하고 미숙할수도 있다.</p>
<p><strong>1.express 사전 준비</strong></p>
<pre><code class="language-javascript">//server.js
const express = require(&#39;express&#39;);
const path = require(&#39;path&#39;);
const mysql = require(&#39;mysql&#39;);
const bodyParser = require(&#39;body-parser&#39;);

const app = express();

app.use(express.json());
var cors = require(&#39;cors&#39;);

app.use(cors());

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended:true}));

app.listen(8000, function () {
  console.log(&#39;listening on 8000&#39;)
}); </code></pre>
<p>우선 express 프로젝트를 생성하고, 기본적인 웹서버를 띄우기 위한 코드를 server.js에 작성한다. 이것으로 localhost의 8000번 포트에 간단한 웹서버가 실행되었다.</p>
<pre><code class="language-javascript">//server.js에 추가
var con = mysql.createConnection({
    host: &#39;localhost&#39;,
    user: &#39;root&#39;,
    password: &#39;&#39;,
    port:3307,
    database: &#39;new_users&#39;

  });

con.connect(function(err){
  if (err) throw err;
  console.log(&#39;Connected&#39;);
})
</code></pre>
<p>그리고 MySQL과 연결하기 위한 코드를 작성한다. 나는 연결에 성공했을시 로그에 Connected를 띄어주는 코드를 추가로 작성하였다.</p>
<pre><code class="language-javascript">//server.js에 추가
app.get(&#39;/get&#39;, function(req, res){
  var sql = &#39;select * from user_table&#39;;
  con.query(sql, function(err, id, fields){
    var user_id = req.params.id;
    if(id){
      var sql=&#39;select * from user_table&#39;
      con.query(sql, function(err, id, fields){
        if(err){
          console.log(err);
        }else{
          res.json(id);
          console.log(&#39;user:&#39;, user_id);
          console.log(&#39;user:&#39;, fields);
        }
      })
    }
  })
})</code></pre>
<p>그리고 마지막으로 /get 으로 요청을 보낼시 응답해주는 간단한 api를 작성한다.
요청을 받으면 sql 변수에 담긴 sql문으로 user_table 테이블에 담긴 모든 데이터를 가져온다. 
id값을 체크하여, 조건문을 통해 한번 더 쿼리를 날려 응답할 값을 가져온다.</p>
<p><img src="https://velog.velcdn.com/images/dc143c_dev/post/46fc7ad2-3f06-4296-ac54-f29b5af8a62b/image.png" alt=""></p>
<p><strong>사전에 MySQL에 작성된 데이터 테이블,</strong> 우리 목표는 api로 저기 작성된 유저의 아이디와 이름, 이메일과 패스워드를 가져오는 것이다.</p>
<p><strong>2.Postman으로 요청 테스트</strong></p>
<p><img src="https://velog.velcdn.com/images/dc143c_dev/post/f28e6a8f-5ff7-4d5d-9c68-2e297dc35291/image.png" alt="">
로컬호스트로 띄워진 express 웹서버의 8000번 포트, 서버에 작성된 /get 에 포스트맨으로 GET 요청을 날려보았다.
사전에 준비했던 MySQL의 데이터가 잘 출력되는걸 볼수 있다. </p>
<p><strong>3.flutter에 구현</strong>
그럼 이제 이 데이터를 flutter에 구현해보자.</p>
<pre><code class="language-dart">//network.dart
import &#39;package:http/http.dart&#39; as http;
import &#39;dart:convert&#39;;

class Network {
  final String url;
  Network(this.url);

  Future&lt;dynamic&gt; getJsonData() async {
    // var url = Uri.parse(&#39;http://localhost:8000/get&#39;);
    http.Response response = await http.get(Uri.parse(url));
    var userJson = response.body;
    var parsingData = jsonDecode(userJson);
    return parsingData;
  }
}</code></pre>
<p>우선 http 패키지를 사용하여 uri를 파싱하고 json 데이터를 담아주는 변수를 포함하는 클래스를 작성한다.
해당 클래스는 url를 받으면 http.get 메소드에 우리가 작성한 url을 넘겨준다.</p>
<pre><code class="language-dart">//전체 코드
import &#39;dart:convert&#39;;

import &#39;package:flutter/material.dart&#39;;
import &#39;package:myproject/network.dart&#39;;

class MyView extends StatefulWidget {
  @override
  State&lt;MyView&gt; createState() =&gt; _MyViewState();
}

class _MyViewState extends State&lt;MyView&gt; {
  //사용할 변수 미리 선언
  late String userName = &#39;&#39;;
  late String userEmail = &#39;&#39;;

  @override
  void initState() {
    super.initState();
    //state 진입시 api 데이터 파싱.
    getTestData();
  }

  getTestData() async {
    //url을 받아 데이터를 파싱하는 network 메소드 사용. 미리 만들어둠.
    //mysql db에서 유저 데이터를 받아오는 express api 호출
    Network network = Network(&#39;http://localhost:8000/get&#39;);

    var jsonData = await network.getJsonData();
    userName = await jsonData[0][&#39;user_name&#39;];
    userEmail = await jsonData[0][&#39;user_email&#39;];
  }

  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Container(
        color: Colors.greenAccent,
        child: Center(
          child: Column(
            children: [
              Text(&#39;${userName}&#39;),
              Text(&#39;${userEmail}&#39;),
            ],
          ),
        ),
      ),
    );
  }
}
</code></pre>
<p>network.dart로 분리한 Network클래스의 getTestData메소드를 initState로 state가 시작될때 한번 호출한다.
그럼 작성된 getTestData의 http요청 주소를 파싱하고, 클래스 상단에 작성된 userName 변수와 userEmail 변수에 json 데이터가 담겨 Text로 출력할수 있게 된다.</p>
<p><img src="https://velog.velcdn.com/images/dc143c_dev/post/9a0229ce-ae31-45c4-85d9-8a4220e00814/image.png" alt=""></p>
<p><strong>최종 결과물.</strong>
두 Text 위젯에 express api를 호출해서 받아온 MySQL 데이터가 잘 보여진다!</p>
<p>대략적인 작동 방식을 요약하자면,</p>
<p><strong>위젯 빌드-&gt;</strong></p>
<p><strong>initState를 통해 widget이 빌드될때 getTestData 메소드를 실행-&gt;</strong></p>
<p><strong>getTestData에서 데이터를 json으로 decode해주는 메소드인 network메소드를 실행-&gt;</strong></p>
<p><strong>getTestData에서 넘겨진 http로 로컬호스트에서 실행중인 api 서버에 요청을 날려 mysql db에 작성된 데이터를 불러옴-&gt;</strong></p>
<p><strong>그렇게 받아온 데이터를 getTestData에서 userName, userEmail 변수로 받아옴-&gt;</strong></p>
<p><strong>flutter UI에 출력.</strong></p>
]]></description>
        </item>
    </channel>
</rss>