<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Yudi Setiawan on Medium]]></title>
        <description><![CDATA[Stories by Yudi Setiawan on Medium]]></description>
        <link>https://medium.com/@coderjava?source=rss-b636a005b4e2------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*-1NV8tmQR7QoMymlR7qJeg.jpeg</url>
            <title>Stories by Yudi Setiawan on Medium</title>
            <link>https://medium.com/@coderjava?source=rss-b636a005b4e2------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Mon, 06 Jul 2026 04:58:04 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@coderjava/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Welcome 2025]]></title>
            <link>https://coderjava.medium.com/welcome-2025-82a734b73a06?source=rss-b636a005b4e2------2</link>
            <guid isPermaLink="false">https://medium.com/p/82a734b73a06</guid>
            <category><![CDATA[2025]]></category>
            <dc:creator><![CDATA[Yudi Setiawan]]></dc:creator>
            <pubDate>Wed, 29 Jan 2025 15:07:19 GMT</pubDate>
            <atom:updated>2025-01-29T15:07:19.887Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bxPRiIiMOXWk0RZufHhnGg.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/@kellysikkema?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Kelly Sikkema</a> on <a href="https://unsplash.com/photos/a-persons-hand-holding-the-number-twenty-twenty-twenty-twenty-twenty-twenty-twenty-twenty-Y2pzmNYinu0?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Unsplash</a></figcaption></figure><p>Di tahun 2025 ini saya ada rencana mau mulai menulis lagi di medium. Walaupun sempat vakum beberapa tahun karena urusan pekerjaan. Tapi, di tahun 2025 ini mudah-mudahan saya bisa kembali menulis lagi di medium.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=82a734b73a06" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Flutter: IndexedStack]]></title>
            <link>https://medium.com/nusanet/flutter-indexedstack-f09847a425bb?source=rss-b636a005b4e2------2</link>
            <guid isPermaLink="false">https://medium.com/p/f09847a425bb</guid>
            <category><![CDATA[flutter-widget]]></category>
            <category><![CDATA[flutter-ui]]></category>
            <category><![CDATA[indexedstack]]></category>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[flutter-app-development]]></category>
            <dc:creator><![CDATA[Yudi Setiawan]]></dc:creator>
            <pubDate>Mon, 25 Jul 2022 02:02:49 GMT</pubDate>
            <atom:updated>2022-07-28T03:10:23.378Z</atom:updated>
            <content:encoded><![CDATA[<h4>How to use widget IndexedStack in Flutter</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cyq0ImHDRvWu-fdwZ40rFw.png" /></figure><h3>Pengenalan</h3><p>Ketika kita ingin membuat sebuah tampilan antarmuka di Flutter pastinya kita sering menjumpai sebuah case yang mana, kita buat sebuah pengkondisian ketika kita ingin menampilkan sebuah widget. Contoh sederhananya misal, ketika si user pilih kategori A maka, akan tampil widget A. Dan jika si user pilih kategori B maka, akan tampil widget B. Nah, hal-hal seperti ini saya yakin kita semua pasti pernah mengalami case tersebut. Dan pada umumnya, untuk solusi tersebut biasanya kita buat pengkondisian secara manual menggunakan if else conditional. Di Flutter, kita sebagai developer benar-benar merasa terbantu dengan adanya kumpulan-kumpulan widget yang disediakan oleh Flutter. Jadi, di Flutter ada satu widget yang bernama <strong>IndexedStack</strong>. Widget ini berfungsi untuk membantu ketika kita ingin mengganti-ganti tampilan sebuah widget tanpa harus membuat if else conditional.</p><h3>Contoh Projek</h3><p>Untuk contohnya, kali ini kita akan membuat sebuah form sederhana saja yang mana didalamnya ada TextField pilih kategori, IndexedStack, dan Button. Kurang lebih kira-kira seperti inilah tampilan aplikasinya.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgfycat.com%2Fifr%2Fagitatedfamiliarguineafowl&amp;display_name=Gfycat&amp;url=https%3A%2F%2Fgfycat.com%2Fagitatedfamiliarguineafowl&amp;image=https%3A%2F%2Fthumbs.gfycat.com%2FAgitatedFamiliarGuineafowl-size_restricted.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=gfycat" width="430" height="912" frameborder="0" scrolling="no"><a href="https://medium.com/media/b96abc5da66e3392c017291f55f03739/href">https://medium.com/media/b96abc5da66e3392c017291f55f03739/href</a></iframe><h4>Buat Projek</h4><p>Untuk langkah pertamanya, mari kita buat projek baru dengan nama <strong>flutter_indexed_stack</strong>.</p><h4>Setup pubspec.yaml</h4><p>Di projek ini kita membutuhkan sebuah plugin <a href="https://pub.dev/packages/intl">intl.</a> Oleh karena itu, kita perlu menambahkan plugin tersebut kedalam projek. Untuk menambahkannya silakan jalankan perintah berikut.</p><pre>flutter pub add intl</pre><p>Lalu, jalankan perintah flutter pub get .</p><h4>Atur file main.dart</h4><p>Langkah berikutnya ialah kita buka file <strong>main.dart</strong> dan ubah kode didalamnya menjadi seperti berikut.</p><pre>void main() {<br>  runApp(const MyApp());<br>}<br><br>class MyApp extends StatelessWidget {<br>  const MyApp({Key? key}) : super(key: key);<br><br>  @override<br>  Widget build(BuildContext context) {<br>    return MaterialApp(<br>      title: &#39;Flutter IndexedStack&#39;,<br>      theme: ThemeData(primarySwatch: Colors.<em>blue</em>),<br>      home: const MyHomePage(title: &#39;Flutter IndexedStack&#39;),<br>    );<br>  }<br>}<br><br>class MyHomePage extends StatefulWidget {<br>  final String title;<br><br>  const MyHomePage({<br>    Key? key,<br>    required this.title,<br>  }) : super(key: key);<br><br>  @override<br>  State&lt;MyHomePage&gt; createState() =&gt; _MyHomePageState();<br>}<br><br>class _MyHomePageState extends State&lt;MyHomePage&gt; {<br>  final listCategories = &lt;String&gt;[<br>    &#39;Peminjaman Dana&#39;,<br>    &#39;Perjalanan Dinas&#39;,<br>    &#39;Pengajuan Karyawan Baru&#39;,<br>  ];<br>  final listJobPositions = &lt;String&gt;[<br>    &#39;Product Engineer&#39;,<br>    &#39;Account Manager&#39;,<br>    &#39;Copywriter&#39;,<br>  ];<br>  final controllerCategory = TextEditingController();<br>  final controllerJobPosition = TextEditingController();<br><br>  int? indexCategory;<br>  var selectedCategory = &#39;&#39;;<br>  var widthScreen = 0.0;<br>  var heightScreen = 0.0;<br>  var selectedJobPosition = &#39;&#39;;<br><br>  @override<br>  Widget build(BuildContext context) {<br>    final mediaQueryData = MediaQuery.<em>of</em>(context);<br>    widthScreen = mediaQueryData.size.width;<br>    heightScreen = mediaQueryData.size.height;<br>    return Scaffold(<br>      appBar: AppBar(<br>        title: Text(widget.title),<br>      ),<br>      body: Container(<br>        width: double.<em>infinity</em>,<br>        padding: const EdgeInsets.all(16),<br>        child: Column(<br>          children: [<br>            buildWidgetTextFieldCategory(),<br>            indexCategory == null ? Container() : IndexedStack(<br>              index: indexCategory,<br>              children: [<br>                buildWidgetInputPeminjamanDana(),<br>                buildWidgetInputPerjalananDinas(),<br>                buildWidgetInputPengajuanKaryawanBaru(),<br>              ],<br>            ),<br>            const SizedBox(height: 16),<br>            buildWidgetButtonSubmit(),<br>          ],<br>        ),<br>      ),<br>    );<br>  }<br><br>  Widget buildWidgetTextFieldCategory() {<br>    // <em>TODO: Buat widget text field category<br>    </em>return Container();<br>  }<br><br>  Widget buildWidgetInputPeminjamanDana() {<br>    // <em>TODO: Buat widget input peminjaman dana<br>    </em>return Container();<br>  }<br><br>  Widget buildWidgetInputPerjalananDinas() {<br>    // <em>TODO: Buat widget input perjalanan dinas<br>    </em>return Container();<br>  }<br><br>  Widget buildWidgetInputPengajuanKaryawanBaru() {<br>    // <em>TODO: Buat widget input pengajuan karyawan baru<br>    </em>return Container();<br>  }<br><br>  Widget buildWidgetButtonSubmit() {<br>    // <em>TODO: Buat widget button submit<br>    </em>return Container();<br>  }<br><br>  void unfocus() {<br>    // <em>TODO: Buat function untuk unfocus setelah pilih data<br>  </em>}<br>}</pre><h4>Buat StatefulWidget WidgetDialogChoose</h4><p>Masih didalam file <strong>main.dart </strong>kita buat StatefulWidget baru dengan nama <strong>WidgetDialogChoose</strong>. Lalu, kita isi kode didalamnya menjadi seperti berikut.</p><pre>class WidgetDialogChoose extends StatefulWidget {<br>  final double widthScreen;<br>  final double heightScreen;<br>  final String title;<br>  final List&lt;String&gt; listOptions;<br>  final String? defaultSelected;<br><br>  const WidgetDialogChoose({<br>    Key? key,<br>    required this.widthScreen,<br>    required this.heightScreen,<br>    required this.title,<br>    required this.listOptions,<br>    required this.defaultSelected,<br>  }) : super(key: key);<br><br>  @override<br>  State&lt;WidgetDialogChoose&gt; createState() =&gt; _WidgetDialogChooseState();<br>}<br><br>class _WidgetDialogChooseState extends State&lt;WidgetDialogChoose&gt; {<br>  String? selectedCategory;<br><br>  @override<br>  void setState(VoidCallback fn) {<br>    if (mounted) {<br>      super.setState(fn);<br>    }<br>  }<br><br>  @override<br>  void initState() {<br>    selectedCategory = widget.defaultSelected;<br>    super.initState();<br>  }<br><br>  @override<br>  Widget build(BuildContext context) {<br>    final children = &lt;Widget&gt;[];<br>    final listWidgetRadioButton = &lt;Widget&gt;[];<br>    for (final itemCategory in widget.listOptions) {<br>      final widgetRadioButton = Radio&lt;String&gt;(<br>        value: itemCategory,<br>        groupValue: selectedCategory,<br>        onChanged: (value) =&gt; setState(() =&gt; selectedCategory = itemCategory),<br>      );<br>      final widgetText = Expanded(<br>        child: Text(<br>          itemCategory,<br>          style: Theme.<em>of</em>(context).textTheme.caption?.copyWith(<br>                color: Colors.<em>grey</em>[900],<br>              ),<br>          maxLines: 1,<br>          overflow: TextOverflow.ellipsis,<br>        ),<br>      );<br>      final widgetItem = GestureDetector(<br>        onTap: () =&gt; setState(() =&gt; selectedCategory = itemCategory),<br>        child: Row(<br>          children: [<br>            widgetRadioButton,<br>            widgetText,<br>          ],<br>        ),<br>      );<br>      listWidgetRadioButton.add(widgetItem);<br>    }<br>    children.addAll(listWidgetRadioButton);<br><br>    return AlertDialog(<br>      title: Text(<br>        widget.title,<br>        textAlign: TextAlign.start,<br>      ),<br>      titleTextStyle: Theme.<em>of</em>(context).textTheme.subtitle2?.copyWith(<br>            fontSize: 16,<br>          ),<br>      titlePadding: const EdgeInsets.only(<br>        left: 16,<br>        top: 16,<br>      ),<br>      contentPadding: const EdgeInsets.all(16),<br>      content: Column(<br>        mainAxisSize: MainAxisSize.min,<br>        children: [<br>          SizedBox(<br>            width: widget.widthScreen / 1.2,<br>            height: widget.heightScreen / 2,<br>            child: ListView(<br>              shrinkWrap: true,<br>              children: children,<br>            ),<br>          ),<br>          Row(<br>            children: [<br>              Expanded(<br>                child: buildWidgetButtonCancel(),<br>              ),<br>              const SizedBox(width: 16),<br>              Expanded(<br>                child: buildWidgetButtonOk(),<br>              ),<br>            ],<br>          ),<br>        ],<br>      ),<br>    );<br>  }<br><br>  Widget buildWidgetButtonCancel() {<br>    return TextButton(<br>      onPressed: () =&gt; Navigator.<em>pop</em>(context),<br>      child: const Text(&#39;Batal&#39;),<br>    );<br>  }<br><br>  Widget buildWidgetButtonOk() {<br>    return TextButton(<br>      onPressed: selectedCategory == null || selectedCategory!.isEmpty<br>          ? null<br>          : () {<br>              Navigator.<em>pop</em>(context, selectedCategory);<br>            },<br>      child: const Text(&#39;OK&#39;),<br>    );<br>  }<br>}</pre><p>Jadi, widget yang kita buat tadi berfungsi untuk menampilkan dialog yang didalamnya ada sebuah radio button yang bisa dipilih oleh user.</p><h4>Buat Class CurrencyIndonesiaInputFormatter</h4><p>Masih didalam file <strong>main.dart</strong> silakan kita buat class baru dengan nama <strong>CurrencyIndonesiaInputFormatter</strong> dan isi kode didalamnya menjadi seperti berikut.</p><pre>class CurrencyIndonesiaInputFormatter extends TextInputFormatter {<br>  @override<br>  TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {<br>    if (newValue.text.isEmpty) {<br>      return const TextEditingValue(<br>        text: &#39;0&#39;,<br>        selection: TextSelection.collapsed(offset: 1),<br>      );<br>    } else if (newValue.selection.baseOffset == 0) {<br>      return newValue;<br>    }<br>    var newNumberValue = double.<em>parse</em>(newValue.text.replaceAll(&#39;,&#39;, &#39;&#39;).replaceAll(&#39;.&#39;, &#39;&#39;));<br>    final formatter = NumberFormat(&#39;#,###&#39;);<br>    final newText = formatter.format(newNumberValue).replaceAll(&#39;,&#39;, &#39;.&#39;);<br>    return newValue.copyWith(<br>      text: newText,<br>      selection: TextSelection.collapsed(offset: newText.length),<br>    );<br>  }<br>}</pre><p>Class tersebut berfungsi untuk melakukan formatting angka rupiah ketika kita input angka didalam TextField.</p><h4>Buat Widget TextField Category</h4><p>Kemudian, kita akan membuat sebuah widget TextField untuk pilih kategori. Buka file <strong>main.dart</strong> dan ubah kode berikut.</p><pre>Widget buildWidgetTextFieldCategory() {<br>  // <em>TODO: Buat widget text field category<br>  </em>return Container();<br>}</pre><p>Menjadi seperti berikut.</p><pre>Widget buildWidgetTextFieldCategory() {<br>  return TextFormField(<br>    controller: controllerCategory,<br>    decoration: const InputDecoration(<br>      labelText: &#39;Kategori&#39;,<br>      isDense: true,<br>      suffixIcon: Icon(<br>        Icons.<em>arrow_drop_down</em>,<br>      ),<br>    ),<br>    readOnly: true,<br>    onTap: () async {<br>      final chooseCategory = await showDialog&lt;String&gt;(<br>        context: context,<br>        builder: (context) {<br>          return WidgetDialogChoose(<br>            widthScreen: widthScreen,<br>            heightScreen: heightScreen,<br>            title: &#39;Pilih Kategori&#39;,<br>            listOptions: listCategories,<br>            defaultSelected: selectedCategory,<br>          );<br>        },<br>      );<br>      if (chooseCategory != null) {<br>        setState(() {<br>          controllerJobPosition.clear();<br>          selectedJobPosition = &#39;&#39;;<br>          selectedCategory = chooseCategory;<br>          indexCategory = listCategories.indexWhere((element) =&gt; element == selectedCategory);<br>          controllerCategory.text = selectedCategory;<br>        });<br>      }<br>      unfocus();<br>    },<br>  );<br>}</pre><p>Sekarang coba kita jalankan programnya maka, outputnya akan menjadi seperti berikut.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgfycat.com%2Fifr%2Fshamefulastonishingindianglassfish&amp;display_name=Gfycat&amp;url=https%3A%2F%2Fgfycat.com%2Fshamefulastonishingindianglassfish&amp;image=https%3A%2F%2Fthumbs.gfycat.com%2FShamefulAstonishingIndianglassfish-size_restricted.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=gfycat" width="430" height="912" frameborder="0" scrolling="no"><a href="https://medium.com/media/35cc33812d226917e7ee600cc83adf30/href">https://medium.com/media/35cc33812d226917e7ee600cc83adf30/href</a></iframe><h4>Buat Widget Input Peminjaman Dana</h4><p>Langkah berikutnya kita akan membuat inputan peminjaman dana. Jadi, inputan ini akan muncul jika si user pilih kategorinya peminjaman dana. Ubah kode berikut.</p><pre>Widget buildWidgetInputPeminjamanDana() {<br>  // <em>TODO: Buat widget input peminjaman dana<br>  </em>return Container();<br>}</pre><p>Menjadi seperti berikut.</p><pre>Widget buildWidgetInputPeminjamanDana() {<br>  return Padding(<br>    padding: const EdgeInsets.only(top: 16),<br>    child: TextFormField(<br>      decoration: const InputDecoration(<br>        labelText: &#39;Jumlah&#39;,<br>        isDense: true,<br>        prefix: Text(&#39;Rp.&#39;),<br>      ),<br>      keyboardType: TextInputType.<em>number</em>,<br>      minLines: 1,<br>      maxLines: 1,<br>      inputFormatters: [<br>        CurrencyIndonesiaInputFormatter(),<br>      ],<br>    ),<br>  );<br>}</pre><p>Sekarang coba kita jalankan kembali programnya. Maka, output-nya akan menjadi seperti berikut.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgfycat.com%2Fifr%2Fgrouchyoddballgrouse&amp;display_name=Gfycat&amp;url=https%3A%2F%2Fgfycat.com%2Fgrouchyoddballgrouse&amp;image=https%3A%2F%2Fthumbs.gfycat.com%2FGrouchyOddballGrouse-size_restricted.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=gfycat" width="430" height="912" frameborder="0" scrolling="no"><a href="https://medium.com/media/a4c6de2645e22d883f1ff2e13ad469aa/href">https://medium.com/media/a4c6de2645e22d883f1ff2e13ad469aa/href</a></iframe><h4>Buat Widget Input Perjalanan Dinas</h4><p>Langkah selanjutnya, kita akan membuat widget input perjalanan dinas jika si user pilih kategori perjalanan dinas. Silakan kita ubah kode berikut.</p><pre>Widget buildWidgetInputPerjalananDinas() {<br>  // <em>TODO: Buat widget input perjalanan dinas<br>  </em>return Container();<br>}</pre><p>Menjadi seperti berikut.</p><pre>Widget buildWidgetInputPerjalananDinas() {<br>  return Padding(<br>    padding: const EdgeInsets.only(top: 16),<br>    child: TextFormField(<br>      decoration: const InputDecoration(<br>        labelText: &quot;Lokasi&quot;,<br>        isDense: true,<br>      ),<br>      keyboardType: TextInputType.<em>text</em>,<br>      minLines: 1,<br>      maxLines: 1,<br>    ),<br>  );<br>}</pre><p>Jika dijalankan programnya maka, outputnya akan menjadi seperti berikut.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgfycat.com%2Fifr%2Fhugewelldocumentedarachnid&amp;display_name=Gfycat&amp;url=https%3A%2F%2Fgfycat.com%2Fhugewelldocumentedarachnid&amp;image=https%3A%2F%2Fthumbs.gfycat.com%2FHugeWelldocumentedArachnid-size_restricted.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=gfycat" width="430" height="912" frameborder="0" scrolling="no"><a href="https://medium.com/media/1107fcc35503944cbf526b7097b59b9f/href">https://medium.com/media/1107fcc35503944cbf526b7097b59b9f/href</a></iframe><h4>Buat Widget Input Pengajuan Karyawan Baru</h4><p>Selanjutnya, kita akan membuat input pengajuan karyawan baru jika si user pilih kategori pengajuan karyawan baru. Untuk membuatnya silakan ubah kode berikut.</p><pre>Widget buildWidgetInputPengajuanKaryawanBaru() {<br>  // <em>TODO: Buat widget input pengajuan karyawan baru<br>  </em>return Container();<br>}</pre><p>Menjadi seperti berikut.</p><pre>Widget buildWidgetInputPengajuanKaryawanBaru() {<br>  return Padding(<br>    padding: const EdgeInsets.only(top: 16),<br>    child: TextFormField(<br>      controller: controllerJobPosition,<br>      decoration: const InputDecoration(<br>        labelText: &#39;Posisi pekerjaan&#39;,<br>        isDense: true,<br>        suffixIcon: Icon(<br>          Icons.<em>arrow_drop_down</em>,<br>        ),<br>      ),<br>      readOnly: true,<br>      onTap: () async {<br>        final chooseJobPosition = await showDialog&lt;String&gt;(<br>          context: context,<br>          builder: (context) {<br>            return WidgetDialogChoose(<br>              widthScreen: widthScreen,<br>              heightScreen: heightScreen,<br>              title: &#39;Pilih Posisi Pekerjaan&#39;,<br>              listOptions: listJobPositions,<br>              defaultSelected: selectedJobPosition,<br>            );<br>          },<br>        );<br>        if (chooseJobPosition != null) {<br>          setState(() {<br>            selectedJobPosition = chooseJobPosition;<br>            controllerJobPosition.text = selectedJobPosition;<br>          });<br>        }<br>        unfocus();<br>      },<br>    ),<br>  );<br>}</pre><p>Dan berikut ialah outputnya.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgfycat.com%2Fifr%2Fbreakableagreeablecheetah&amp;display_name=Gfycat&amp;url=https%3A%2F%2Fgfycat.com%2Fbreakableagreeablecheetah&amp;image=https%3A%2F%2Fthumbs.gfycat.com%2FBreakableAgreeableCheetah-size_restricted.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=gfycat" width="430" height="912" frameborder="0" scrolling="no"><a href="https://medium.com/media/8f6a8af76a9d5d09a8b69e0b218e366c/href">https://medium.com/media/8f6a8af76a9d5d09a8b69e0b218e366c/href</a></iframe><h4>Buat Function unfocus</h4><p>Silakan kita ubah kode berikut.</p><pre>void unfocus() {<br>  // <em>TODO: Buat function untuk unfocus setelah pilih data<br></em>}</pre><p>Menjadi seperti berikut.</p><pre>void unfocus() {<br>  final primaryFocus = FocusManager.<em>instance</em>.primaryFocus;<br>  if (primaryFocus != null) {<br>    primaryFocus.unfocus();<br>  }<br>}</pre><p>Jadi, function tersebut berfungsi agar ketika si user pilih kategori yang didalam TextField maka, fokus-nya itu tidak mengarah ke TextField setelah selesai pilih kategori-nya.</p><h4>Buat Widget Button Submit</h4><p>Langkah terakhir kita akan membuat widget button submit. Silakan kita ubah kode berikut.</p><pre>Widget buildWidgetButtonSubmit() {<br>  // <em>TODO: Buat widget button submit<br>  </em>return Container();<br>}</pre><p>Menjadi seperti berikut.</p><pre>Widget buildWidgetButtonSubmit() {<br>  return SizedBox(<br>    width: double.<em>infinity</em>,<br>    height: 42,<br>    child: ElevatedButton(<br>      onPressed: () {<br>        ScaffoldMessenger.<em>of</em>(context).showSnackBar(<br>          const SnackBar(<br>            content: Text(&#39;Data berhasil dikirimkan&#39;),<br>          ),<br>        );<br>      },<br>      child: const Text(&#39;Kirim&#39;),<br>    ),<br>  );<br>}</pre><h3>Kesimpulan</h3><p>Jadi, kurang lebih seperti itulah cara menggunakan widget <strong>IndexedStack </strong>di Flutter. Sangat gampangkan cara pakainya. Seperti biasa untuk source code lengkapnya bisa dicek di Github ya.</p><p><a href="https://github.com/CoderJava/flutter-indexed-stack">GitHub - CoderJava/flutter-indexed-stack: How to use IndexedStack widget in Flutter</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f09847a425bb" width="1" height="1" alt=""><hr><p><a href="https://medium.com/nusanet/flutter-indexedstack-f09847a425bb">Flutter: IndexedStack</a> was originally published in <a href="https://medium.com/nusanet">Nusanet Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Create chrome extension using Flutter]]></title>
            <link>https://medium.com/nusanet/create-chrome-extension-using-flutter-3050a7910977?source=rss-b636a005b4e2------2</link>
            <guid isPermaLink="false">https://medium.com/p/3050a7910977</guid>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[flutter-app-development]]></category>
            <category><![CDATA[google-chrome-extensions]]></category>
            <category><![CDATA[chrome-extension]]></category>
            <category><![CDATA[flutter-web]]></category>
            <dc:creator><![CDATA[Yudi Setiawan]]></dc:creator>
            <pubDate>Sat, 21 May 2022 09:34:34 GMT</pubDate>
            <atom:updated>2022-05-21T13:08:09.336Z</atom:updated>
            <content:encoded><![CDATA[<h4>How to create chrome extension using Flutter</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4pBBG63cpL8xsp-4c-VDsA.png" /></figure><h3>Pengenalan</h3><p>Melihat perkembangan Flutter pada acara Google I/O 2022 kemarin saya benar-benar sangat antusias. Dan benar saja sesuai prediksi saya bahwa tahun ini Flutter desktop sudah benar-benar bisa kita coba di production. Dan untuk Flutter web saya lihat juga sudah lumayan bagus ya performance-nya daripada versi yang sebelumnya. Nah, berhubung si Flutter ini sudah semakin bagus saja perkembangannya saya jadi tertarik untuk membahas Flutter web. Jadi, di tulisan kali ini saya akan coba membahas bagaimana cara membuat extension Google Chrome menggunakan Flutter. Oya, untuk versi Flutter-nya kita nggak perlu pakai yang versi 3 ya. Yang versi 2 saja sudah cukup.</p><h3>Contoh projek</h3><p>Untuk contoh projeknya kali ini kita akan membuat extension Google Chrome yang berfungsi untuk membuat QR kode dari teks yang diinput oleh user.</p><h4>Buat projek</h4><p>Langkah pertamanya silakan kita buka text editor favorit kita masing-masing terus buat projek baru dengan nama <strong>qr_code_chrome_extension</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/830/1*L9ijobt343OOo3WcHhanwg.png" /><figcaption>Buat projek baru</figcaption></figure><h4>Setup manifest.json</h4><p>Buka file <strong>manifest.json </strong>dimana, didalamnya kita bisa lihat kode didalamnya kira-kira seperti berikut ini.</p><pre>{<br>    &quot;name&quot;: &quot;qr_code_chrome_extension&quot;,<br>    &quot;short_name&quot;: &quot;qr_code_chrome_extension&quot;,<br>    &quot;start_url&quot;: &quot;.&quot;,<br>    &quot;display&quot;: &quot;standalone&quot;,<br>    &quot;background_color&quot;: &quot;#0175C2&quot;,<br>    &quot;theme_color&quot;: &quot;#0175C2&quot;,<br>    &quot;description&quot;: &quot;QR code tools for chrome extension&quot;,<br>    &quot;orientation&quot;: &quot;portrait-primary&quot;,<br>    &quot;prefer_related_applications&quot;: false,<br>    &quot;icons&quot;: [<br>        {<br>            &quot;src&quot;: &quot;icons/Icon-192.png&quot;,<br>            &quot;sizes&quot;: &quot;192x192&quot;,<br>            &quot;type&quot;: &quot;image/png&quot;<br>        },<br>        {<br>            &quot;src&quot;: &quot;icons/Icon-512.png&quot;,<br>            &quot;sizes&quot;: &quot;512x512&quot;,<br>            &quot;type&quot;: &quot;image/png&quot;<br>        },<br>        {<br>            &quot;src&quot;: &quot;icons/Icon-maskable-192.png&quot;,<br>            &quot;sizes&quot;: &quot;192x192&quot;,<br>            &quot;type&quot;: &quot;image/png&quot;,<br>            &quot;purpose&quot;: &quot;maskable&quot;<br>        },<br>        {<br>            &quot;src&quot;: &quot;icons/Icon-maskable-512.png&quot;,<br>            &quot;sizes&quot;: &quot;512x512&quot;,<br>            &quot;type&quot;: &quot;image/png&quot;,<br>            &quot;purpose&quot;: &quot;maskable&quot;<br>        }<br>    ]<br>}</pre><p>Kemudian, kita ubah menjadi seperti berikut.</p><pre>{<br>    <strong>&quot;name&quot;: &quot;qr_code_chrome_extension&quot;,<br>    &quot;description&quot;: &quot;QR code tools for chrome extension&quot;,</strong><br>    &quot;version&quot;: &quot;1.0.0&quot;,<br>    &quot;content_security_policy&quot;: {<br>        &quot;extension_pages&quot;: &quot;script-src &#39;self&#39; ; object-src &#39;self&#39;&quot;<br>    },<br>    &quot;action&quot;: {<br>        <strong>&quot;default_popup&quot;: &quot;index.html&quot;,<br>        &quot;default_icon&quot;: &quot;/icons/Icon-192.png&quot;</strong><br>    },<br>    &quot;manifest_version&quot;: 3<br>}</pre><blockquote>Catatan: Yang saya tandain bold harap disesuaikan dengan lokal kita masing-masing ya.</blockquote><h4>Setup index.html</h4><p>Langkah berikutnya ialah kita buka file <strong>index.html</strong>.</p><pre>&lt;!DOCTYPE html&gt;<br>&lt;html&gt;<br>&lt;head&gt;<br>  &lt;!--<br>    If you are serving your web app in a path other than the root, change the<br>    href value below to reflect the base path you are serving from.<br><br>    The path provided below has to start and end with a slash &quot;/&quot; in order for<br>    it to work correctly.<br><br>    For more details:<br>    * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base<br><br>    This is a placeholder for base href that will be replaced by the value of<br>    the `--base-href` argument provided to `flutter build`.<br>  --&gt;<br>  &lt;base href=&quot;$FLUTTER_BASE_HREF&quot;&gt;<br><br>  &lt;meta charset=&quot;UTF-8&quot;&gt;<br>  &lt;meta content=&quot;IE=Edge&quot; http-equiv=&quot;X-UA-Compatible&quot;&gt;<br>  &lt;meta name=&quot;description&quot; content=&quot;QR code tools for chrome extension&quot;&gt;<br><br>  &lt;!-- iOS meta tags &amp; icons --&gt;<br>  &lt;meta name=&quot;apple-mobile-web-app-capable&quot; content=&quot;yes&quot;&gt;<br>  &lt;meta name=&quot;apple-mobile-web-app-status-bar-style&quot; content=&quot;black&quot;&gt;<br>  &lt;meta name=&quot;apple-mobile-web-app-title&quot; content=&quot;qr_code_chrome_extension&quot;&gt;<br>  &lt;link rel=&quot;apple-touch-icon&quot; href=&quot;icons/Icon-192.png&quot;&gt;<br><br>  &lt;!-- Favicon --&gt;<br>  &lt;link rel=&quot;icon&quot; type=&quot;image/png&quot; href=&quot;favicon.png&quot;/&gt;<br><br>  &lt;title&gt;qr_code_chrome_extension&lt;/title&gt;<br>  &lt;link rel=&quot;manifest&quot; href=&quot;manifest.json&quot;&gt;<br>&lt;/head&gt;<br>&lt;body&gt;<br>  &lt;!-- This script installs service_worker.js to provide PWA functionality to<br>       application. For more information, see:<br>       https://developers.google.com/web/fundamentals/primers/service-workers --&gt;<br>  &lt;script&gt;<br>    var serviceWorkerVersion = null;<br>    var scriptLoaded = false;<br>    function loadMainDartJs() {<br>      if (scriptLoaded) {<br>        return;<br>      }<br>      scriptLoaded = true;<br>      var scriptTag = document.createElement(&#39;script&#39;);<br>      scriptTag.src = &#39;main.dart.js&#39;;<br>      scriptTag.type = &#39;application/javascript&#39;;<br>      document.body.append(scriptTag);<br>    }<br><br>    if (&#39;serviceWorker&#39; in navigator) {<br>      // Service workers are supported. Use them.<br>      window.addEventListener(&#39;load&#39;, function () {<br>        // Wait for registration to finish before dropping the &lt;script&gt; tag.<br>        // Otherwise, the browser will load the script multiple times,<br>        // potentially different versions.<br>        var serviceWorkerUrl = &#39;flutter_service_worker.js?v=&#39; + serviceWorkerVersion;<br>        navigator.serviceWorker.register(serviceWorkerUrl)<br>          .then((reg) =&gt; {<br>            function waitForActivation(serviceWorker) {<br>              serviceWorker.addEventListener(&#39;statechange&#39;, () =&gt; {<br>                if (serviceWorker.state == &#39;activated&#39;) {<br>                  console.log(&#39;Installed new service worker.&#39;);<br>                  loadMainDartJs();<br>                }<br>              });<br>            }<br>            if (!reg.active &amp;&amp; (reg.installing || reg.waiting)) {<br>              // No active web worker and we have installed or are installing<br>              // one for the first time. Simply wait for it to activate.<br>              waitForActivation(reg.installing || reg.waiting);<br>            } else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {<br>              // When the app updates the serviceWorkerVersion changes, so we<br>              // need to ask the service worker to update.<br>              console.log(&#39;New service worker available.&#39;);<br>              reg.update();<br>              waitForActivation(reg.installing);<br>            } else {<br>              // Existing service worker is still good.<br>              console.log(&#39;Loading app from service worker.&#39;);<br>              loadMainDartJs();<br>            }<br>          });<br><br>        // If service worker doesn&#39;t succeed in a reasonable amount of time,<br>        // fallback to plaint &lt;script&gt; tag.<br>        setTimeout(() =&gt; {<br>          if (!scriptLoaded) {<br>            console.warn(<br>              &#39;Failed to load app from service worker. Falling back to plain &lt;script&gt; tag.&#39;,<br>            );<br>            loadMainDartJs();<br>          }<br>        }, 4000);<br>      });<br>    } else {<br>      // Service workers not supported. Just drop the &lt;script&gt; tag.<br>      loadMainDartJs();<br>    }<br>  &lt;/script&gt;<br>&lt;/body&gt;<br>&lt;/html&gt;</pre><p>Kemudian, kita ubah bagian tag script -nya menjadi seperti berikut.</p><pre>&lt;!DOCTYPE html&gt;<br>&lt;html&gt;<br>&lt;head&gt;<br>  &lt;!--<br>    If you are serving your web app in a path other than the root, change the<br>    href value below to reflect the base path you are serving from.<br><br>    The path provided below has to start and end with a slash &quot;/&quot; in order for<br>    it to work correctly.<br><br>    For more details:<br>    * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base<br><br>    This is a placeholder for base href that will be replaced by the value of<br>    the `--base-href` argument provided to `flutter build`.<br>  --&gt;<br>  &lt;base href=&quot;$FLUTTER_BASE_HREF&quot;&gt;<br><br>  &lt;meta charset=&quot;UTF-8&quot;&gt;<br>  &lt;meta content=&quot;IE=Edge&quot; http-equiv=&quot;X-UA-Compatible&quot;&gt;<br>  &lt;meta name=&quot;description&quot; content=&quot;QR code tools for chrome extension&quot;&gt;<br><br>  &lt;!-- iOS meta tags &amp; icons --&gt;<br>  &lt;meta name=&quot;apple-mobile-web-app-capable&quot; content=&quot;yes&quot;&gt;<br>  &lt;meta name=&quot;apple-mobile-web-app-status-bar-style&quot; content=&quot;black&quot;&gt;<br>  &lt;meta name=&quot;apple-mobile-web-app-title&quot; content=&quot;qr_code_chrome_extension&quot;&gt;<br>  &lt;link rel=&quot;apple-touch-icon&quot; href=&quot;icons/Icon-192.png&quot;&gt;<br><br>  &lt;!-- Favicon --&gt;<br>  &lt;link rel=&quot;icon&quot; type=&quot;image/png&quot; href=&quot;favicon.png&quot;/&gt;<br><br>  &lt;title&gt;qr_code_chrome_extension&lt;/title&gt;<br>  &lt;link rel=&quot;manifest&quot; href=&quot;manifest.json&quot;&gt;<br>&lt;/head&gt;<br>&lt;body&gt;<br>  &lt;!-- This script installs service_worker.js to provide PWA functionality to<br>       application. For more information, see:<br>       https://developers.google.com/web/fundamentals/primers/service-workers --&gt;<br>  <strong>&lt;script src=&quot;main.dart.js&quot; type=&quot;application/javascript&quot;&gt;&lt;/script&gt;</strong><br>&lt;/body&gt;<br>&lt;/html&gt;</pre><p>Lalu, kita perlu atur size window dari extension yang akan kita buat. Caranya, kita perlu tambahkan style di bagian tag html -nya menjadi seperti berikut.</p><pre>&lt;html <strong>style=&quot;height: 300px; width: 300px&quot;</strong>&gt;</pre><h4>Tes pasang extension</h4><p>Sekarang kita coba build flutter web-nya menggunakan perintah berikut.</p><pre>flutter build web --web-renderer html --csp</pre><p>Setelah berhasil ter-build. Coba kita buka Google Chrome. Kemudian, buka <strong>chrome://extensions</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/442/1*0q1SSkl6trVOYfIOXe5XHg.png" /><figcaption>Halaman manage extensions</figcaption></figure><p>Pilih <strong>Load unpacked</strong> dan pilih directory <strong>build/web</strong> hasil dari perintah build diatas. Jika berhasil pada tahap-tahap sebelumnya maka, aplikasi yang kita build tadi seharusnya akan tampil.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/655/1*wBaDIUayUqy6npMW6k4cMQ.png" /><figcaption>Aplikasi Flutter yang di-build tadi</figcaption></figure><p>Sekarang coba kita buka aplikasi extension-nya maka, tampil programnya seperti berikut.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/531/1*sD8-IY8q18eX57e6bmN4ZA.png" /><figcaption>Yeay… Proses pembuatan chrome extension-nya sudah berhasil</figcaption></figure><h3>Buat fitur QR kode</h3><p>Nah, jika proses pembuatan extension-nya sudah berhasil. Maka, langkah berikutnya adalah kita tinggal buat fitur QR kode-nya aja nih.</p><h4>Setup pubspec.yaml</h4><p>Untuk membuat fitur tersebut kita perlu menggunakan plugin berikut ini.</p><p><a href="https://pub.dev/packages/qr_flutter">qr_flutter | Flutter Package</a></p><p>Untuk menambahkan plugin tersebut kedalam projek silakan gunakan perintah berikut ini ya.</p><pre>flutter pub add qr_flutter</pre><p>Tunggu beberapa saat sampai proses download plugin-nya selesai. Jika proses tersebut berhasil seharusnya didalam file <strong>pubspec.yaml</strong> kita akan menemukan bahwa plugin qr_flutter sudah ditambahkan.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/558/1*kw6A6LTrDS7gc0NubEae8Q.png" /><figcaption>Plugin qr_flutter berhasil ditambahkan</figcaption></figure><h4>Buat tampilan utama</h4><p>Nah, langkah berikutnya kita akan membuat tampilan utamanya ya. Untuk membuatnya silakan buka file <strong>main.dart</strong> dan ubah kode didalamnya menjadi seperti berikut.</p><pre>import &#39;package:flutter/material.dart&#39;;<br>import &#39;package:qr_flutter/qr_flutter.dart&#39;;<br><br>void main() =&gt; runApp(const App());<br><br>class App extends StatelessWidget {<br>  const App({Key? key}) : super(key: key);<br><br>  @override<br>  Widget build(BuildContext context) {<br>    return const MaterialApp(<br>      home: HomePage(),<br>    );<br>  }<br>}<br><br>class HomePage extends StatefulWidget {<br>  const HomePage({Key? key}) : super(key: key);<br><br>  @override<br>  State&lt;HomePage&gt; createState() =&gt; _HomePageState();<br>}<br><br>class _HomePageState extends State&lt;HomePage&gt; {<br>  final controllerText = TextEditingController();<br>  final valueNotifierQr = ValueNotifier(&#39;&#39;);<br>  final formKey = GlobalKey&lt;FormState&gt;();<br><br>  @override<br>  Widget build(BuildContext context) {<br>    return Scaffold(<br>      body: Padding(<br>        padding: const EdgeInsets.all(16.0),<br>        child: Column(<br>          mainAxisSize: MainAxisSize.min,<br>          children: [<br>            Form(<br>              key: formKey,<br>              child: TextFormField(<br>                controller: controllerText,<br>                decoration: const InputDecoration(<br>                  isDense: true,<br>                  hintText: &#39;Enter a text or URL&#39;,<br>                ),<br>                maxLines: 1,<br>                minLines: 1,<br>                onFieldSubmitted: (value) {<br>                  // <em>TODO: Buat fitur membuat QR kode<br>                </em>},<br>              ),<br>            ),<br>            const SizedBox(height: 16),<br>            SizedBox(<br>              width: double.<em>infinity</em>,<br>              height: 36,<br>              child: ElevatedButton(<br>                onPressed: () {<br>                  // <em>TODO: Buat fitur membuat QR kode<br>                </em>},<br>                child: const Text(&#39;Create QR&#39;),<br>              ),<br>            ),<br>            const SizedBox(height: 16),<br>            ValueListenableBuilder(<br>              valueListenable: valueNotifierQr,<br>              builder: (BuildContext context, String value, _) {<br>                return QrImage(<br>                  data: value,<br>                  version: QrVersions.<em>auto</em>,<br>                  size: 180,<br>                );<br>              },<br>            ),<br>          ],<br>        ),<br>      ),<br>    );<br>  }<br>}</pre><p>Sekarang coba kita jalankan terlebih dahulu aplikasinya ya. Biar kelihatan tampilannya gimana.</p><pre>flutter run -d chrome</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mlVCF34i-7dNis1oy51f0w.png" /><figcaption>Tampilan utamanya</figcaption></figure><h4>Buat fitur QR kode</h4><p>Jika tampilannya sudah selesai maka, langkah berikutnya adalah kita tinggal buat fungsi untuk membuat QR kode dari teks yang diinput oleh si user. Silakan kita buka kembali file <strong>main.dart</strong> dan buat function berikut.</p><pre>void doCreateQrCode() {<br>  if (formKey.currentState!.validate()) {<br>    final qrData = controllerText.text.trim();<br>    valueNotifierQr.value = qrData;<br>  }<br>}</pre><p>Lalu, kita ubah kode todo berikut ini.</p><pre>// <em>TODO: Buat fitur membuat QR kode</em></pre><p>Menjadi seperti ini.</p><pre>doCreateQrCode();</pre><p>Kemudian, coba kita jalankan lagi programnya dan tes apakah fungsi buat QR kode sudah berhasil atau tidak.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgfycat.com%2Fifr%2Frectangularwateryamericancurl&amp;display_name=Gfycat&amp;url=https%3A%2F%2Fgfycat.com%2Frectangularwateryamericancurl&amp;image=https%3A%2F%2Fthumbs.gfycat.com%2FRectangularWateryAmericancurl-size_restricted.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=gfycat" width="750" height="404" frameborder="0" scrolling="no"><a href="https://medium.com/media/e52ff1aceb49651b15eb961787b875b7/href">https://medium.com/media/e52ff1aceb49651b15eb961787b875b7/href</a></iframe><h4>Build ulang extension-nya</h4><p>Sekarang coba kita build ulang extension-nya ya.</p><pre>flutter build web --web-renderer --csp</pre><p>Lalu, kita reload extension di Google chrome-nya dengan cara tekan tombol seperti yang ada di gambar berikut.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/436/1*lORJDC5ShahHIbDBlwkvlg.png" /><figcaption>Reload qr_code_chrome_extension</figcaption></figure><h4>Tes fitur QR kode</h4><p>Sekarang coba kita tes fitur QR kode di Google Chrome extension-nya.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgfycat.com%2Fifr%2Fteeminginexperiencedappaloosa&amp;display_name=Gfycat&amp;url=https%3A%2F%2Fgfycat.com%2Fteeminginexperiencedappaloosa&amp;image=https%3A%2F%2Fthumbs.gfycat.com%2FTeemingInexperiencedAppaloosa-size_restricted.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=gfycat" width="750" height="434" frameborder="0" scrolling="no"><a href="https://medium.com/media/e5b0e01ce2e29a4eb649bf9a343d49c9/href">https://medium.com/media/e5b0e01ce2e29a4eb649bf9a343d49c9/href</a></iframe><p>Sekarang coba saya tes apakah QR kode-nya sudah sesuai atau belum ya menggunakan aplikasi QR code scanner di HP.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgfycat.com%2Fifr%2Fclevereverlastingcheetah&amp;display_name=Gfycat&amp;url=https%3A%2F%2Fgfycat.com%2Fclevereverlastingcheetah&amp;image=https%3A%2F%2Fthumbs.gfycat.com%2FCleverEverlastingCheetah-size_restricted.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=gfycat" width="590" height="1280" frameborder="0" scrolling="no"><a href="https://medium.com/media/bb8f1b490965d09fb07a4862659c1812/href">https://medium.com/media/bb8f1b490965d09fb07a4862659c1812/href</a></iframe><h3>Kesimpulan</h3><p>Jadi kesimpulannya adalah bahwa Flutter web sudah bisa membuat extension untuk Google Chrome. Dan ini merupakan tanda bahwa Flutter benar-benar serius dikembangkan untuk web. Dan mudah-mudahan saja Flutter web bisa ikut berpartisipasi dalam daftar web framework yang ada.</p><p><a href="https://github.com/CoderJava/qr_code_chrome_extension">GitHub - CoderJava/qr_code_chrome_extension: QR code tools for chrome extension using Flutter</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3050a7910977" width="1" height="1" alt=""><hr><p><a href="https://medium.com/nusanet/create-chrome-extension-using-flutter-3050a7910977">Create chrome extension using Flutter</a> was originally published in <a href="https://medium.com/nusanet">Nusanet Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Flutter: StatefulBuilder]]></title>
            <link>https://medium.com/nusanet/flutter-statefulbuilder-f804a860e85a?source=rss-b636a005b4e2------2</link>
            <guid isPermaLink="false">https://medium.com/p/f804a860e85a</guid>
            <category><![CDATA[flutter-widget]]></category>
            <category><![CDATA[statefulbuilder]]></category>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[flutter-app-development]]></category>
            <category><![CDATA[flutter-ui]]></category>
            <dc:creator><![CDATA[Yudi Setiawan]]></dc:creator>
            <pubDate>Sat, 21 May 2022 03:22:19 GMT</pubDate>
            <atom:updated>2022-05-21T03:22:19.988Z</atom:updated>
            <content:encoded><![CDATA[<h4>How to use widget StatefulBuilder</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0sa_nblnkxdcptGI3blIdQ.png" /></figure><h3>Pengenalan</h3><p>Pada tulisan kali ini kita akan membahas mengenai salah satu widget yang bernama StatefulBuilder . Dari namanya saja pasti kita sudah tahu apa fungsi dari widget tersebut. Jadi, StatefulBuilder berfungsi untuk membuat state tersendiri didalam builder. Fungsi dari widget ini menurut saya cukup bagus ya karena widget ini seperti membuat state tersendiri sehingga state-nya itu terpisah dari parent-nya dan kadang kalau widget-nya yang ingin kita buat itu cukup sederhana saya pikir lebih baik pakai StatefulBuilder aja sih. Ketimbang harus buat class sendiri yang extends ke StatefulWidget .</p><h4>Contoh Projek</h4><p>Jadi, untuk contoh projek kali ini kita akan menggunakan salah satu design yang ada di material design miliknya si Google.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/720/1*QTuEzc0MyVSMjNhssWvIjA.png" /><figcaption><a href="https://material.io/components/dialogs">https://material.io/components/dialogs</a></figcaption></figure><p>Bisa kita lihat bahwa pada design diatas sebenarnya sangat sederhana banget ya. Jadi, kalau teman-teman sudah paham di Flutter pasti sudah bisa membayangkan kalau untuk membuat design diatas kita pasti akan membuat sebuah widget Stateful tersendiri untuk dialog tersebut agar ketika di-setState (user pilih options-nya) maka, parent-nya tidak ikut ter-build juga. Jadi, di tulisan kali ini saya akan buat 2 versi. Versi pertamanya kita akan pakai class tersendiri khusus untuk dialog-nya dan versi keduanya kita akan pakai StatefulBuilder.</p><h4>Mulai Pembuatan Projek</h4><p>Untuk langkah pertamanya silakan buat projek baru dengan nama <strong>belajar_stateful_builder</strong>.</p><h4>Buat Tampilan Utama</h4><p>Buka file <strong>main.dart</strong> dan ubah kode didalamnya menjadi seperti berikut.</p><pre>import &#39;package:flutter/material.dart&#39;;<br><br>void main() {<br>  runApp(const MyApp());<br>}<br><br>class MyApp extends StatelessWidget {<br>  const MyApp({Key? key}) : super(key: key);<br><br>  @override<br>  Widget build(BuildContext context) {<br>    return MaterialApp(<br>      title: &#39;Belajar Stateful Builder&#39;,<br>      theme: ThemeData(<br>        primarySwatch: Colors.<em>blue</em>,<br>      ),<br>      home: const MyHomePage(),<br>    );<br>  }<br>}<br><br>class MyHomePage extends StatefulWidget {<br>  const MyHomePage({Key? key}) : super(key: key);<br><br>  @override<br>  State&lt;MyHomePage&gt; createState() =&gt; _MyHomePageState();<br>}<br><br>class _MyHomePageState extends State&lt;MyHomePage&gt; {<br>  final listRingtones = &lt;String&gt;[<br>    &#39;None&#39;,<br>    &#39;Callisto&#39;,<br>    &#39;Ganymede&#39;,<br>    &#39;Luna&#39;,<br>    &#39;Mash-up&#39;,<br>    &#39;School\&#39;s out&#39;,<br>    &#39;Zen too&#39;,<br>  ];<br>  late String selectedRingtone;<br><br>  @override<br>  void initState() {<br>    selectedRingtone = listRingtones.first;<br>    super.initState();<br>  }<br><br>  @override<br>  Widget build(BuildContext context) {<br>    return Scaffold(<br>      appBar: AppBar(<br>        title: const Text(&#39;Belajar Stateful Builder&#39;),<br>      ),<br>      body: InkWell(<br>        onTap: () {<br>          // <em>TODO: tampilkan dialog untuk pilih phone ringtone<br>        </em>},<br>        child: Padding(<br>          padding: const EdgeInsets.all(16.0),<br>          child: SizedBox(<br>            width: double.<em>infinity</em>,<br>            child: Column(<br>              crossAxisAlignment: CrossAxisAlignment.start,<br>              mainAxisSize: MainAxisSize.min,<br>              children: [<br>                Text(<br>                  &#39;Phone ringtone&#39;,<br>                  style: Theme.<em>of</em>(context).textTheme.bodyText2?.copyWith(<br>                    fontWeight: FontWeight.<em>w500</em>,<br>                  ),<br>                ),<br>                Text(<br>                  selectedRingtone,<br>                  style: Theme.<em>of</em>(context).textTheme.bodyText2?.copyWith(<br>                    color: Colors.<em>grey</em>,<br>                  ),<br>                ),<br>              ],<br>            ),<br>          ),<br>        ),<br>      ),<br>    );<br>  }<br>}</pre><p>Berikut adalah output dari kode diatas.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wwI8EQcb9Vh0WK5D6J8KyA.png" /><figcaption>Tampilan utama</figcaption></figure><h4>Buat Versi Pertama</h4><p>Untuk versi pertamanya kita akan membuat dialog-nya menggunakan class StatefulWidget. Untuk membuatnya silakan kita buka file <strong>main.dart</strong> dan tambahkan kode berikut.</p><pre>class DialogPhoneRingtone extends StatefulWidget {<br>  final List&lt;String&gt; listRingtones;<br>  final String defaultSelectedRingtone;<br><br>  const DialogPhoneRingtone({<br>    Key? key,<br>    required this.listRingtones,<br>    required this.defaultSelectedRingtone,<br>  }) : super(key: key);<br><br>  @override<br>  State&lt;DialogPhoneRingtone&gt; createState() =&gt; _DialogPhoneRingtoneState();<br>}<br><br>class _DialogPhoneRingtoneState extends State&lt;DialogPhoneRingtone&gt; {<br>  late String selectedRingtone;<br><br>  @override<br>  void initState() {<br>    selectedRingtone = widget.defaultSelectedRingtone;<br>    super.initState();<br>  }<br><br>  @override<br>  Widget build(BuildContext context) {<br>    final mediaQueryData = MediaQuery.<em>of</em>(context);<br>    final widthScreen = mediaQueryData.size.width;<br>    final heightScreen = mediaQueryData.size.height;<br>    return AlertDialog(<br>      title: const Text(<br>        &#39;Phone ringtone&#39;,<br>        textAlign: TextAlign.start,<br>      ),<br>      titlePadding: const EdgeInsets.only(<br>        left: 16,<br>        top: 16,<br>      ),<br>      contentPadding: const EdgeInsets.all(16),<br>      content: Column(<br>        mainAxisSize: MainAxisSize.min,<br>        children: [<br>          SizedBox(<br>            width: widthScreen / 1.2,<br>            height: heightScreen / 2,<br>            child: ListView(<br>              shrinkWrap: true,<br>              children: widget.listRingtones.map((element) {<br>                return Row(<br>                  children: [<br>                    Radio&lt;String&gt;(<br>                      value: element,<br>                      groupValue: selectedRingtone,<br>                      onChanged: (_) {<br>                        setState(() {<br>                          selectedRingtone = element;<br>                        });<br>                      },<br>                    ),<br>                    Text(<br>                      element,<br>                      style: Theme.<em>of</em>(context).textTheme.caption?.copyWith(<br>                            color: Colors.<em>grey</em>[900],<br>                          ),<br>                      maxLines: 1,<br>                      overflow: TextOverflow.ellipsis,<br>                    ),<br>                  ],<br>                );<br>              }).toList(),<br>            ),<br>          ),<br>          Row(<br>            mainAxisAlignment: MainAxisAlignment.end,<br>            children: [<br>              TextButton(<br>                child: const Text(&#39;CANCEL&#39;),<br>                style: TextButton.<em>styleFrom</em>(<br>                  tapTargetSize: MaterialTapTargetSize.shrinkWrap,<br>                ),<br>                onPressed: () {<br>                  Navigator.<em>pop</em>(context);<br>                },<br>              ),<br>              TextButton(<br>                child: const Text(&#39;OK&#39;),<br>                style: TextButton.<em>styleFrom</em>(<br>                  tapTargetSize: MaterialTapTargetSize.shrinkWrap,<br>                ),<br>                onPressed: () {<br>                  Navigator.<em>pop</em>(context, selectedRingtone);<br>                },<br>              ),<br>            ],<br>          ),<br>        ],<br>      ),<br>    );<br>  }<br>}</pre><p>Lalu, kita ubah kode todo berikut ini.</p><pre>onTap: () async {<br>  // <em>TODO: tampilkan dialog untuk pilih phone ringtone  <br></em>},</pre><p>Menjadi seperti berikut.</p><pre>final resultSelectedRingtone = await showDialog&lt;String?&gt;(<br>  context: context,<br>  builder: (context) {<br>    return DialogPhoneRingtone(<br>      listRingtones: listRingtones,<br>      defaultSelectedRingtone: selectedRingtone,<br>    );<br>  },<br>);<br>if (resultSelectedRingtone != null) {<br>  setState(() {<br>    selectedRingtone = resultSelectedRingtone;<br>  });<br>}</pre><p>Sekarang coba jalankan kembali aplikasinya. Dan seharusnya didalam aplikasinya kita sudah bisa pilih phone ringtone-nya.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgfycat.com%2Fifr%2Fblissfulquarrelsomebluefish&amp;display_name=Gfycat&amp;url=https%3A%2F%2Fgfycat.com%2Fblissfulquarrelsomebluefish&amp;image=https%3A%2F%2Fthumbs.gfycat.com%2FBlissfulQuarrelsomeBluefish-size_restricted.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=gfycat" width="432" height="944" frameborder="0" scrolling="no"><a href="https://medium.com/media/d02a1699357d8936b0cee42a370d77d7/href">https://medium.com/media/d02a1699357d8936b0cee42a370d77d7/href</a></iframe><h4>Buat Versi Kedua</h4><p>Nah, untuk versi keduanya kita akan membuat tampilan yang sama hanya saja kita menggunakan StatefulBuilder . Untuk membuatnya silakan kita buka kembali file <strong>main.dart</strong> dan tambahkan function berikut kedalam class <strong>_MyHomePageState</strong>.</p><pre>Widget buildDialogPhoneRingtone() {<br>  return StatefulBuilder(<br>    builder: (context, setState) {<br>      debugPrint(&#39;build stateful builder&#39;);<br>      final mediaQueryData = MediaQuery.<em>of</em>(context);<br>      final widthScreen = mediaQueryData.size.width;<br>      final heightScreen = mediaQueryData.size.height;<br>      return AlertDialog(<br>        title: const Text(<br>          &#39;Phone ringtone&#39;,<br>          textAlign: TextAlign.start,<br>        ),<br>        titlePadding: const EdgeInsets.only(<br>          left: 16,<br>          top: 16,<br>        ),<br>        contentPadding: const EdgeInsets.all(16),<br>        content: Column(<br>          mainAxisSize: MainAxisSize.min,<br>          children: [<br>            SizedBox(<br>              width: widthScreen / 1.2,<br>              height: heightScreen / 2,<br>              child: ListView(<br>                shrinkWrap: true,<br>                children: listRingtones.map((element) {<br>                  return Row(<br>                    children: [<br>                      Radio&lt;String&gt;(<br>                        value: element,<br>                        groupValue: selectedRingtone,<br>                        onChanged: (_) {<br>                          setState(() {<br>                            selectedRingtone = element;<br>                          });<br>                        },<br>                      ),<br>                      Text(<br>                        element,<br>                        style: Theme.<em>of</em>(context).textTheme.caption?.copyWith(<br>                          color: Colors.<em>grey</em>[900],<br>                        ),<br>                        maxLines: 1,<br>                        overflow: TextOverflow.ellipsis,<br>                      ),<br>                    ],<br>                  );<br>                }).toList(),<br>              ),<br>            ),<br>            Row(<br>              mainAxisAlignment: MainAxisAlignment.end,<br>              children: [<br>                TextButton(<br>                  child: const Text(&#39;CANCEL&#39;),<br>                  style: TextButton.<em>styleFrom</em>(<br>                    tapTargetSize: MaterialTapTargetSize.shrinkWrap,<br>                  ),<br>                  onPressed: () {<br>                    Navigator.<em>pop</em>(context);<br>                  },<br>                ),<br>                TextButton(<br>                  child: const Text(&#39;OK&#39;),<br>                  style: TextButton.<em>styleFrom</em>(<br>                    tapTargetSize: MaterialTapTargetSize.shrinkWrap,<br>                  ),<br>                  onPressed: () {<br>                    Navigator.<em>pop</em>(context, selectedRingtone);<br>                  },<br>                ),<br>              ],<br>            ),<br>          ],<br>        ),<br>      );<br>    },<br>  );<br>}</pre><p>Lalu, kita ubah kode berikut.</p><pre>context: context,<br>builder: (context) {<br>  <strong>return DialogPhoneRingtone(<br>    listRingtones: listRingtones,<br>    defaultSelectedRingtone: selectedRingtone,<br>  );</strong><br>},</pre><p>Menjadi seperti berikut.</p><pre>context: context,<br>builder: (context) {<br>  <strong>return buildDialogPhoneRingtone(); </strong><br>},</pre><p>Kemudian, biar kelihatan proses build state dari class <strong>_MyHomePageState </strong>kita perlu tambahkan kode debugging-nya didalam method build .</p><pre>@override<br>Widget build(BuildContext context) {<br>  <strong>debugPrint(&#39;build parent&#39;);</strong><br>  return Scaffold(<br>...</pre><p>Sekarang coba kita jalankan lagi aplikasinya maka, outputnya akan tetap sama.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgfycat.com%2Fifr%2Fboguszanygalapagosalbatross&amp;display_name=Gfycat&amp;url=https%3A%2F%2Fgfycat.com%2Fboguszanygalapagosalbatross&amp;image=https%3A%2F%2Fthumbs.gfycat.com%2FBogusZanyGalapagosalbatross-size_restricted.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=gfycat" width="944" height="1050" frameborder="0" scrolling="no"><a href="https://medium.com/media/fc166cdbb1c7143757d97d4c3ac4ec95/href">https://medium.com/media/fc166cdbb1c7143757d97d4c3ac4ec95/href</a></iframe><p>Nah, bisa kita lihat di video diatas dibagian log console-nya bahwa widget StatefulBuilder menggunakan state tersendiri sehingga ketika melakukan setState didalam StatefulBuilder maka, state dari parent-nya tidak akan ter-build ulang.</p><h3>Kesimpulan</h3><p>Jadi, kesimpulannya adalah widget StatefulBuilder bisa membantu kita untuk membuat widget stateful tanpa harus buat class yang terpisah. Tapi, perlu dicatat ya bahwa jika widget stateful yang kita pakai itu ingin reusable atau bisa dipakai secara berulang-ulang di halaman lain maka, saya lebih sarankan pakai yang buat class sendiri daripada pakai StatefulBuilder . Untuk source code lengkapnya bisa kamu lihat di Github ya.</p><p><a href="https://github.com/CoderJava/belajar-stateful-builder">GitHub - CoderJava/belajar-stateful-builder: How to use Stateful Builder widget in Flutter</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f804a860e85a" width="1" height="1" alt=""><hr><p><a href="https://medium.com/nusanet/flutter-statefulbuilder-f804a860e85a">Flutter: StatefulBuilder</a> was originally published in <a href="https://medium.com/nusanet">Nusanet Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Flutter: Event Channel]]></title>
            <link>https://medium.com/nusanet/flutter-event-channel-d266ec9de02d?source=rss-b636a005b4e2------2</link>
            <guid isPermaLink="false">https://medium.com/p/d266ec9de02d</guid>
            <category><![CDATA[platform]]></category>
            <category><![CDATA[platform-channel]]></category>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[flutter-event-channel]]></category>
            <category><![CDATA[event-channel]]></category>
            <dc:creator><![CDATA[Yudi Setiawan]]></dc:creator>
            <pubDate>Mon, 14 Mar 2022 09:01:17 GMT</pubDate>
            <atom:updated>2022-03-14T09:01:17.030Z</atom:updated>
            <content:encoded><![CDATA[<h4>How to stream data from native side</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BZmwLjrGQ7eTRyyKmROljQ.png" /></figure><h3>Pengenalan</h3><p>Salah satu tantangan terbesar untuk framework cross platform seperti Flutter ini ialah bagaimana cara mereka agar si Flutter ini tetap bisa menggunakan fungsi-fungsi native dari sebuah platform. Baik itu di sisi mobile ada Android dan iOS maupun nanti kedepannya untuk platform Web dan Desktop (Mac, Linux, dan Windows). Beruntungnya di Flutter sudah mereka sediakan yang namanya itu platform channel. Jadi, platform channel ini ialah cara bagaimana si Flutter atau bahasa Dart bisa berkomunikasi dengan kode-kode native-nya si platform. Misal, kalau di Android kita bisa berkomunikasi dengan kode Java atau Kotlin. Dan di iOS bisa berkomunikasi dengan Obj-C atau Swift. Jadi, di platform channel ini ada 3 metode yang bisa kita gunakan untuk melakukan komunikasi dengan kode native-nya. Berikut adalah beberapa metodenya.</p><h4>1. Method Channel</h4><p>Untuk yang pertama, method channel ini ialah sebuah metode dimana, kita membuat kode Dart yang memanggil fungsi yang ada di native-nya. Jadi, secara sederhana saya katakan saja bahwa method channel ini cara kerjanya hampir mirip seperti kita memanggil sebuah method. Kita panggil method yang ada di native dari bahasa Dart lalu, kita tunggu respon yang diberikan oleh si native.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nZae2VRD9g5nBtoxNxbsLA.png" /><figcaption>Cara kerja method channel di Flutter</figcaption></figure><p>Untuk pembahasan detail-nya bisa kamu baca pada link berikut ini ya.</p><p><a href="https://medium.com/nusanet/flutter-to-native-code-525c25acf993">Flutter to Native Code</a></p><h4>2. Event Channel</h4><p>Untuk metode kedua, ini cara kerjanya berbeda dengan metode pertama. Jadi, jika di method channel kita panggil method yang ada di native dan menunggu kembalian datanya. Maka, di event channel kita cukup meng-listen data yang diberikan oleh si native. Cara kerjanya ini hampir mirip kayak stream. Jadi, di sisi Dart kita cukup listen aja. Dan di sisi native-nya nanti yang akan mengirimkan datanya.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ObYrh4ya4Mp_NMqDPS-UjA.png" /><figcaption>Cara kerja event channel</figcaption></figure><h4>3. Basic Message Channel</h4><p>Untuk metode yang ketiga ini jujur saja saya kurang begitu paham gimana cara kerjanya. Tapi, nanti kalau saya sudah paham akan saya buat tulisan terpisah mengenai detail penggunaannya.</p><p>Dari ketiga metode tersebut yang pernah saya pakai di lapangan itu hanya yang method channel saja. Untuk event channel saya belum pakai karena belum ada kebutuhan untuk menggunakannya. Jadi, untuk method channel saya pernah pakai untuk menggunakan plugin camera native-nya si Android ketimbang menggunakan plugin camera-nya si Flutter. Alasannya cukup sederhana yaitu, karena pas dipakai oleh user ternyata ada beberapa sebagian device Android yang error ketika coba pakai plugin camera-nya si Flutter. Alhasil saya kepikiran untuk memisahkan kode untuk fitur camera-nya. Untuk Android-nya saya pakai method channel agar menggunakan plugin camera-nya si Android sementara, untuk iOS saya tetap pakai plugin camera-nya si Flutter. So far sih untuk plugin camera di iOS belum ada kendala ya.</p><h3>Contoh Kasus</h3><p>Jadi, di tulisan ini saya akan bahas detail mengenai penggunaan event channel. Untuk projeknya silakan kita buat projek baru dengan nama <strong>belajar_flutter_event_channel</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/830/1*Y3UN9Kg2C2GsXCAZZ1MHjw.png" /><figcaption>Buat projek baru</figcaption></figure><p>Untuk platform-nya nanti kita coba buat untuk Android dan iOS saja ya.</p><h4>Buat UI Utama</h4><p>Jadi, di contoh kasus kali ini kita akan coba buat aplikasi timer. Cuma timer-nya itu dibuat dari kode native-nya bukan dari Dart. Untuk memulai membuatnya kita perlu buat UI-nya terlebih dahulu. Silakan kita buka file <strong>main.dart</strong> dan ubah kode didalamnya menjadi seperti berikut.</p><pre>import &#39;package:flutter/material.dart&#39;;<br>import &#39;package:flutter/services.dart&#39;;<br><br>void main() {<br>  runApp(const MyApp());<br>}<br><br>class MyApp extends StatelessWidget {<br>  const MyApp({Key? key}) : super(key: key);<br><br>  @override<br>  Widget build(BuildContext context) {<br>    return MaterialApp(<br>      title: &#39;Belajar Event Channel&#39;,<br>      theme: ThemeData(<br>        primarySwatch: Colors.<em>blue</em>,<br>      ),<br>      home: const MyHomePage(title: &#39;Belajar Event Channel&#39;),<br>    );<br>  }<br>}<br><br>class MyHomePage extends StatefulWidget {<br>  const MyHomePage({Key? key, required this.title}) : super(key: key);<br><br>  final String title;<br><br>  @override<br>  State&lt;MyHomePage&gt; createState() =&gt; _MyHomePageState();<br>}<br><br>class _MyHomePageState extends State&lt;MyHomePage&gt; {<br>  final eventChannel = const EventChannel(&#39;belajar_flutter_event_channel&#39;);<br>  final valueNotifier = ValueNotifier&lt;String&gt;(&#39;value&#39;);<br><br>  @override<br>  void initState() {<br>    eventChannel.receiveBroadcastStream().listen((event) {<br>      if (event is String) {<br>        valueNotifier.value = event;<br>      }<br>    });<br>    super.initState();<br>  }<br><br>  @override<br>  Widget build(BuildContext context) {<br>    return Scaffold(<br>      appBar: AppBar(<br>        title: Text(widget.title),<br>      ),<br>      body: Center(<br>        child: ValueListenableBuilder&lt;String&gt;(<br>          valueListenable: valueNotifier,<br>          builder: (BuildContext context, String value, Widget? child) {<br>            return Text(<br>              value,<br>              style: Theme.<em>of</em>(context).textTheme.headline6,<br>            );<br>          },<br>        ),<br>      ),<br>    );<br>  }<br>}</pre><p>Bisa kita lihat pada kode diatas bahwa kita ada menggunakan widget ValueListenableBuilder untuk meng-listen perubahan dari variable valueNotifier . Kemudian, didalam method initState kita ada menggunakan event channel untuk meng-listen data yang diberikan dari si native dan data yang dari native akan kita masukkan kedalam variable valueNotifier . Dan berikut ini adalah tampilan dari programnya.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/905/1*vT5kEVk0lcd0D0vBNBP5yQ.png" /><figcaption>Tampilan utama dari program</figcaption></figure><h4>Buat Event Channel di Android</h4><p>Untuk membuat event channel di Android silakan kita buka file <strong>MainActivity.kt </strong>(sesuaikan dengan bahasa native-nya masing-masing ya) dan ubah kode didalamnya menjadi seperti berikut.</p><pre>package com.example.belajar_flutter_event_channel<br><br>import io.flutter.embedding.android.FlutterActivity<br>import io.flutter.embedding.engine.FlutterEngine<br>import io.flutter.plugin.common.EventChannel<br>import java.util.*<br><br>class MainActivity: FlutterActivity() {<br>    private val channel = &quot;belajar_flutter_event_channel&quot;<br>    private lateinit var timer: Timer<br>    private var eventSink: EventChannel.EventSink? = null<br>    private var counter = 0<br><br>    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {<br>        super.configureFlutterEngine(flutterEngine)<br>        val eventChannel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, channel)<br>        eventChannel.setStreamHandler(object : EventChannel.StreamHandler {<br>            override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {<br>                eventSink = events<br>            }<br><br>            override fun onCancel(arguments: Any?) {<br>                stopTimer()<br>            }<br>        })<br>        startTimer()<br>    }<br><br>    private fun startTimer() {<br>        timer = Timer()<br>        timer.scheduleAtFixedRate(object : TimerTask() {<br>            override fun run() {<br>                this@MainActivity.runOnUiThread <strong>{<br>                    </strong>counter += 1<br>                    eventSink?.success(&quot;Counter from Android: $counter&quot;)<br>                <strong>}<br>            </strong>}<br>        }, 0, 1000)<br>    }<br><br>    private fun stopTimer() {<br>        timer.cancel()<br>    }<br>}</pre><p>Jadi, didalam file tersebut kita ada menggunakan timer miliknya si Android lalu, data counter-nya kita kirim kedalam variable eventSink . Nah, data yang kita kirim ini nantinya akan diterima oleh si Flutter karena si Flutter kan tadinya sudah meng-listen ya. Sehingga jika kita jalankan programnya maka, output untuk di Android akan menjadi seperti berikut.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgfycat.com%2Fifr%2Fidioticsoftblackfootedferret&amp;display_name=Gfycat&amp;url=https%3A%2F%2Fgfycat.com%2Fidioticsoftblackfootedferret&amp;image=https%3A%2F%2Fthumbs.gfycat.com%2FIdioticSoftBlackfootedferret-size_restricted.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=gfycat" width="444" height="998" frameborder="0" scrolling="no"><a href="https://medium.com/media/12d05e44e5ef734cc59b05c2761805ae/href">https://medium.com/media/12d05e44e5ef734cc59b05c2761805ae/href</a></iframe><h4>Buat Event Channel di iOS</h4><p>Untuk yang iOS kita bisa buka file <strong>AppDelegate.swift</strong> dan ubah kode didalamnya menjadi seperti berikut.</p><pre>import UIKit<br>import Flutter<br><br>@UIApplicationMain<br>@objc class AppDelegate: FlutterAppDelegate, FlutterStreamHandler {<br>    private var eventSink: FlutterEventSink?<br>    private var timer: Timer?<br>    private var counter = 0<br>    <br>    override func application(<br>        _ application: UIApplication,<br>        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?<br>    ) -&gt; Bool {<br>        GeneratedPluginRegistrant.register(with: self)<br>        <br>        let controller : FlutterViewController = window?.rootViewController as! FlutterViewController<br>        let eventChannel = FlutterEventChannel(name: &quot;belajar_flutter_event_channel&quot;, binaryMessenger: controller.binaryMessenger)<br>        eventChannel.setStreamHandler(self)<br>        <br>        timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: &quot;increaseCounter&quot;, userInfo: nil, repeats: true)<br>        <br>        return super.application(application, didFinishLaunchingWithOptions: launchOptions)<br>    }<br>    <br>    @objc func increaseCounter() {<br>        counter += <em>1<br></em>        eventSink?(&quot;Counter from iOS: \(counter)&quot;)<br>    }<br>    <br>    public func onListen(withArguments arguments: Any?, eventSink: @escaping FlutterEventSink) -&gt; FlutterError? {<br>        self.eventSink = eventSink<br>        return <em>nil<br>    </em>}<br>    <br>    public func onCancel(withArguments arguments: Any?) -&gt; FlutterError? {<br>        timer?.invalidate()<br>        eventSink = <em>nil<br>        </em>return <em>nil<br>    </em>}<br>}</pre><p>Cara kerjanya sama seperti di Android. Kita ada menggunakan timer-nya si iOS lalu data dari variable counter kita masukkan kedalam variable eventSink.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgfycat.com%2Fifr%2Fminorbruisedadouri&amp;display_name=Gfycat&amp;url=https%3A%2F%2Fgfycat.com%2Fminorbruisedadouri&amp;image=https%3A%2F%2Fthumbs.gfycat.com%2FMinorBruisedAdouri-size_restricted.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=gfycat" width="444" height="970" frameborder="0" scrolling="no"><a href="https://medium.com/media/a130381e7260738a14e7d045f3b530d5/href">https://medium.com/media/a130381e7260738a14e7d045f3b530d5/href</a></iframe><h3>Kesimpulan</h3><p>Jadi, kesimpulannya adalah Flutter memiliki 3 metode untuk bisa melakukan komunikasi dari Dart ke platform-nya masing-masing yaitu, method channel, event channel, dan basic message channel. Event channel memiliki cara kerja mirip seperti stream. Dari Dart cukup meng-listen datanya dan native-nya akan mengirimkan datanya. Untuk source code lengkapnya bisa di Github ya.</p><p><a href="https://github.com/CoderJava/belajar-flutter-event-channel">GitHub - CoderJava/belajar-flutter-event-channel: How to use event channel in Flutter</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d266ec9de02d" width="1" height="1" alt=""><hr><p><a href="https://medium.com/nusanet/flutter-event-channel-d266ec9de02d">Flutter: Event Channel</a> was originally published in <a href="https://medium.com/nusanet">Nusanet Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Server Side Dart with Shelf and Supabase]]></title>
            <link>https://medium.com/nusanet/server-side-dart-with-shelf-and-supabase-c4f4ea5861af?source=rss-b636a005b4e2------2</link>
            <guid isPermaLink="false">https://medium.com/p/c4f4ea5861af</guid>
            <category><![CDATA[dart]]></category>
            <category><![CDATA[backend-development]]></category>
            <category><![CDATA[shelves]]></category>
            <category><![CDATA[dartlang]]></category>
            <category><![CDATA[supabase]]></category>
            <dc:creator><![CDATA[Yudi Setiawan]]></dc:creator>
            <pubDate>Mon, 28 Feb 2022 03:02:49 GMT</pubDate>
            <atom:updated>2022-02-28T03:02:49.718Z</atom:updated>
            <content:encoded><![CDATA[<h4>How create CRUD API with Dart Shelf and Supabase</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uMWkdZDl2zha4hbq8VWDzw.png" /></figure><h3>Pengenalan</h3><h4>Shelf</h4><p>Di tulisan kali ini saya mau bahas lagi mengenai server side menggunakan bahasa Dart. Sebelumnya saya pernah juga membahas mengenai cara membuat CRUD API dengan Dart menggunakan Aqueduct.</p><p><a href="https://medium.com/nusanet/restful-api-dengan-aqueduct-e15dd51b8463">Restful API dengan Aqueduct</a></p><p>Namun, setelah saya lihat ternyata perkembangan dari Aqueduct sudah tidak dilanjutkan lagi. Oleh karena itu, di tulisan ini saya mau bahas lagi mengenai server side menggunakan bahasa Dart tapi, menggunakan plugin yang berbeda pula. Kali ini kita akan mencoba menggunakan plugin yang bernama <strong>Shelf</strong>.</p><p><a href="https://github.com/dart-lang/shelf">GitHub - dart-lang/shelf: Web server middleware for Dart</a></p><p>Jadi, Shelf ini merupakan plugin yang dibuat langsung oleh tim Dart. Dan saat ini saya lihat commit-an terakhir mereka sekitar 17 hari yang lalu. Yang menandakan bahwa ini plugin masih dikembangkan (mudah-mudahan saja 😄)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/950/1*Dz3P4ybaVAhSa1NIhTpvLA.png" /><figcaption>Commit-an terakhir 17 hari yang lalu</figcaption></figure><p>Tapi, berhubung ini plugin dibuat oleh tim Dart-nya. Saya jadi semakin penasaran gimana cara membuat server side menggunakan plugin tersebut.</p><h4>Supabase</h4><p>Nah, kalau bahas server side rasanya kurang lengkap kalau kita tidak bahas juga mengenai database-nya. Jadi, biar lebih kece untuk server side-nya nanti kita akan pakai database miliknya si Supabase ya.</p><p><a href="https://supabase.com/">Supabase | The Postgres Development Platform.</a></p><p>Jadi, Supabase ini hampir mirip seperti Firebase gitu ya. Cuma kalau dilihat dari halaman depannya saat ini mereka bilang kalau Supabase ini salah satu alternatif dari Firebase yang open source. Dan biasanya nih produk-produk yang open source begini prospek kedepannya bakalan cerah apalagi fungsinya hampir mirip atau bahkan melebihi expectation dari produk yang serupa.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AK94kiRESCUzTLwdAEtbKQ.png" /><figcaption>Halaman depan Supabase</figcaption></figure><p>Dan setelah saya cek di pub.dev ternyata plugin supabase untuk projek Dart sudah ada yang buat.</p><p><a href="https://pub.dev/packages/supabase">supabase | Dart Package</a></p><p>Btw, perlu dicatat ya bahwa plugin diatas merupakan plugin untuk projek Dart bukan untuk Flutter. Kalau Supabase untuk Flutter bisa coba yang ini.</p><p><a href="https://pub.dev/packages/supabase_flutter">supabase_flutter | Flutter package</a></p><p>Tapi, untuk saat ini kita nggak akan bahas mengenai Supabase untuk Flutter. Fokus kita di tulisan ini adalah bagaimana cara membuat CRUD API menggunakan Shelf dan Supabase.</p><h3>Pembuatan Projek</h3><h4>Buat Projek Supabase</h4><p>Untuk langkah pertamanya, kita perlu buat akun terlebih dahulu di Supabase.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-L090uH4jAR_VVP13pv92w.png" /><figcaption>Sign in di Supabase</figcaption></figure><p>Selanjutnya, kita perlu buat sebuah projek di Supabase.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ioNx7yBS-nN0biZe81ndtA.png" /><figcaption>Buat projek di Supabase</figcaption></figure><p>Kemudian, kita perlu isi sebuah form untuk membuat projeknya.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fwswv0ZXhZ96-Nyej_DbOQ.png" /><figcaption>Form create new project</figcaption></figure><p>Setelah itu, kita perlu menunggu beberapa menit sampai projeknya benar-benar berhasil dibuat.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*My7fDW-6xUAqoW49T0vvCQ.png" /><figcaption>Menunggu projeknya berhasil dibuat</figcaption></figure><p>Jika sudah selesai seharusnya tampilan dashboard-nya menjadi seperti gambar berikut ini ya.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cG5CGgXPBEgacs8j-HvlnQ.png" /><figcaption>Projek berhasil dibuat di Supabase</figcaption></figure><h4>Buat Table Profile</h4><p>Masih di Supabase. Kita perlu buat table baru dengan nama <strong>profile</strong> di Database. Caranya, kita tekan saja menu Database lalu, tekan tombol New.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3oOTCZMxMgLT3OoupNhlbQ.png" /><figcaption>Buat table baru di Supabase</figcaption></figure><p>Kemudian, kita atur kolomnya menjadi seperti gambar berikut.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0I_YpYXIc9Pu4669ROSg9A.png" /><figcaption>Buat table baru dengan nama profile</figcaption></figure><p>Oya, pada kolom diatas pastikan yang nullable hanya kolom email dan phone_number. Kemudian, kita coba insert row di table tersebut dengan cara tekan tombol <strong>Insert row</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Gdwa4_LMaWDpXBNhu-PKVQ.png" /><figcaption>Insert row di table profile</figcaption></figure><p>Kemudian, kita isi datanya.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XEUx-7FOBD_GJWxZYJO6_A.png" /><figcaption>Isi data table profile</figcaption></figure><p>Selanjutnya, tekan <strong>Save</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*r9TshD8lALUkfAD8ywB2DA.png" /><figcaption>Datanya sudah masuk ya</figcaption></figure><h4>Buat Projek Dart</h4><p>Oke, sekarang kita beralih ke Dart. Untuk langkah pertama di sisi Dart, kita perlu buat projek baru Dart-nya. Untuk membuatnya kita bisa menggunakan IDE atau command line. Biar seragam saya contohkan menggunakan command line saja ya.</p><pre>dart create dart_profile</pre><p>Setelah itu kita buka projeknya.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/267/1*8gYoq1AWFmFGLFRM8sU-cw.png" /><figcaption>Struktur projek Dart</figcaption></figure><p>Kemudian, kita perlu rename <strong>dart_profile.dart </strong>menjadi <strong>main.dart</strong>.</p><h4>Atur pubspec.yaml</h4><p>Langkah berikutnya kita perlu menambahkan plugin berikut kedalam <strong>pubspec.yaml</strong>.</p><pre>name: dart_profile<br>description: A simple command-line application.<br>version: 1.0.0<br><br>environment:<br>  sdk: &#39;&gt;=2.15.1 &lt;3.0.0&#39;<br><br><strong>dependencies:<br>  args: ^2.3.0<br>  shelf: ^1.2.0<br>  shelf_router: ^1.1.2<br>  supabase: ^0.2.14<br>  jiffy: ^5.0.0<br>  json_annotation: ^4.4.0<br><br>dev_dependencies:<br>  lints: ^1.0.0<br>  http: ^0.13.0<br>  build_runner: ^2.1.7<br>  json_serializable: ^6.1.4</strong></pre><p>Selanjutnya, kita jalankan perintah dart pub get untuk mengunduh dependency-nya.</p><h4>Buat Class Model</h4><p>Biar lebih gampang kita akan membuat class model dari table <strong>profile. </strong>Untuk membuatnya, silakan buat file baru dengan nama <strong>profile.dart</strong> dan isikan dengan kode berikut.</p><pre>import &#39;package:json_annotation/json_annotation.dart&#39;;<br><br>part &#39;profile.g.dart&#39;;<br><br>@JsonSerializable()<br>class Profile {<br>  @JsonKey(name: &#39;id&#39;)<br>  final int? id;<br>  @JsonKey(name: &#39;created_at&#39;)<br>  final String? createdAt;<br>  @JsonKey(name: &#39;full_name&#39;)<br>  final String? fullName;<br>  @JsonKey(name: &#39;email&#39;)<br>  final String? email;<br>  @JsonKey(name: &#39;phone_number&#39;)<br>  final String? phoneNumber;<br>  @JsonKey(name: &#39;birth_date&#39;)<br>  final String? birthDate;<br><br>  Profile({<br>    this.id,<br>    this.createdAt,<br>    this.fullName,<br>    this.email,<br>    this.phoneNumber,<br>    this.birthDate,<br>  });<br><br>  factory Profile.fromJson(Map&lt;String, dynamic&gt; json) =&gt; _$ProfileFromJson(json);<br><br>  Map&lt;String, dynamic&gt; toJson() =&gt; _$ProfileToJson(this);<br><br>  @override<br>  String toString() {<br>    return &#39;Profile{id: $id, createdAt: $createdAt, fullName: $fullName, &#39;<br>        &#39;email: $email, phoneNumber: $phoneNumber, birthDate: $birthDate}&#39;;<br>  }<br>}</pre><p>Kemudian, jalankan perintah dart run build_runner build untuk membuat file generator-nya. Jika berhasil seharusnya ada 1 file baru yang terbentuk secara otomatis dengan nama <strong>profile.g.dart</strong>.</p><h4>Buat Fungsi Supabase</h4><p>Langkah berikutnya kita akan membuat fungsi CRUD kedalam database Supabase. Caranya, kita buat file baru dengan nama <strong>supabase_helper.dart</strong>. Kemudian, kita isi kode didalamnya menjadi seperti berikut.</p><pre>class SupabaseHelper {<br>  final supabaseUrl = &#39;&#39;;<br>  final supabaseKey = &#39;&#39;;<br>  final profileTableName = &#39;profile&#39;;<br>  late SupabaseClient supabaseClient;<br><br>  SupabaseHelper() {<br>    supabaseClient = SupabaseClient(supabaseUrl, supabaseKey);<br>  }<br><br>  Map&lt;String, dynamic&gt; getTemplateErrorMessage({<br>    String title = &#39;Info&#39;,<br>    String message = &#39;Data not found&#39;,<br>  }) {<br>    return {<br>      &#39;title&#39;: title,<br>      &#39;message&#39;: message,<br>    };<br>  }<br><br>  Future&lt;Response&gt; getAllProfile() async {<br>    // <em>TODO: Tampilkan semua data profile<br>    </em>return Response.ok(&#39;get all profile&#39;);<br>  }<br><br>  Future&lt;Response&gt; getProfileById(String id) async {<br>    // <em>TODO: Tampilkan profile berdasarkan id yang diberikan<br>    </em>return Response.ok(&#39;get profile by id&#39;);<br>  }<br><br>  Future&lt;Response&gt; createProfile(String bodyRequest) async {<br>    // <em>TODO: Masukkan data profile baru kedalam table<br>    </em>return Response.ok(&#39;create profile&#39;);<br>  }<br><br>  Future&lt;Response&gt; updateProfile(String id, String bodyRequest) async {<br>    // <em>TODO: Update data profile kedalam table<br>    </em>return Response.ok(&#39;update profile&#39;);<br>  }<br><br>  Future&lt;Response&gt; deleteProfile(String id) async {<br>    // <em>TODO: Hapus data profile dari table<br>    </em>return Response.ok(&#39;delete profile&#39;);<br>  }<br>}</pre><blockquote>Untuk nilai <strong>supabaseUrl</strong> dan <strong>supabaseKey</strong> bisa kamu ambil dari Supabase. Caranya, pilih menu <strong>Settings-Project settings-API</strong>. Salin nilai anon public kedalam variable <strong>supabaseKey</strong>. Dan salin juga nilai URL kedalam variable <strong>supabaseUrl</strong>.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mMkaIuL-TMZplaAdKS4nKg.png" /><figcaption>Cara mendapatkan nilai dari variable supabaseKey dan supabaseUrl</figcaption></figure><h4>Buat Fungsi Menampilkan Semua Data Profile</h4><p>Masih didalam file <strong>supabase_helper.dart</strong> kita akan membuat fungsi yang menampilkan semua data profile. Untuk membuatnya, silakan kita ubah kode berikut.</p><pre>Future&lt;Response&gt; getAllProfile() async {<br>  // <em>TODO: Tampilkan semua data profile<br>  </em>return Response.ok(&#39;get all profile&#39;);<br>}</pre><p>Menjadi seperti berikut.</p><pre>Future&lt;Response&gt; getAllProfiles() async {<br>  try {<br>    final response = await supabaseClient.from(profileTableName).select().execute();<br>    late Map&lt;String, dynamic&gt; responseJson;<br>    if (response.data == null) {<br>      responseJson = getTemplateErrorMessage();<br>      return Response.notFound(jsonEncode(responseJson));<br>    }<br>    final listProfiles = List.from(response.data).map((e) =&gt; Profile.fromJson(e)).toList();<br>    responseJson = {<br>      &#39;data&#39;: listProfiles,<br>    };<br>    return Response.ok(jsonEncode(responseJson));<br>  } catch (error) {<br>    return Response.internalServerError(<br>      body: getTemplateErrorMessage(<br>        title: &#39;Error&#39;,<br>        message: error.toString(),<br>      ),<br>    );<br>  }<br>}</pre><p>Bisa kita lihat pada kode diatas bahwa kita ada handle untuk beberapa case misal datanya tidak ada, datanya ada dan error-nya pun juga kita tangkap dalam blok try-catch . Jadi, seumpama ada case yang tidak ter-handle bisa langsung masuk kedalam catch -nya.</p><h4>Buat Fungsi Menampilkan Profile Berdasarkan ID</h4><p>Langkah berikutnya kita akan membuat fungsi yang berguna untuk menampilkan sebuah data profile berdasarkan ID yang diberikan. Untuk membuatnya silakan ubah kode berikut.</p><pre>Future&lt;Response&gt; getProfileById(String id) async {<br>  // <em>TODO: Tampilkan profile berdasarkan id yang diberikan<br>  </em>return Response.ok(&#39;get profile by id&#39;);<br>}</pre><p>Menjadi seperti berikut.</p><pre>Future&lt;Response&gt; getProfileById(String id) async {<br>  try {<br>    final response = await supabaseClient.from(profileTableName).select().eq(&#39;id&#39;, id).execute();<br>    final listProfiles = List.from(response.data).map((e) =&gt; Profile.fromJson(e)).toList();<br>    late Map&lt;String, dynamic&gt; responseJson;<br>    if (listProfiles.isEmpty) {<br>      responseJson = getTemplateErrorMessage();<br>      return Response.notFound(jsonEncode(responseJson));<br>    }<br>    responseJson = listProfiles.first.toJson();<br>    return Response.ok(jsonEncode(responseJson));<br>  } catch (error) {<br>    return Response.internalServerError(<br>      body: getTemplateErrorMessage(<br>        title: &#39;Error&#39;,<br>        message: error.toString(),<br>      ),<br>    );<br>  }<br>}</pre><h4>Buat Fungsi Tambah Data Profile</h4><p>Fungsi ini berguna untuk menambahkan data profile baru kedalam table. Untuk membuatnya ubah kode berikut.</p><pre>Future&lt;Response&gt; createProfile(String bodyRequest) async {<br>  // <em>TODO: Masukkan data profile baru kedalam table<br>  </em>return Response.ok(&#39;create profile&#39;);<br>}</pre><p>Menjadi seperti berikut.</p><pre>Future&lt;Response&gt; createProfile(String bodyRequest) async {<br>  try {<br>    final profile = Profile.fromJson(jsonDecode(bodyRequest)).toJson()<br>      ..remove(&#39;id&#39;)<br>      ..remove(&#39;created_at&#39;);<br>    final response = await supabaseClient.from(profileTableName).insert(profile).execute();<br>    late Map&lt;String, dynamic&gt; responseJson;<br>    if (response.data == null) {<br>      responseJson = getTemplateErrorMessage(message: &#39;Create profile failed&#39;);<br>      return Response.internalServerError(body: jsonEncode(responseJson));<br>    }<br>    final listData = List.from(response.data).map((e) =&gt; Profile.fromJson(e)).toList();<br>    if (listData.isEmpty) {<br>      responseJson = getTemplateErrorMessage();<br>      return Response.notFound(jsonEncode(responseJson));<br>    }<br>    responseJson = listData.first.toJson();<br>    return Response(201, body: jsonEncode(responseJson));<br>  } catch (error) {<br>    return Response.internalServerError(<br>      body: getTemplateErrorMessage(<br>        title: &#39;Error&#39;,<br>        message: error.toString(),<br>      ),<br>    );<br>  }<br>}</pre><p>Mungkin kamu bertanya-tanya mengapa respon dari Supabase selalu kita handle dalam bentuk List. Karena, tipe data dari response.data berbentuk List. Makanya, kita selalu handle-nya dalam bentuk List yang kemudian, kita mapping-kan kedalam class model Profile .</p><h4>Buat Fungsi Ubah Data Profile</h4><p>Fungsi ini berguna untuk mengubah data yang sudah ada atau sering kita sebut dengan edit data. Untuk membuatnya silakan ubah kode berikut.</p><pre>Future&lt;Response&gt; updateProfile(String id, String bodyRequest) async {<br>  // <em>TODO: Update data profile kedalam table<br>  </em>return Response.ok(&#39;update profile&#39;);<br>}</pre><p>Menjadi seperti berikut.</p><pre>Future&lt;Response&gt; updateProfile(String id, String bodyRequest) async {<br>  try {<br>    final profile = Profile.fromJson(jsonDecode(bodyRequest)).toJson()<br>      ..remove(&#39;id&#39;)<br>      ..remove(&#39;created_at&#39;);<br>    final response = await supabaseClient.from(profileTableName).update(profile).eq(&#39;id&#39;, id).execute();<br>    late Map&lt;String, dynamic&gt; responseJson;<br>    if (response.data == null) {<br>      responseJson = getTemplateErrorMessage(message: &#39;Update profile failed&#39;);<br>      return Response.internalServerError(body: jsonEncode(responseJson));<br>    }<br>    final listData = List.from(response.data).map((e) =&gt; Profile.fromJson(e)).toList();<br>    if (listData.isEmpty) {<br>      responseJson = getTemplateErrorMessage();<br>      return Response.notFound(jsonEncode(responseJson));<br>    }<br>    responseJson = listData.first.toJson();<br>    return Response.ok(jsonEncode(responseJson));<br>  } catch (error) {<br>    return Response.internalServerError(<br>      body: getTemplateErrorMessage(<br>        title: &#39;Error&#39;,<br>        message: error.toString(),<br>      ),<br>    );<br>  }<br>}</pre><h4>Buat Fungsi Hapus Data Profile</h4><p>Fungsi ini berguna untuk menghapus data profile yang ada. Untuk membuatnya silakan ubah kode berikut.</p><pre>Future&lt;Response&gt; deleteProfile(String id) async {<br>  // <em>TODO: Hapus data profile dari table<br>  </em>return Response.ok(&#39;delete profile&#39;);<br>}</pre><p>Menjadi seperti berikut.</p><pre>Future&lt;Response&gt; deleteProfile(String id) async {<br>  try {<br>    final response = await supabaseClient.from(profileTableName).delete().eq(&#39;id&#39;, id).execute();<br>    late Map&lt;String, dynamic&gt; responseJson;<br>    if (response.data == null) {<br>      responseJson = getTemplateErrorMessage(message: &#39;Delete profile failed&#39;);<br>      return Response.internalServerError(body: jsonEncode(responseJson));<br>    }<br>    final listData = List.from(response.data).map((e) =&gt; Profile.fromJson(e)).toList();<br>    if (listData.isEmpty) {<br>      responseJson = getTemplateErrorMessage();<br>      return Response.notFound(jsonEncode(responseJson));<br>    }<br>    responseJson = listData.first.toJson();<br>    return Response.ok(jsonEncode(responseJson));<br>  } catch (error) {<br>    return Response.internalServerError(<br>      body: getTemplateErrorMessage(<br>        title: &#39;Error&#39;,<br>        message: error.toString(),<br>      ),<br>    );<br>  }<br>}</pre><h4>Buat Routing Endpoint-nya</h4><p>Setelah fungsi CRUD-nya selesai kita buat. Maka, langkah selanjutnya adalah kita perlu define route endpoint-nya. Untuk membuatnya silakan buat file baru dengan nama <strong>profile_controller.dart</strong> dan isikan dengan kode berikut.</p><pre>import &#39;package:shelf/shelf.dart&#39;;<br>import &#39;package:shelf_router/shelf_router.dart&#39;;<br><br>import &#39;supabase_helper.dart&#39;;<br><br>class ProfileController {<br>  Handler get handler {<br>    final router = Router();<br>    final supabaseHelper = SupabaseHelper();<br><br>    router.get(&#39;/profile&#39;, (Request request) async {<br>      return supabaseHelper.getAllProfiles();<br>    });<br><br>    router.get(&#39;/profile/&lt;id&gt;&#39;, (Request request, String id) async {<br>      return supabaseHelper.getProfileById(id);<br>    });<br><br>    router.post(&#39;/profile&#39;, (Request request) async {<br>      final strBodyRequest = await request.readAsString();<br>      return supabaseHelper.createProfile(strBodyRequest);<br>    });<br><br>    router.put(&#39;/profile/&lt;id&gt;&#39;, (Request request, String id) async {<br>      final strBodyRequest = await request.readAsString();<br>      return supabaseHelper.updateProfile(id, strBodyRequest);<br>    });<br><br>    router.delete(&#39;/profile/&lt;id&gt;&#39;, (Request request, String id) async {<br>      return supabaseHelper.deleteProfile(id);<br>    });<br><br>    router.all(&#39;/&lt;ignored|.*&gt;&#39;, (Request request) async {<br>      return Response.notFound(&#39;Page not found&#39;);<br>    });<br><br>    return router;<br>  }<br>}</pre><h4>Update File main.dart</h4><p>Langkah terakhir adalah kita perlu ubah kode didalam file <strong>main.dart </strong>menjadi seperti berikut.</p><pre>import &#39;package:shelf/shelf.dart&#39;;<br>import &#39;package:shelf/shelf_io.dart&#39;;<br><br>import &#39;profile_controller.dart&#39;;<br><br>void main(List&lt;String&gt; arguments) async {<br>  final handler = const Pipeline().addMiddleware(logRequests()).addHandler(ProfileController().handler);<br>  await serve(handler, &#39;localhost&#39;, 8080);<br>  print(&#39;Server is running&#39;);<br>}</pre><p>Setelah selesai semuanya mari kita jalankan programnya menggunakan perintah berikut.</p><pre>dart run bin/main.dart</pre><h4>Testing</h4><p>Berikut adalah video hasil testing dari semua endpoint yang sudah kita buat.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FiR7fGMJfDlM%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DiR7fGMJfDlM&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FiR7fGMJfDlM%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="640" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/9dfde9a17536fd64dc161a7a919a8fa5/href">https://medium.com/media/9dfde9a17536fd64dc161a7a919a8fa5/href</a></iframe><h3>Kesimpulan</h3><p>Jadi kesimpulannya adalah bahwa penggunaan plugin Shelf ini jika kita lihat ternyata cukuplah mudah dan familiar. Untuk Supabase-nya menurut saya bagus ya. Database-nya itu loh mereka adopsi Postgres. Jadi, agak sedikit berbeda dengan database si Firebase yang bentuknya lebih ke NoSQL. Dan Supabase ini menurut saya bisa menjadi alternatif selain dari Firebase. Seperti biasa untuk source code lengkapnya bisa dilihat di Github ya.</p><p><a href="https://github.com/CoderJava/dart-profile">GitHub - CoderJava/dart-profile: Server side dart with Shelf and Supabase</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c4f4ea5861af" width="1" height="1" alt=""><hr><p><a href="https://medium.com/nusanet/server-side-dart-with-shelf-and-supabase-c4f4ea5861af">Server Side Dart with Shelf and Supabase</a> was originally published in <a href="https://medium.com/nusanet">Nusanet Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Flutter: Face Detection with ML Kit]]></title>
            <link>https://medium.com/nusanet/flutter-face-detection-with-ml-kit-9cf966fd3925?source=rss-b636a005b4e2------2</link>
            <guid isPermaLink="false">https://medium.com/p/9cf966fd3925</guid>
            <category><![CDATA[facedetection]]></category>
            <category><![CDATA[flutter-app-development]]></category>
            <category><![CDATA[flutter-plugin]]></category>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[ml-kit]]></category>
            <dc:creator><![CDATA[Yudi Setiawan]]></dc:creator>
            <pubDate>Sun, 13 Feb 2022 10:03:56 GMT</pubDate>
            <atom:updated>2022-02-13T11:41:29.528Z</atom:updated>
            <content:encoded><![CDATA[<h4>How to create face detection with Google ML Kit</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DvmcCgc9ZSnMXsdPm3pnoA.png" /></figure><h3>Pengenalan</h3><p>Di tulisan kali ini kita akan membahas mengenai cara membuat pendeteksi wajah menggunakan layanan dari Google ML Kit. Btw, di tulisan-tulisan saya yang sebelumnya saya pernah bahas juga mengenai topik yang seperti ini namun bukan deteksi wajah melainkan deteksi penggunaan masker menggunakan TensorFlow Lite. Kalau kamu tertarik, tulisan tersebut bisa kamu baca pada link berikut ini ya.</p><p><a href="https://medium.com/nusanet/aplikasi-pendeteksi-masker-di-flutter-dengan-tensorflow-lite-10ab70fe3720">Aplikasi Pendeteksi Masker di Flutter dengan TensorFlow Lite</a></p><p>Untuk membuat face detection di Flutter tidaklah terlalu sulit karena ada banyak sekali third party diluar sana yang sudah bagus-bagus mulai dari yang gratisan sampai yang berbayar pun juga ada. Tergantung dari kebutuhan kita mau buat pendeteksi wajah yang gimana. Berhubung ditulisan kali ini kita mau belajar maka, saya akan pakai yang gratisan saja ya. Untuk yang gratisan kita bisa pakai third party miliknya si Google yang bernama Google ML Kit. Dan kalau kita cek di pub.dev itu plugin-nya sudah lumayan banyak yang buat ya.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*e0HxZ1Ze6iY6e2XO_s2OSQ.png" /><figcaption>List plugin google ml kit di pub.dev</figcaption></figure><p>Ya, walaupun kalau kita perhatikan itu sebenarnya bukan milik official-nya langsung dari Google tapi nggak apa-apa lah. Itulah gunanya komunitas. Dan sedikit info aja nih kalau plugin yang miliknya Firebase itu plugin-nya sudah discontinued ya. Ini plugin official-nya milik si Firebase.</p><p><a href="https://pub.dev/packages/firebase_ml_vision">firebase_ml_vision | Flutter Package</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/919/1*_uxG3Q3ia1YL8Mz3qO1spA.png" /><figcaption>Plugin firebase ml vision sudah discontinued</figcaption></figure><p>Bisa kita lihat sendiri bahwa plugin firebase ml vision sudah discontinued dan mereka sarankan untuk pakai plugin google_ml_kit yang ini.</p><p><a href="https://pub.dev/packages/google_ml_kit">google_ml_kit | Flutter Package</a></p><p>Tapi, saya sudah coba duluan itu plugin <a href="https://pub.dev/packages/google_ml_kit">google_ml_kit</a> dan sayangnya plugin tersebut menyebabkan plugin <a href="https://pub.dev/packages/image_picker">image_picker</a> tidak berfungsi dengan baik di iOS. Jadi, sorry to say bahwa untuk di tulisan kali ini saya tidak akan memakai plugin tersebut ya. Dan yang kena bukan cuma saya saja. Yang lain juga kena loh. Ini salah satu thread-nya di Stack Overflow.</p><p><a href="https://stackoverflow.com/questions/70714106/flutter-imagepicker-await-picker-pickimagesource-imagesource-gallery-doesn/70849699#comment125242693_70714106">Flutter: ImagePicker: await picker.pickImage(source: ImageSource.gallery) doesn&#39;t return</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/734/1*hfZpFZoWce7X8GNH_wCKhw.png" /></figure><p>Jadi, berhubung plugin google_ml_kit bermasalah jadi kita akan pakai plugin yang ini saja ya.</p><p><a href="https://pub.dev/packages/google_ml_vision">google_ml_vision | Flutter Package</a></p><h3>Pembuatan Aplikasi</h3><p>Untuk langkah pertamanya silakan kita buat projek baru pada text editor masing-masing dengan nama <strong>flutter_face_detection_demo</strong>. Dan pastikan projeknya kita buat target-nya hanya ke Android dan iOS karena untuk platform web dan desktop tidak akan kita buat ya karena dari plugin-nya juga belum support.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/830/1*PB7ZQOfkQyEQRrMl9EUb8Q.png" /><figcaption>Buat projek baru dengan nama flutter_face_detection_demo</figcaption></figure><h4>Setup pubspec.yaml</h4><p>Langkah selanjutnya kita perlu atur file <strong>pubspec.yaml</strong>-nya dan ubah bagian dependencies-nya menjadi seperti berikut.</p><pre>...<br>dependencies:<br>  flutter:<br>    sdk: flutter<br><br>  <em># The following adds the Cupertino Icons font to your application.<br>  # Use with the CupertinoIcons class for iOS style icons.<br>  </em>cupertino_icons: ^1.0.2<br><br>  <strong><em># Flutter plugin for Google ML Kit on-device vision apis. It includes barcode scanning,<br>  # image labeling, text recognition and face detection.<br>  </em>google_ml_vision: ^0.0.7<br><br>  <em># Flutter plugin for selecting images from the Android and iOS image library, and<br>  # taking new pictures with the camera.<br>  </em>image_picker: ^0.8.4+8<br></strong><br>...</pre><p>Bisa kita lihat diatas bahwa kita membutuhkan beberapa plugin untuk membuat aplikasi pada projek kali ini. Dan beberapa plugin tersebut adalah sebagai berikut:</p><ol><li>google_ml_vision<br>Plugin ini kita butuhkan untuk membuat fitur face detection-nya.</li><li>image_picker<br>Plugin ini kita butuhkan untuk mengambil foto dari kamera dan galeri.</li></ol><p>Selanjutnya, jangan lupa ya jalankan perintah flutter pub get untuk mengunduh paket-paket dependency-nya.</p><h4>Setup Plugin Google ML Vision dan Image Picker</h4><p>Setelah kita berhasil mengunduh paket-paket dependency-nya. Langkah berikutnya adalah kita perlu setup di sisi Android dan iOS-nya agar plugin Google ML Vision-nya dan Image Picker bisa berjalan dengan baik.</p><h4>Android</h4><p>Untuk di sisi Android kita hanya perlu mengubah nilai minSdkVersion-nya menjadi 19. Kamu bisa ubah nilainya didalam file <strong>android/app/build.gradle</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/885/1*hYu5y34DQ0Uz2eNDEk1i7A.png" /><figcaption>Ubah nilai minSdkVersion menjadi 19</figcaption></figure><h4>iOS</h4><p>Untuk di sisi iOS kita hanya perlu mengubah iOS deployment target-nya menjadi 11.0</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/871/1*GpBG7ya88ZH_jQFgyAL1ow.png" /><figcaption>Ubah iOS Deployment Target menjadi 11.0</figcaption></figure><p>Dan karena di tulisan kali ini kita mau buat app-nya agar ambil gambar dari galeri maka, kita perlu menambahkan permission NSPhotoLibraryUsageDescription kedalam file <strong>Info.plist</strong></p><pre>...<br>&lt;true/&gt;<br>   &lt;key&gt;NSPhotoLibraryUsageDescription&lt;/key&gt;<br>   &lt;string&gt;Butuh permission untuk mengambil gambar dari galeri&lt;/string&gt;   <br>...</pre><h4>Buat UI Utama</h4><p>Langkah berikutnya adalah kita perlu buat halaman utama dari app kita. Buka file <strong>main.dart</strong> dan ubah kode didalamnya menjadi seperti berikut.</p><pre>import &#39;dart:io&#39;;<br><br>import &#39;package:flutter/material.dart&#39;;<br><br>void main() {<br>  runApp(const MyApp());<br>}<br><br>class MyApp extends StatelessWidget {<br>  const MyApp({Key? key}) : super(key: key);<br><br>  @override<br>  Widget build(BuildContext context) {<br>    return MaterialApp(<br>      title: &#39;Flutter Face Detection&#39;,<br>      theme: ThemeData(<br>        primarySwatch: Colors.<em>blue</em>,<br>      ),<br>      home: const MyHomePage(title: &#39;Flutter Face Detection&#39;),<br>    );<br>  }<br>}<br><br>class MyHomePage extends StatefulWidget {<br>  final String title;<br><br>  const MyHomePage({<br>    Key? key,<br>    required this.title,<br>  }) : super(key: key);<br><br>  @override<br>  _MyHomePageState createState() =&gt; _MyHomePageState();<br>}<br><br>class _MyHomePageState extends State&lt;MyHomePage&gt; {<br>  var pathPhoto = &#39;&#39;;<br>  var isLoading = false;<br><br>  @override<br>  Widget build(BuildContext context) {<br>    return Scaffold(<br>      appBar: AppBar(<br>        title: Text(widget.title),<br>      ),<br>      floatingActionButton: FloatingActionButton(<br>        child: const Icon(Icons.<em>add</em>),<br>        onPressed: () async {<br>          // <em>TODO: Ambil foto dari kamera atau galeri menggunakan plugin image_picker<br>        </em>},<br>      ),<br>      body: buildWidgetBody(),<br>    );<br>  }<br><br>  Widget buildWidgetBody() {<br>    <em>/// Tampilkan loading ditengah-tengah layar<br>    </em>if (isLoading) {<br>      return const Center(<br>        child: CircularProgressIndicator.adaptive(),<br>      );<br>    }<br><br>    <em>/// Tampilkan info bahwa tidak ada foto yang diambil<br>    </em>if (pathPhoto.isEmpty) {<br>      return const Center(<br>        child: Padding(<br>          padding: EdgeInsets.all(16.0),<br>          child: Text(<br>            &#39;Silakan ambil foto dulu ya\n&#39;<br>            &#39;dengan cara tekan tombol tambah di bagian kanan bawah&#39;,<br>            textAlign: TextAlign.center,<br>          ),<br>        ),<br>      );<br>    }<br><br>    <em>/// Tampilkan foto yang kita ambil dari kamera atau galeri<br>    </em>return Center(<br>      child: Image.file(<br>        File(<br>          pathPhoto,<br>        ),<br>      ),<br>    );<br>  }<br><br>  void showDialogMessage(String message) {<br>    // <em>TODO: Tampilkan pesan dialog<br>  </em>}<br>}</pre><p>Sekarang coba kita jalankan programnya maka, outputnya akan menjadi seperti berikut.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*myU13AP27NO9xYsULbahog.png" /><figcaption>Buat UI utama</figcaption></figure><h4>Buat Fungsi Tampilkan Pesan Dialog</h4><p>Sebelum kita membuat fungsi untuk mengambil gambar-nya. Kita perlu lengkapi dulu nih fungsi showDialogMessage . Silakan kita ubah kode berikut ini</p><pre>void showDialogMessage(String message) {<br>  // <em>TODO: Tampilkan pesan dialog<br></em>}</pre><p>Menjadi seperti berikut.</p><pre>void showDialogMessage(String message) {<br>  if (Platform.<em>isIOS</em>) {<br>    showCupertinoDialog(<br>      context: context,<br>      builder: (context) {<br>        return CupertinoAlertDialog(<br>          title: const Text(&#39;Info&#39;),<br>          content: Text(message),<br>          actions: [<br>            CupertinoDialogAction(<br>              child: const Text(&#39;Ok&#39;),<br>              onPressed: () =&gt; Navigator.<em>pop</em>(context),<br>            ),<br>          ],<br>        );<br>      },<br>    );<br>  } else {<br>    showDialog(<br>      context: context,<br>      builder: (context) {<br>        return AlertDialog(<br>          title: const Text(&#39;Info&#39;),<br>          content: Text(message),<br>          actions: [<br>            TextButton(<br>              onPressed: () =&gt; Navigator.<em>pop</em>(context),<br>              child: const Text(&#39;OK&#39;),<br>            ),<br>          ],<br>        );<br>      },<br>    );<br>  }<br>}</pre><h4>Buat Fitur Face Detection</h4><p>Langkah berikutnya adalah kita masuk ke bagian pembuatan fitur face detection-nya ya. Untuk membuatnya silakan kita ubah bagian kode berikut ini.</p><pre>floatingActionButton: FloatingActionButton(<br>  child: const Icon(Icons.<em>add</em>),<br>  onPressed: () async {<br>    <strong>// <em>TODO: Ambil foto dari kamera atau galeri menggunakan plugin image_picker</em></strong><em><br>  </em>},<br>),</pre><p>Menjadi seperti berikut.</p><pre>floatingActionButton: FloatingActionButton(<br>  child: const Icon(Icons.<em>add</em>),<br>  <strong>onPressed: () async {<br>    <em>/// Ambil gambar dari galeri<br>    </em>final image = await ImagePicker().pickImage(source: ImageSource.gallery);<br><br>    <em>/// Pastikan bahwa gambarnya valid<br>    </em>if (image != null) {<br>      <em>/// Tampilkan loading<br>      </em>setState(() =&gt; isLoading = true);<br><br>      <em>/// Ambil path image-nya<br>      </em>pathPhoto = image.path;<br><br>      <em>/// Buat objek GoogleVisionImage dengan datanya adalah gambar yang kita ambil dari galeri<br>      </em>final googleVisionImage = GoogleVisionImage.fromFilePath(pathPhoto);<br><br>      <em>/// Buat objek FaceDetector<br>      </em>final faceDetector = GoogleVision.<em>instance</em>.faceDetector();<br><br>      <em>/// Jalankan proses untuk deteksi wajahnya<br>      </em>final faces = await faceDetector.processImage(googleVisionImage);<br><br>      <em>/// Tampilkan pesan apakah wajahnya terdeteksi atau tidak<br>      </em>if (faces.isEmpty) {<br>        showDialogMessage(&#39;Wajah tidak terdeteksi&#39;);<br>      } else {<br>        showDialogMessage(&#39;Wajah terdeteksi&#39;);<br>      }<br><br>      <em>/// Sembunyikan loading<br>      </em>setState(() =&gt; isLoading = false);<br>    }<br>  }</strong>,<br>),</pre><p>Berikut adalah output dari fitur face detection-nya.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FPNwxsT0PVXk%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DPNwxsT0PVXk&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FPNwxsT0PVXk%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="640" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/97e996b69a8722a96a2f465e235b2194/href">https://medium.com/media/97e996b69a8722a96a2f465e235b2194/href</a></iframe><p>Nah, fitur face detection-nya sudah berfungsi ya. Tapi, kalau face detection-nya cuma sekedar pesan dialog gitu kurang keren lah. Biar lebih keren kita perlu tambah sedikit fitur nih. Jadi, kita akan beri bingkai dimana letak wajahnya dari foto yang kita upload.</p><h4>Buat Fitur Bingkai Wajah Yang Terdeteksi</h4><p>Untuk membuatnya kita perlu menggunakan CustomPainter untuk menggambar bingkai-nya. Kita buat file baru dengan nama <strong>face_detector_painter.dart</strong> dan isi dengan kode berikut.</p><pre>import &#39;package:flutter/material.dart&#39;;<br>import &#39;package:google_ml_vision/google_ml_vision.dart&#39;;<br><br>class FaceDetectorPainter extends CustomPainter {<br>  final Size imageSize;<br>  final List&lt;Face&gt; faces;<br>  final bool isReflection;<br><br>  const FaceDetectorPainter(<br>    this.imageSize,<br>    this.faces, {<br>    this.isReflection = false,<br>  });<br><br>  @override<br>  void paint(Canvas canvas, Size size) {<br>    final paint = Paint()<br>      ..style = PaintingStyle.stroke<br>      ..strokeWidth = 1.0<br>      ..color = Colors.<em>blue</em>;<br><br>    for (final face in faces) {<br>      final faceRect = _reflectionRect(face.boundingBox);<br>      canvas.drawRect(<br>        _scaleRect(<br>          rect: faceRect,<br>          imageSize: imageSize,<br>          widgetSize: size,<br>        ),<br>        paint,<br>      );<br>    }<br>  }<br><br>  @override<br>  bool shouldRepaint(covariant FaceDetectorPainter oldDelegate) {<br>    return oldDelegate.imageSize != imageSize || oldDelegate.faces != faces;<br>  }<br><br>  Rect _reflectionRect(Rect boundingBox) {<br>    if (isReflection) {<br>      return boundingBox;<br>    }<br>    final centerX = imageSize.width / 2;<br>    final left = ((boundingBox.left - centerX) * -1) + centerX;<br>    final right = ((boundingBox.right - centerX) * -1) + centerX;<br>    return Rect.fromLTRB(left, boundingBox.top, right, boundingBox.bottom);<br>  }<br><br>  Rect _scaleRect({<br>    required Rect rect,<br>    required Size imageSize,<br>    required Size widgetSize,<br>  }) {<br>    final scaleX = widgetSize.width / imageSize.width;<br>    final scaleY = widgetSize.height / imageSize.height;<br><br>    final scaledRect = Rect.fromLTRB(<br>      rect.left.toDouble() * scaleX,<br>      rect.top.toDouble() * scaleY,<br>      rect.right.toDouble() * scaleX,<br>      rect.bottom.toDouble() * scaleY,<br>    );<br><br>    return scaledRect;<br>  }<br>}</pre><p>Lalu, kita perlu ubah sedikit kode kita yang didalam <strong>main.dart</strong>. Pertama, kita perlu tambahkan variable-variable berikut didalam class _MyHomePageState .</p><pre>class _MyHomePageState extends State&lt;MyHomePage&gt; {<br>  var pathPhoto = &#39;&#39;;<br>  var isLoading = false;<br>  <strong>var widthImage = 0.0;<br>  var heightImage = 0.0;<br>  var faces = &lt;Face&gt;[];</strong><br>...</pre><p>Kemudian, kita juga perlu ubah kode bagian berikut.</p><pre><em>/// Tampilkan foto yang kita ambil dari kamera atau galeri<br></em>return Center(<br>  child: Image.file(<br>    File(<br>      pathPhoto,<br>    ),<br>  ),<br>);</pre><p>Menjadi seperti berikut.</p><pre><em>/// Tampilkan foto yang kita ambil dari kamera atau galeri<br></em>return Center(<br>  child: CustomPaint(<br>    foregroundPainter: FaceDetectorPainter(<br>      Size(widthImage, heightImage),<br>      faces,<br>      isReflection: true,<br>    ),<br>    child: Image.file(<br>      File(<br>        pathPhoto,<br>      ),<br>    ),<br>  ),<br>);</pre><p>Langkah berikutnya, kita perlu tambahkan sedikit logic agar kita bisa menggambarkan bingkai didalam gambarnya. Untuk membuatnya kita perlu ubah kode bagian berikut.</p><pre><em>/// Pastikan bahwa gambarnya valid<br></em>if (image != null) {<br>  <em>/// Tampilkan loading<br>  </em>setState(() =&gt; isLoading = true);<br><br>  <em>/// Ambil path image-nya<br>  </em>pathPhoto = image.path;<br><br>  <em>/// Buat objek GoogleVisionImage dengan datanya adalah gambar yang kita ambil dari galeri<br>  </em>final googleVisionImage = GoogleVisionImage.fromFilePath(pathPhoto);<br><br>  <em>/// Buat objek FaceDetector<br>  </em>final faceDetector = GoogleVision.<em>instance</em>.faceDetector();<br><br>  <em>/// Jalankan proses untuk deteksi wajahnya<br>  </em>final faces = await faceDetector.processImage(googleVisionImage);<br><br>  <em>/// Tampilkan pesan apakah wajahnya terdeteksi atau tidak<br>  </em>if (faces.isEmpty) {<br>    showDialogMessage(&#39;Wajah tidak terdeteksi&#39;);<br>  } else {<br>    showDialogMessage(&#39;Wajah terdeteksi&#39;);<br>  }<br><br>  <em>/// Sembunyikan loading<br>  </em>setState(() =&gt; isLoading = false);<br>}</pre><p>Menjadi seperti berikut.</p><pre><em>/// Pastikan bahwa gambarnya valid<br></em>if (imagePicker != null) {<br>  <em>/// Tampilkan loading<br>  </em>setState(() =&gt; isLoading = true);<br><br>  <em>/// Ambil path image-nya<br>  </em>pathPhoto = imagePicker.path;<br><br>  <strong><em>/// Ambil nilai width dan height dari gambar<br>  </em>final imageBytes = await File(pathPhoto).readAsBytes();<br>  final image = await decodeImageFromList(imageBytes);<br>  widthImage = image.width.toDouble();<br>  heightImage = image.height.toDouble();</strong><br><br>  <em>/// Buat objek GoogleVisionImage dengan datanya adalah gambar yang kita ambil dari galeri<br>  </em>final googleVisionImage = GoogleVisionImage.fromFilePath(pathPhoto);<br><br>  <em>/// Buat objek FaceDetector<br>  </em>final faceDetector = GoogleVision.<em>instance</em>.faceDetector();<br><br>  <em>/// Jalankan proses untuk deteksi wajahnya<br>  </em><strong>faces.clear();</strong><br>  <strong>faces = await faceDetector.processImage(googleVisionImage);</strong><br><br>  <em>/// Tampilkan pesan apakah wajahnya terdeteksi atau tidak<br>  </em>if (faces.isEmpty) {<br>    showDialogMessage(&#39;Wajah tidak terdeteksi&#39;);<br>  } else {<br>    showDialogMessage(&#39;Wajah terdeteksi&#39;);<br>  }<br><br>  <em>/// Sembunyikan loading<br>  </em>setState(() =&gt; isLoading = false);<br>}</pre><p>Sekarang coba kita jalankan lagi app-nya. Seharusnya sudah ada bingkainya untuk menunjukkan dimana letak wajahnya.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FNlIEPIhZZgU%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DNlIEPIhZZgU&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FNlIEPIhZZgU%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="640" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/1904460bb9e6f1ebe6281a383250e15b/href">https://medium.com/media/1904460bb9e6f1ebe6281a383250e15b/href</a></iframe><h4>Kesimpulan</h4><p>Jadi gimana? Gampang kan cara buatnya. Jadi, kesimpulannya adalah pada tulisan ini kita sudah berhasil membuat fitur face detection di Android dan iOS menggunakan plugin Google ML Vision. Selain itu, kita juga telah belajar gimana cara menambahkan bingkai di setiap wajah yang ada didalam foto. Seperti biasa untuk source code lengkapnya bisa dilihat di Github ya.</p><p><a href="https://github.com/CoderJava/flutter-face-detection-demo">GitHub - CoderJava/flutter-face-detection-demo: How to create face detection with Google ML Kit in Flutter</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9cf966fd3925" width="1" height="1" alt=""><hr><p><a href="https://medium.com/nusanet/flutter-face-detection-with-ml-kit-9cf966fd3925">Flutter: Face Detection with ML Kit</a> was originally published in <a href="https://medium.com/nusanet">Nusanet Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[New Post In January 2022]]></title>
            <link>https://medium.com/nusanet/new-post-in-january-2022-b9a2b045874f?source=rss-b636a005b4e2------2</link>
            <guid isPermaLink="false">https://medium.com/p/b9a2b045874f</guid>
            <category><![CDATA[nusanet]]></category>
            <category><![CDATA[newsletter]]></category>
            <category><![CDATA[nusanet-developer]]></category>
            <category><![CDATA[nusanet-silaturahmi]]></category>
            <dc:creator><![CDATA[Yudi Setiawan]]></dc:creator>
            <pubDate>Mon, 31 Jan 2022 10:03:36 GMT</pubDate>
            <atom:updated>2022-01-31T10:03:36.617Z</atom:updated>
            <content:encoded><![CDATA[<p>Setelah sekian lama akhirnya medium Nusanet kembali aktif setelah lama tidak merilis tulisan di Medium. Memang di tahun 2021 kami ada merilis beberapa tulisan tapi, kami kurang konsisten sehingga tulisannya itu jadi kurang terjadwalkan gitu. Selain itu, di tahun 2021 kami juga cukup lumayan sibuk dengan pekerjaan kami sehingga waktu luang untuk menulis itu tidak ada sama sekali. Mudah-mudahan saja di tahun 2022 ini kami tetap bisa konsisten rilis minimal 1 tulisan setiap bulannya. Lalu, berhubung ini masih di awal tahun 2022 kami ada merilis beberapa tulisan berikut.</p><h4><a href="https://medium.com/nusanet/flutter-infinite-listview-e82ff55efe84">Flutter: Infinite ListView</a></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eq_j2WLKhUwLEFl3sL72Yw.png" /></figure><p>Infinite ListView merupakan salah satu teknik di app mobile untuk membuat pagination. Jadi, cara kerjanya adalah ketika si user berada di posisi terakhir dari sebuah List maka, app akan menampilkan loading untuk memuat data di halaman berikutnya. Nah, ditulisan ini kita akan belajar bagaimana cara membuatnya di Flutter tanpa menggunakan plugin tambahan.</p><h4><a href="https://medium.com/nusanet/flutter-valuelistenablebuilder-50a8a802300d">Flutter: ValueListenableBuilder</a></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LvWYDK18GgIGhLp4Bk_3jw.png" /></figure><p>Kita semua tahu bahwa Flutter memiliki banyak banget widget yang tersedia. Nah, salah satu widget yang akan kita bahas di tulisan ini ialah ValueListenableBuilder. Jadi, widget ini berfungsi untuk mengubah state dari widget tertentu yang kita inginkan. Dan perubahannya itu hanya terjadi jika value yang di-listen dari widget ValueListenableBuilder berubah. Penasaran gimana cara buatnya? Langsung saja cek ke TKP ya.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b9a2b045874f" width="1" height="1" alt=""><hr><p><a href="https://medium.com/nusanet/new-post-in-january-2022-b9a2b045874f">New Post In January 2022</a> was originally published in <a href="https://medium.com/nusanet">Nusanet Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Flutter: ValueListenableBuilder]]></title>
            <link>https://medium.com/nusanet/flutter-valuelistenablebuilder-50a8a802300d?source=rss-b636a005b4e2------2</link>
            <guid isPermaLink="false">https://medium.com/p/50a8a802300d</guid>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[state-management]]></category>
            <category><![CDATA[flutter-app-development]]></category>
            <category><![CDATA[flutter-widget]]></category>
            <category><![CDATA[flutter-ui]]></category>
            <dc:creator><![CDATA[Yudi Setiawan]]></dc:creator>
            <pubDate>Sun, 30 Jan 2022 10:03:55 GMT</pubDate>
            <atom:updated>2022-02-22T03:25:05.925Z</atom:updated>
            <content:encoded><![CDATA[<h4>Cara menggunakan widget ValueListenableBuilder</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LvWYDK18GgIGhLp4Bk_3jw.png" /></figure><h3>Pengenalan</h3><p>Di Flutter untuk mengubah state sebuah halaman kita bisa menggunakan setState. Selain setState, kita juga bisa menggunakan state management agar kode kita lebih rapi dan gampang dibaca. Walaupun penggunaan setState bisa dikatakan cukup mudah namun, jika kita salah dalam menggunakannya bisa-bisa performance app kita jadi jelek loh. Misalnya nih, kita mau mengubah widget Time yang ada di halaman Home. Let say widget Time ini nantinya kita mau buat realtime dimana setiap detik gitu jamnya berubah dengan format HH:mm:ss. Niat kita hanya ingin mengubah widget Time saja tapi, karena kita tidak tahu cara menggunakan setState yang baik. Alhasil kita langsung panggil saja tuh setState di halaman Home yang mana dampaknya adalah semua widget yang ada di halaman Home akan ter-build ulang. Terus pertanyaannya gimana caranya agar kita hanya bisa build ulang widget Time-nya saja? Sebenarnya ada banyak tekniknya namun, di tulisan kali ini saya akan contohkan caranya menggunakan widget ValueListenableBuilder.</p><h3>Design</h3><p>Biar pembahasan kali ini lebih menarik saya mau sekalian cari referensi design UI yang mau kita jadikan bahan di tulisan ini. Jadi, untuk referensi design-nya saya ada ambil dari dribbble ya.</p><p><a href="https://dribbble.com/shots/16649409-Aplikasi-Pengawasan-Konstruksi-Attendance-Apps/attachments/11683451?mode=media">Dribbble - #2 Aplikasi Pengawasan Kontruksi.png by Muhammad Ilham Julianto</a></p><p>Untuk design-nya nanti kita cuma akan pakai yang bagian ini aja ya.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/366/1*NYgzxUBDRyCV19v3U1DcAQ.png" /><figcaption>Referensi design</figcaption></figure><h3>Pembuatan Projek</h3><h4>Buat Projek</h4><p>Untuk langkah pertamanya, silakan kita buat projek Flutter baru dengan nama <strong>flutter_mobile_attendance</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/830/1*R1nBmAnjO6E79637BZfh-Q.png" /><figcaption>Buat Projek</figcaption></figure><h4>Persiapan Aset</h4><p>Sebelum kita mulai membuatnya. Kita memerlukan aset foto profil. Untuk foto profilnya bebas mau pakai yang mana ya. Tapi, kali ini saya akan pakai salah satu foto dari website <a href="https://www.pexels.com/photo/woman-wearing-black-spaghetti-strap-top-415829/">pexels.com</a>. Setelah kita unduh fotonya kemudian, kita masukkan foto tersebut kedalam projek kedalam directory <strong>assets</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/395/1*Hm85NvR4yd3Ia5YIwklZdw.png" /><figcaption>Persiapan Aset</figcaption></figure><p>Selanjutnya, kita daftarkan directory <strong>assets</strong> kedalam file <strong>pubspec.yaml</strong>. Dan kita juga ada pakai plugin <a href="https://pub.dev/packages/intl">intl</a>.</p><pre>name: flutter_mobile_attendance<br>description: How to use ValueListenableBuilder<br>publish_to: &#39;none&#39; <em># Remove this line if you wish to publish to pub.dev<br></em>version: 1.0.0+1<br><br>environment:<br>  sdk: &quot;&gt;=2.15.1 &lt;3.0.0&quot;<br><br>dependencies:<br>  flutter:<br>    sdk: flutter<br><br>  cupertino_icons: ^1.0.2<br><strong>  intl: ^0.17.0</strong><br><br>dev_dependencies:<br>  flutter_test:<br>    sdk: flutter<br><br>  flutter_lints: ^1.0.0<br><br>flutter:<br><br>  <em># The following line ensures that the Material Icons font is<br>  # included with your application, so that you can use the icons in<br>  # the material Icons class.<br>  </em>uses-material-design: true<br><br><strong>  assets:<br>    - assets/</strong></pre><p>Selanjutnya, kita jalankan perintah flutter pub get.</p><h4>Buat Kerangka UI</h4><p>Sekarang kita buka file <strong>main.dart</strong> dan ubah kode didalamnya menjadi seperti berikut.</p><pre>import &#39;dart:async&#39;;<br><br>import &#39;package:flutter/material.dart&#39;;<br><br>void main() {<br>  runApp(const MyApp());<br>}<br><br>class MyApp extends StatelessWidget {<br>  const MyApp({Key? key}) : super(key: key);<br><br>  @override<br>  Widget build(BuildContext context) {<br>    return MaterialApp(<br>      title: &#39;Flutter Demo&#39;,<br>      theme: ThemeData(<br>        primarySwatch: Colors.<em>orange</em>,<br>      ),<br>      home: const MyHomePage(title: &#39;Flutter: ValueListenableBuilder&#39;),<br>    );<br>  }<br>}<br><br>class MyHomePage extends StatefulWidget {<br>  final String title;<br><br>  const MyHomePage({<br>    Key? key,<br>    required this.title,<br>  }) : super(key: key);<br><br>  @override<br>  State&lt;MyHomePage&gt; createState() =&gt; _MyHomePageState();<br>}<br><br>class _MyHomePageState extends State&lt;MyHomePage&gt; {<br>  var paddingTop = 0.0;<br>  var paddingBottom = 0.0;<br>  var widthScreen = 0.0;<br><br>  var now = DateTime.now();<br>  Timer? timer;<br><br>  @override<br>  Widget build(BuildContext context) {<br>    final mediaQueryData = MediaQuery.<em>of</em>(context);<br>    widthScreen = mediaQueryData.size.width;<br>    paddingTop = mediaQueryData.padding.top;<br>    paddingBottom = mediaQueryData.padding.bottom;<br><br>    return Scaffold(<br>      body: Container(<br>        width: double.<em>infinity</em>,<br>        color: Colors.<em>white</em>,<br>        padding: EdgeInsets.fromLTRB(<br>          16,<br>          paddingTop + 16,<br>          16,<br>          paddingBottom &gt; 0 ? paddingBottom : 16,<br>        ),<br>        child: Column(<br>          children: [<br>            buildWidgetHeader(),<br>            const SizedBox(height: 24),<br>            buildWidgetDateTime(),<br>            buildWidgetButtonPresence(),<br>            buildWidgetHistoryPresence(),<br>            const SizedBox(height: 24),<br>            buildWidgetMenu(),<br>          ],<br>        ),<br>      ),<br>    );<br>  }<br><br>  Widget buildWidgetHeader() {<br>    // <em>TODO: buat widget yang menampilkan info nama dan foto profil si user<br>    </em>return Container();<br>  }<br><br>  Widget buildWidgetDateTime() {<br>    // <em>TODO: buat widget yang menampilkan tanggal dan jam sekarang<br>    </em>return Container();<br>  }<br><br>  Widget buildWidgetButtonPresence() {<br>    // <em>TODO: buat widget yang menampilkan button presence<br>    </em>return Container();<br>  }<br><br>  Widget buildWidgetHistoryPresence() {<br>    // <em>TODO: buat widget yang menampilkan button ijin tidak hadir dan history presensi<br>    </em>return Container();<br>  }<br><br>  Widget buildWidgetMenu() {<br>    // <em>TODO: buat widget yang menampilkan menu-nya<br>    </em>return Container();<br>  }<br>}</pre><p>Nah, jadi kode diatas adalah kerangka dari UI yang akan kita buat.</p><h4>Buat Widget Header</h4><p>Sekarang kita akan coba membuat widget bagian header-nya dimana, dibagian tersebut kita akan menampilkan nama dan foto profil si pengguna.</p><p>Silakan kita ubah kode berikut.</p><pre>Widget buildWidgetHeader() {<br>  // <em>TODO: buat widget yang menampilkan info nama dan foto profil si user<br>  </em>return Container();<br>}</pre><p>Menjadi seperti berikut.</p><pre>Widget buildWidgetHeader() {<br>  return Row(<br>    children: [<br>      Expanded(<br>        child: Column(<br>          crossAxisAlignment: CrossAxisAlignment.start,<br>          children: [<br>            Text(<br>              &#39;Selamat Datang&#39;,<br>              style: Theme.<em>of</em>(context).textTheme.headline6?.copyWith(<br>                    fontWeight: FontWeight.<em>normal</em>,<br>                  ),<br>            ),<br>            Text(<br>              &#39;Putri&#39;,<br>              style: Theme.<em>of</em>(context).textTheme.headline6,<br>            ),<br>          ],<br>        ),<br>      ),<br>      ClipOval(<br>        child: Image.asset(<br>          &#39;assets/photo_profile.jpeg&#39;,<br>          width: 42,<br>          height: 42,<br>          fit: BoxFit.cover,<br>        ),<br>      ),<br>    ],<br>  );<br>}</pre><p>Jika dijalankan maka, outputnya akan menjadi seperti berikut.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/564/1*1iLW07FZxaIzLNDEb8Zg-Q.png" /><figcaption>Buat Widget Header</figcaption></figure><h4>Buat Widget Date Time</h4><p>Langkah selanjutnya adalah kita akan membuat widget yang menampilkan jam dan tanggal saat ini. Untuk membuatnya, silakan ubah kode berikut.</p><pre>Widget buildWidgetDateTime() {<br>  // <em>TODO: buat widget yang menampilkan tanggal dan jam sekarang<br>  </em>return Container();<br>}</pre><p>Menjadi seperti berikut.</p><pre>Widget buildWidgetDateTime() {<br>  final formattedTime = DateFormat(&#39;HH:mm&#39;, &#39;id&#39;).format(now);<br>  final formattedTime2 = DateFormat(&#39;:ss&#39;, &#39;id&#39;).format(now);<br>  final formattedDate = DateFormat(&#39;EEEE, dd MMM yyy&#39;, &#39;id&#39;).format(now);<br>  return Column(<br>    mainAxisSize: MainAxisSize.min,<br>    children: [<br>      Row(<br>        mainAxisAlignment: MainAxisAlignment.center,<br>        crossAxisAlignment: CrossAxisAlignment.baseline,<br>        textBaseline: TextBaseline.ideographic,<br>        children: [<br>          Text(<br>            formattedTime,<br>            style: Theme.<em>of</em>(context).textTheme.headline4?.copyWith(<br>                  fontWeight: FontWeight.<em>w500</em>,<br>                  color: Colors.<em>grey</em>[800],<br>                ),<br>          ),<br>          Text(<br>            formattedTime2,<br>            style: Theme.<em>of</em>(context).textTheme.bodyText2?.copyWith(<br>                  fontWeight: FontWeight.<em>w500</em>,<br>                  color: Colors.<em>grey</em>[800],<br>                ),<br>          ),<br>        ],<br>      ),<br>      Text(<br>        formattedDate,<br>        style: Theme.<em>of</em>(context).textTheme.headline6?.copyWith(<br>              color: Colors.<em>grey</em>,<br>            ),<br>      ),<br>    ],<br>  );<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/564/1*V205ajVsX2bou4UXG1c_pw.png" /><figcaption>Buat Widget Date Time</figcaption></figure><p>Jadi, untuk sementara widget date time-nya belum kita buat realtime. Kita fokus buat UI-nya dulu ya.</p><h4>Buat Widget Button Presence</h4><p>Nah, di langkah berikutnya kita akan membuat widget button presence. Silakan kita ubah kode berikut.</p><pre>Widget buildWidgetButtonPresence() {<br>  // <em>TODO: buat widget yang menampilkan button presence<br>  </em>return Container();<br>}</pre><p>Menjadi seperti berikut.</p><pre>Widget buildWidgetButtonPresence() {<br>  return Expanded(<br>    child: Container(<br>      width: widthScreen / 1.5,<br>      height: widthScreen / 1.5,<br>      decoration: BoxDecoration(<br>        shape: BoxShape.circle,<br>        gradient: LinearGradient(<br>          begin: Alignment.<em>bottomLeft</em>,<br>          end: Alignment.<em>topRight</em>,<br>          colors: [<br>            Colors.<em>orange</em>[900]!,<br>            Colors.<em>orange</em>,<br>            Colors.<em>orange</em>[200]!,<br>          ],<br>        ),<br>        boxShadow: [<br>          BoxShadow(<br>            color: Colors.<em>orange</em>.withOpacity(0.3),<br>            offset: const Offset(0, 8),<br>            blurRadius: 8,<br>            spreadRadius: 8,<br>          ),<br>        ],<br>      ),<br>      alignment: Alignment.<em>center</em>,<br>      child: Column(<br>        mainAxisSize: MainAxisSize.min,<br>        children: [<br>          Icon(<br>            Icons.<em>fingerprint</em>,<br>            color: Colors.<em>white</em>,<br>            size: widthScreen / 3,<br>          ),<br>          Text(<br>            &#39;Absen Masuk&#39;,<br>            style: Theme.<em>of</em>(context).textTheme.subtitle2?.copyWith(<br>                  color: Colors.<em>white</em>,<br>                ),<br>          ),<br>        ],<br>      ),<br>    ),<br>  );<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/564/1*OjWSvFVzxPcbYEFy1PPFnw.png" /><figcaption>Buat Widget Button Presence</figcaption></figure><h4>Buat Widget History Presence</h4><p>Langkah selanjutnya kita akan membuat widget history presence. Silakan ubah kode berikut.</p><pre>Widget buildWidgetHistoryPresence() {<br>  // <em>TODO: buat widget yang menampilkan button ijin tidak hadir dan history presensi<br>  </em>return Container();<br>}</pre><p>Menjadi seperti berikut.</p><pre>Widget buildWidgetHistoryPresence() {<br>  return Row(<br>    mainAxisAlignment: MainAxisAlignment.spaceAround,<br>    children: [<br>      Column(<br>        children: [<br>          Container(<br>            decoration: BoxDecoration(<br>              shape: BoxShape.circle,<br>              border: Border.all(color: Colors.<em>grey</em>),<br>            ),<br>            alignment: Alignment.<em>center</em>,<br>            padding: const EdgeInsets.all(12),<br>            child: const Icon(<br>              Icons.<em>remove_circle</em>,<br>              color: Colors.<em>red</em>,<br>            ),<br>          ),<br>          const SizedBox(height: 8),<br>          const Text(&#39;Ijin Tidak Hadir&#39;),<br>        ],<br>      ),<br>      Column(<br>        children: [<br>          Container(<br>            decoration: BoxDecoration(<br>              shape: BoxShape.circle,<br>              border: Border.all(color: Colors.<em>grey</em>),<br>            ),<br>            alignment: Alignment.<em>center</em>,<br>            padding: const EdgeInsets.all(12),<br>            child: const Icon(<br>              Icons.<em>article_rounded</em>,<br>              color: Colors.<em>orange</em>,<br>            ),<br>          ),<br>          const SizedBox(height: 8),<br>          const Text(&quot;Lihat History&quot;),<br>        ],<br>      ),<br>    ],<br>  );<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/564/1*Pi-X2oQb5CPOruAEcndBVg.png" /><figcaption>Buat Widget History Presence</figcaption></figure><h4>Buat Widget Menu</h4><p>Nah, ini widget terakhir yang akan kita buat. Untuk membuatnya, silakan ubah kode berikut.</p><pre>Widget buildWidgetMenu() {<br>  // <em>TODO: buat widget yang menampilkan menu-nya<br>  </em>return Container();<br>}</pre><p>Menjadi seperti berikut.</p><pre>Widget buildWidgetMenu() {<br>  return Container(<br>    width: double.<em>infinity</em>,<br>    decoration: BoxDecoration(<br>      color: Colors.<em>grey</em>[900],<br>      borderRadius: BorderRadius.circular(8),<br>    ),<br>    padding: const EdgeInsets.symmetric(<br>      horizontal: 16,<br>      vertical: 8,<br>    ),<br>    child: Row(<br>      mainAxisAlignment: MainAxisAlignment.spaceAround,<br>      children: [<br>        Column(<br>          children: [<br>            const Icon(<br>              Icons.<em>fingerprint</em>,<br>              color: Colors.<em>orange</em>,<br>            ),<br>            const SizedBox(height: 4),<br>            Text(<br>              &#39;Absensi&#39;,<br>              style: Theme.<em>of</em>(context).textTheme.subtitle2?.copyWith(<br>                    fontSize: 12,<br>                    color: Colors.<em>orange</em>,<br>                  ),<br>            ),<br>          ],<br>        ),<br>        Column(<br>          children: [<br>            const Icon(<br>              Icons.<em>local_activity</em>,<br>              color: Colors.<em>grey</em>,<br>            ),<br>            const SizedBox(height: 4),<br>            Text(<br>              &#39;Kegiatan&#39;,<br>              style: Theme.<em>of</em>(context).textTheme.subtitle2?.copyWith(<br>                    fontSize: 12,<br>                    color: Colors.<em>grey</em>,<br>                  ),<br>            ),<br>          ],<br>        ),<br>        Column(<br>          children: [<br>            const Icon(<br>              Icons.<em>help</em>,<br>              color: Colors.<em>grey</em>,<br>            ),<br>            const SizedBox(height: 4),<br>            Text(<br>              &#39;Problem&#39;,<br>              style: Theme.<em>of</em>(context).textTheme.subtitle2?.copyWith(<br>                    fontSize: 12,<br>                    color: Colors.<em>grey</em>,<br>                  ),<br>            ),<br>          ],<br>        ),<br>        Column(<br>          children: [<br>            const Icon(<br>              Icons.<em>location_on</em>,<br>              color: Colors.<em>grey</em>,<br>            ),<br>            const SizedBox(height: 4),<br>            Text(<br>              &#39;Lokasi&#39;,<br>              style: Theme.<em>of</em>(context).textTheme.subtitle2?.copyWith(<br>                    fontSize: 12,<br>                    color: Colors.<em>grey</em>,<br>                  ),<br>            ),<br>          ],<br>        ),<br>      ],<br>    ),<br>  );<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/564/1*klxLuyIiGYw-uzvSeUAUVQ.png" /><figcaption>Buat Widget Menu</figcaption></figure><p>Bisa kita lihat sekarang bahwa UI-nya sudah selesai kita buat. Namun, kalau diperhatikan di sini masih ada yang kurang ya. Yaitu, kita mau widget date time-nya itu jalan atau dibuat realtime.</p><h3>Buat Widget Date Time menjadi Realtime</h3><p>Oke, sekarang akan kita buat ya. Tapi, sebelum kita buat. Saya mau tunjukkan dulu bahwa untuk membuatnya saya akan tunjukkan dengan 2 cara yaitu, cara pertama dengan menggunakan setState dan cara kedua menggunakan widget ValueListenableBuilder. Sengaja saya tunjukkan biar kita semua tahu mana yang lebih praktis dan efisien dalam penggunaannya untuk kasus seperti ini.</p><h4>Menggunakan setState</h4><p>Nah, untuk cara pertama kita akan coba menggunakan setState. Caranya, silakan kita update kode didalam file <strong>main.dart </strong>dan kita perlu override function initState dan dispose menjadi seperti berikut.</p><pre>@override<br>void initState() {<br>  timer = Timer.periodic(const Duration(seconds: 1), (_) {<br>    now = now.add(const Duration(seconds: 1));<br>    setState(() {});<br>  });<br>  super.initState();<br>}<br><br>@override<br>void dispose() {<br>  timer?.cancel();<br>  super.dispose();<br>}</pre><p>Kemudian, untuk membuktikan bahwa method build benar-benar ter-build ulang ketika timer-nya jalan kita perlu tambahkan kode berikut didalam method build .</p><pre>@override<br>Widget build(BuildContext context) {<br>  <strong>print(&#39;Ini method build&#39;);</strong><br>  final mediaQueryData = MediaQuery.<em>of</em>(context);<br>  widthScreen = mediaQueryData.size.width;<br>  paddingTop = mediaQueryData.padding.top;<br>  paddingBottom = mediaQueryData.padding.bottom;<br><br>  return Scaffold(<br>...</pre><p>Sekarang kalau kita jalankan programnya maka, kita sudah bisa lihat bahwa widget date time-nya sudah berjalan secara realtime.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgfycat.com%2Fifr%2Fsnarlingvastatlanticridleyturtle&amp;display_name=Gfycat&amp;url=https%3A%2F%2Fgfycat.com%2Fsnarlingvastatlanticridleyturtle&amp;image=https%3A%2F%2Fthumbs.gfycat.com%2FSnarlingVastAtlanticridleyturtle-size_restricted.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=gfycat" width="1548" height="1004" frameborder="0" scrolling="no"><a href="https://medium.com/media/ba8362a4ef97bdd31b3d0aa2dcc28842/href">https://medium.com/media/ba8362a4ef97bdd31b3d0aa2dcc28842/href</a></iframe><p>Tapi, kalau kita perhatikan kodenya. Cara pertama kuranglah efisien. Mengapa berani saya katakan kurang efisien? Karena, ketika kita menggunakan setState maka, tanpa kita sadari bahwa semua widget yang ada didalam method build akan terbuat ulang semuanya. Dan ini kurang efektif mengingat kembali bahwa tujuan diawalnya kita hanya mau meng-update state dari widget Date Time saja. Widget yang lain tidak perlu diupdate. Memang di contoh kasus kali ini kita tidak melihat secara langsung efeknya tapi, kalau UI-nya sudah cukup kompleks maka, itu akan sangat kelihatan sekali bahwa jank atau freeze-nya terjadi. Jadi, untuk cara pertama ini kurang efektif ya.</p><h4>Menggunakan Widget ValueListenableBuilder</h4><p>Oke sekarang kita coba cara kedua ya. Untuk cara kedua kita akan menggunakan widget yang bernama ValueListenableBuilder . Jadi, kalau kita baca-baca secara singkat dokumentasinya. Disebutkan bahwa widget ini berfungsi untuk build ulang widget yang ada didalamnya ketika nilai yang kita berikan berubah. Agak susah dimengerti ya? Oke, jadi bahasa gampangnya adalah widget ini hanya ter-build ulang kalau nilai didalamnya juga berubah.</p><p>Biar lebih gampang. Akan saya tunjukkan gimana cara buatnya. Pertama-tama kita hapus dulu tuh perintah setState yang ada didalam method initState. Sekaligus hapus variable yang bernama now.</p><pre>Timer? timer;<br><br>@override<br>void initState() {<br>  timer = Timer.periodic(const Duration(seconds: 1), (_) {<br>    // <em>TODO: do something in here  <br>  </em>});<br>  super.initState();<br>}</pre><p>Kemudian, kita perlu buat 1 variable global dengan nama valueNotifierNow. Dan kita inisialisasikan nilainya didalam method initState.</p><pre>Timer? timer;<br><strong>late ValueNotifier&lt;DateTime&gt; valueNotifierNow;</strong><br><br>@override<br>void initState() {<br>  <strong>valueNotifierNow = ValueNotifier&lt;DateTime&gt;(DateTime.now());</strong><br>  timer = Timer.periodic(const Duration(seconds: 1), (_) {<br>    <strong>valueNotifierNow.value = DateTime.now();</strong><br>  });<br>  super.initState();<br>}</pre><p>Untuk saat ini kita akan melihat bahwa kode kita mengalami error tapi, it’s okay. Kita abaikan dulu ya. Sekarang kita ubah kode bagian ini.</p><pre>child: Column(<br>  children: [<br>    buildWidgetHeader(),<br>    const SizedBox(height: 24),<br>    buildWidgetDateTime(),<br>    buildWidgetButtonPresence(),<br>    buildWidgetHistoryPresence(),<br>    const SizedBox(height: 24),<br>    buildWidgetMenu(),<br>  ],<br>),</pre><p>Menjadi seperti ini.</p><pre>child: Column(<br>  children: [<br>    buildWidgetHeader(),<br>    const SizedBox(height: 24),<br>    <strong>ValueListenableBuilder&lt;DateTime&gt;(<br>      valueListenable: valueNotifierNow,<br>      builder: (BuildContext context, DateTime now, Widget? child) {<br>        return buildWidgetDateTime(now);<br>      },<br>    ),</strong><br>    buildWidgetButtonPresence(),<br>    buildWidgetHistoryPresence(),<br>    const SizedBox(height: 24),<br>    buildWidgetMenu(),<br>  ],<br>),</pre><p>Lalu, kita tambahkan parameter didalam method buildWidgetDateTime .</p><pre><strong>Widget buildWidgetDateTime(DateTime now)</strong> {<br>  final formattedTime = DateFormat(&#39;HH:mm&#39;, &#39;id&#39;).format(now);<br>  final formattedTime2 = DateFormat(&#39;:ss&#39;, &#39;id&#39;).format(now);<br>  final formattedDate = DateFormat(&#39;EEEE, dd MMM yyy&#39;, &#39;id&#39;).format(now);<br>  return Column(<br>...</pre><p>Sekarang coba kita jalankan lagi app-nya maka, outputnya akan sama yaitu, waktu akan berjalan secara realtime. Mungkin kalau cuma dari UI-nya saja kita tidak percaya ya kalau yang ter-build ulang hanyalah method buildWidgetDateTime. Untuk membuktikannya silakan kita tambahkan kode print berikut didalam method buildWidgetDateTime .</p><pre>...</pre><pre>Widget buildWidgetDateTime(DateTime now) {<br>  <strong>print(&#39;ini method buildWidgetDateTime&#39;);</strong><br>  final formattedTime = DateFormat(&#39;HH:mm&#39;, &#39;id&#39;).format(now);<br>  final formattedTime2 = DateFormat(&#39;:ss&#39;, &#39;id&#39;).format(now);<br>  final formattedDate = DateFormat(&#39;EEEE, dd MMM yyy&#39;, &#39;id&#39;).format(now);<br>  return Column(<br>...</pre><p>Sekarang coba kita jalankan programnya dan buka DevTools untuk melihat output di console-nya apakah yang ter-build hanya method buildWidgetDateTime saja atau method build juga ikut ter-build.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgfycat.com%2Fifr%2Fwellgroomedboringgordonsetter&amp;display_name=Gfycat&amp;url=https%3A%2F%2Fgfycat.com%2Fwellgroomedboringgordonsetter&amp;image=https%3A%2F%2Fthumbs.gfycat.com%2FWellgroomedBoringGordonsetter-size_restricted.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=gfycat" width="1548" height="1004" frameborder="0" scrolling="no"><a href="https://medium.com/media/06d9f766535c4c74a6021c65ca6eeb36/href">https://medium.com/media/06d9f766535c4c74a6021c65ca6eeb36/href</a></iframe><p>Nah, lihat kan perbedaannya? Kalau pakai widget ValueListenableBuilder maka, yang ter-build hanyalah method buildWidgetDateTime-nya saja.</p><h3>Kesimpulan</h3><p>Jadi, di tulisan ini kita telah mempelajari mengenai penggunaan widget ValueListenableBuilder. Jadi, widget ValueListenableBuilder ini berfungsi untuk membuat widget yang kita inginkan hanya ter-build ulang jika nilai yang diberikan berubah. Jadi, lebih efektif daripada setState jikalau kita hanya ingin mem-build ulang widget tertentu. Untuk source code lengkapnya bisa dicek di Github ya 😉.</p><p><a href="https://github.com/CoderJava/flutter-demo-value-listenable-builder">GitHub - CoderJava/flutter-demo-value-listenable-builder: How to use widget ValueListenableBuilder in Flutter.</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=50a8a802300d" width="1" height="1" alt=""><hr><p><a href="https://medium.com/nusanet/flutter-valuelistenablebuilder-50a8a802300d">Flutter: ValueListenableBuilder</a> was originally published in <a href="https://medium.com/nusanet">Nusanet Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Flutter Infinite ListView]]></title>
            <link>https://medium.com/nusanet/flutter-infinite-listview-e82ff55efe84?source=rss-b636a005b4e2------2</link>
            <guid isPermaLink="false">https://medium.com/p/e82ff55efe84</guid>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[loadmore]]></category>
            <category><![CDATA[infinite-scroll]]></category>
            <category><![CDATA[flutter-app-development]]></category>
            <category><![CDATA[listview]]></category>
            <dc:creator><![CDATA[Yudi Setiawan]]></dc:creator>
            <pubDate>Sat, 22 Jan 2022 10:21:55 GMT</pubDate>
            <atom:updated>2022-01-22T10:21:55.261Z</atom:updated>
            <content:encoded><![CDATA[<h4>How to implement infinite ListView in Flutter</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eq_j2WLKhUwLEFl3sL72Yw.png" /></figure><h3>Pengenalan</h3><p>Sebuah aplikasi pada umumnya pasti terdapat fitur untuk menampilkan sekumpulan data. Data-data tersebut biasanya ditampilkan sesuai dengan platform-nya masing-masing. Kalau di desktop/web umumnya menggunakan table pagination.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*N4mWlkwEAzXWTa2pgEz__A.jpeg" /><figcaption><a href="https://mdbootstrap.com/docs/b4/jquery/tables/pagination/">https://mdbootstrap.com/docs/b4/jquery/tables/pagination/</a></figcaption></figure><p>Lalu, untuk platform mobile biasanya kita menampilkan datanya dalam bentuk list.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/384/1*1yAPBn6jNcZt5gN56VfL6g.png" /><figcaption><a href="https://dribbble.com/shots/10181589-Order-List-and-Track-Orders-Ui-Design">https://dribbble.com/shots/10181589-Order-List-and-Track-Orders-Ui-Design</a></figcaption></figure><p>Untuk mobile, implementasi pagination-nya kita bisa menggunakan teknik Infinite Scrolling atau Infinite ListView. Jadi, konsepnya itu adalah app-nya menampilkan list datanya dalam bentuk ListView lalu, ketika berada di paling bawah maka, app-nya akan memuat lagi ke API atau database untuk menampilkan data berikutnya.</p><h3>Cara Pembuatan</h3><h4>Buat Projek</h4><p>Langkah pertamanya, silakan kita buat projek baru dengan nama <strong>flutter-swapi</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/830/1*622eeIFzwiUQUxajtaPuiQ.png" /><figcaption>Buat projek baru</figcaption></figure><h4>Atur pubspec.yaml</h4><p>Untuk membuatnya ada beberapa plugin yang perlu kita tambahkan kedalam projek kita. Silakan buka file <strong>pubspec.yaml</strong> dan tambahkan beberapa plugin berikut.</p><ol><li>dio</li><li>json_annotation</li><li>json_serializable</li><li>build_runner</li></ol><p><em>Catatan: Untuk versinya silakan pakai yang versi terbaru ya.</em></p><pre>...<br>dependencies:<br>  flutter:<br>    sdk: flutter<br><br>  cupertino_icons: ^1.0.2<br>  dio: ^4.0.4<br>  json_annotation: ^4.4.0<br><br>dev_dependencies:<br>  flutter_test:<br>    sdk: flutter<br><br>  flutter_lints: ^1.0.0<br>  json_serializable: ^6.1.4<br>  build_runner: ^2.1.7<br>...</pre><h4>Buat Class Model</h4><p>Langkah berikutnya, kita perlu membuat class model dari respon endpoint yang bakalan kita pakai. Berikut adalah endpoint yang akan kita pakai.</p><pre>https://swapi.dev/api/people?page=:page</pre><p>Dan berikut adalah format respon dari endpoint tersebut.</p><pre>{<br>  &quot;count&quot;: 82,<br>  &quot;next&quot;: &quot;https://swapi.dev/api/people/?page=2&quot;,<br>  &quot;previous&quot;: null,<br>  &quot;results&quot;: [<br>    {<br>      &quot;name&quot;: &quot;Luke Skywalker&quot;,<br>      &quot;height&quot;: &quot;172&quot;,<br>      &quot;mass&quot;: &quot;77&quot;,<br>      &quot;hair_color&quot;: &quot;blond&quot;,<br>      &quot;skin_color&quot;: &quot;fair&quot;,<br>      &quot;eye_color&quot;: &quot;blue&quot;,<br>      &quot;birth_year&quot;: &quot;19BBY&quot;,<br>      &quot;gender&quot;: &quot;male&quot;,<br>      &quot;homeworld&quot;: &quot;https://swapi.dev/api/planets/1/&quot;,<br>      &quot;films&quot;: [<br>        &quot;https://swapi.dev/api/films/1/&quot;,<br>        &quot;https://swapi.dev/api/films/2/&quot;,<br>        &quot;https://swapi.dev/api/films/3/&quot;,<br>        &quot;https://swapi.dev/api/films/6/&quot;<br>      ],<br>      &quot;species&quot;: [],<br>      &quot;vehicles&quot;: [<br>        &quot;https://swapi.dev/api/vehicles/14/&quot;,<br>        &quot;https://swapi.dev/api/vehicles/30/&quot;<br>      ],<br>      &quot;starships&quot;: [<br>        &quot;https://swapi.dev/api/starships/12/&quot;,<br>        &quot;https://swapi.dev/api/starships/22/&quot;<br>      ],<br>      &quot;created&quot;: &quot;2014-12-09T13:50:51.644000Z&quot;,<br>      &quot;edited&quot;: &quot;2014-12-20T21:17:56.891000Z&quot;,<br>      &quot;url&quot;: &quot;https://swapi.dev/api/people/1/&quot;<br>    },<br>    ...<br>  ]<br>}</pre><p>Nah, karena kita sudah tahu format respon JSON-nya. Maka, sekarang kita buat class modelnya. Caranya silakan buat file baru dengan nama <strong>people_response.dart</strong>. Dan isikan dengan kode berikut.</p><pre>import &#39;package:json_annotation/json_annotation.dart&#39;;<br><br>part &#39;people_response.g.dart&#39;;<br><br>@JsonSerializable()<br>class PeopleResponse {<br>  @JsonKey(name: &#39;count&#39;)<br>  final int count;<br>  @JsonKey(name: &#39;next&#39;)<br>  final String? nextUrl;<br>  @JsonKey(name: &#39;previous&#39;)<br>  final String? previousUrl;<br>  @JsonKey(name: &#39;results&#39;)<br>  final List&lt;ItemPeopleResponse&gt; results;<br><br>  PeopleResponse({<br>    required this.count,<br>    required this.nextUrl,<br>    required this.previousUrl,<br>    required this.results,<br>  });<br><br>  factory PeopleResponse.fromJson(Map&lt;String, dynamic&gt; json) =&gt; _$PeopleResponseFromJson(json);<br><br>  Map&lt;String, dynamic&gt; toJson() =&gt; _$PeopleResponseToJson(this);<br>}<br><br>@JsonSerializable()<br>class ItemPeopleResponse {<br>  @JsonKey(name: &#39;name&#39;)<br>  final String? name;<br>  @JsonKey(name: &#39;birth_year&#39;)<br>  final String? birthYear;<br>  @JsonKey(name: &#39;gender&#39;)<br>  final String? gender;<br><br>  ItemPeopleResponse({<br>    required this.name,<br>    required this.birthYear,<br>    required this.gender,<br>  });<br><br>  factory ItemPeopleResponse.fromJson(Map&lt;String, dynamic&gt; json) =&gt; _$ItemPeopleResponseFromJson(json);<br><br>  Map&lt;String, dynamic&gt; toJson() =&gt; _$ItemPeopleResponseToJson(this);<br>}</pre><p>Untuk menghilangkan tanda error-nya. Kita perlu jalankan build runner-nya untuk membuat file generator-nya. Untuk membuat file generator-nya silakan jalankan perintah berikut.</p><pre>flutter pub run build_runner build</pre><h4>Buat UI</h4><p>Langkah berikutnya kita perlu membuat tampilannya. Silakan buka file <strong>main.dart</strong> dan ubah kode didalamnya menjadi seperti berikut.</p><pre>import &#39;package:dio/dio.dart&#39;;<br>import &#39;package:flutter/material.dart&#39;;<br>import &#39;package:flutter_swapi/people_response.dart&#39;;<br><br>enum ItemType {<br>  data,<br>  loading,<br>}<br><br>void main() {<br>  runApp(const MyApp());<br>}<br><br>class MyApp extends StatelessWidget {<br>  const MyApp({Key? key}) : super(key: key);<br><br>  @override<br>  Widget build(BuildContext context) {<br>    return MaterialApp(<br>      title: &#39;Flutter Swapi&#39;,<br>      theme: ThemeData(<br>        primarySwatch: Colors.<em>blue</em>,<br>      ),<br>      home: const MyHomePage(title: &#39;Flutter Swapi&#39;),<br>    );<br>  }<br>}<br><br>class MyHomePage extends StatefulWidget {<br>  final String title;<br><br>  const MyHomePage({Key? key, required this.title}) : super(key: key);<br><br>  @override<br>  State&lt;MyHomePage&gt; createState() =&gt; _MyHomePageState();<br>}<br><br>class _MyHomePageState extends State&lt;MyHomePage&gt; {<br>  final _scrollController = ScrollController();<br>  final _listData = &lt;Data&gt;[];<br>  final _dio = Dio();<br><br>  String? _nextUrl;<br>  var _isLoading = false;<br>  var _paddingBottom = 0.0;<br>  var _errorMessage = &#39;&#39;;<br><br>  @override<br>  void initState() {<br>    _doLoadData();<br>    // <em>TODO: Buat fitur load more<br>    </em>super.initState();<br>  }<br><br>  @override<br>  void dispose() {<br>    _scrollController.dispose();<br>    super.dispose();<br>  }<br><br>  @override<br>  Widget build(BuildContext context) {<br>    final mediaQueryData = MediaQuery.<em>of</em>(context);<br>    _paddingBottom = mediaQueryData.padding.bottom;<br>    return Scaffold(<br>      appBar: AppBar(<br>        title: Text(widget.title),<br>      ),<br>      body: _buildWidgetBody(),<br>    );<br>  }<br><br>  Widget _buildWidgetBody() {<br>    if (_errorMessage.isNotEmpty) {<br>      return _buildWidgetInfoTryAgain(_errorMessage);<br>    }<br><br>    if (_listData.isEmpty) {<br>      if (_isLoading) {<br>        return const Center(<br>          child: CircularProgressIndicator.adaptive(),<br>        );<br>      } else {<br>        return _buildWidgetInfoTryAgain(&#39;Data not available&#39;);<br>      }<br>    } else {<br>      return ListView.separated(<br>        padding: EdgeInsets.only(<br>          left: 16,<br>          top: 16,<br>          right: 16,<br>          bottom: _paddingBottom &gt; 0 ? _paddingBottom : 16,<br>        ),<br>        itemBuilder: (context, index) {<br>          final itemPeople = _listData[index];<br>          final typeData = itemPeople.itemType;<br>          if (typeData == ItemType.loading) {<br>            return const Center(<br>              child: CircularProgressIndicator.adaptive(),<br>            );<br>          } else if (typeData == ItemType.data) {<br>            return Card(<br>              child: Padding(<br>                padding: const EdgeInsets.all(16.0),<br>                child: Column(<br>                  crossAxisAlignment: CrossAxisAlignment.start,<br>                  children: [<br>                    Text(<br>                      itemPeople.people?.name ?? &#39;-&#39;,<br>                      style: Theme.<em>of</em>(context).textTheme.headline6,<br>                    ),<br>                    Text(<br>                      itemPeople.people?.gender ?? &#39;-&#39;,<br>                      style: Theme.<em>of</em>(context).textTheme.bodyText2,<br>                    ),<br>                    Text(<br>                      itemPeople.people?.birthYear ?? &#39;-&#39;,<br>                      style: Theme.<em>of</em>(context).textTheme.caption,<br>                    ),<br>                  ],<br>                ),<br>              ),<br>            );<br>          } else {<br>            return Container();<br>          }<br>        },<br>        separatorBuilder: (context, index) =&gt; Container(<br>          height: 8,<br>        ),<br>        itemCount: _listData.length,<br>      );<br>    }<br>  }<br><br>  Widget _buildWidgetInfoTryAgain(String text) {<br>    return Center(<br>      child: Column(<br>        mainAxisAlignment: MainAxisAlignment.center,<br>        children: [<br>          Text(text),<br>          ElevatedButton(<br>            child: const Text(&#39;Try Again&#39;),<br>            onPressed: () {<br>              _doLoadData();<br>            },<br>          ),<br>        ],<br>      ),<br>    );<br>  }<br><br>  Future&lt;void&gt; _doLoadData() async {<br>    _errorMessage = &#39;&#39;;<br>    var url = &#39;&#39;;<br>    if (_listData.isEmpty) {<br>      url = &#39;https://swapi.dev/api/people?page=1&#39;;<br>    } else {<br>      url = _nextUrl ?? &#39;&#39;;<br>    }<br>    if (url.isEmpty) {<br>      return;<br>    }<br>    setState(() =&gt; _isLoading = true);<br>    try {<br>      final response = await _dio.get(url);<br>      if (response.statusCode.toString().startsWith(&#39;2&#39;)) {<br>        final peopleResponse = PeopleResponse.fromJson(response.data);<br>        if (_listData.isNotEmpty) {<br>          <em>/// Hapus widget elemen loading<br>          </em>final lastItem = _listData.last;<br>          if (lastItem.itemType == ItemType.loading) {<br>            _listData.removeLast();<br>          }<br>        }<br><br>        <em>/// Masukkan data baru kedalam _listData<br>        </em>_listData.addAll(<br>          peopleResponse.results.map(<br>            (element) {<br>              return Data(<br>                people: element,<br>                itemType: ItemType.data,<br>              );<br>            },<br>          ),<br>        );<br><br>        <em>/// Jika nilai _nextUrl != null maka, tambahkan widget elemen loading<br>        </em>_nextUrl = peopleResponse.nextUrl;<br>        if (_nextUrl != null) {<br>          _listData.add(<br>            Data(<br>              people: null,<br>              itemType: ItemType.loading,<br>            ),<br>          );<br>        }<br>      } else {<br>        <em>/// Munculkan pesan error<br>        </em>_listData.clear();<br>        _errorMessage = &quot;Failed to get data&quot;;<br>        ScaffoldMessenger.<em>of</em>(context).showSnackBar(SnackBar(content: Text(_errorMessage)));<br>      }<br>    } catch (error) {<br>      <em>/// Munculkan pesan error<br>      </em>_errorMessage = &#39;Failed to get data&#39;;<br>      ScaffoldMessenger.<em>of</em>(context).showSnackBar(SnackBar(content: Text(_errorMessage)));<br>    } finally {<br>      setState(() =&gt; _isLoading = false);<br>    }<br>  }<br>}<br><br>class Data {<br>  final ItemPeopleResponse? people;<br>  final ItemType itemType;<br><br>  Data({<br>    required this.people,<br>    required this.itemType,<br>  });<br>}</pre><p>Adapun output dari kode UI diatas adalah sebagai berikut.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/555/1*Vb2WidIoS43W2-cZEXCKYQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/555/1*WHvCGExBPFpTmdRDa6IEuQ.png" /></figure><h4>Buat Fitur Infinite List</h4><p>Di langkah sebelumnya, kita sudah berhasil menampilkan datanya dari endpoint. Tapi, fitur infinite list-nya belum bisa berfungsi. Nah, di langkah ini kita akan membuatnya. Silakan buka kembali file <strong>main.dart</strong> dan ubah kode berikut.</p><pre>// TODO: Buat fitur load more</pre><p>Menjadi seperti berikut.</p><pre>_scrollController.addListener(() {<br>  if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {<br>    _doLoadData();<br>  }<br>});</pre><p>Kemudian, kita set property controller dari widget ListView.separated .</p><pre>return ListView.separated(<br>  <strong>controller: _scrollController,</strong><br>  padding: EdgeInsets.only(<br>    left: 16,</pre><p>Berikut adalah video hasilnya.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FIZUm33OL8D0%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DIZUm33OL8D0&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FIZUm33OL8D0%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/2a1df6bd572ae3181f1ea14b4358af67/href">https://medium.com/media/2a1df6bd572ae3181f1ea14b4358af67/href</a></iframe><h4>Kesimpulan</h4><p>Jadi, di tulisan ini kita telah berhasil membuat Infinite ListView di Flutter. Caranya cukup mudah kan. Daripada kita pakai plugin spesial untuk fitur tersebut mending kita buat sendiri dan lebih fully customize. Untuk source code lengkapnya bisa dilihat di Github berikut ya.</p><p><a href="https://github.com/CoderJava/flutter-swapi-infinite-list">GitHub - CoderJava/flutter-swapi-infinite-list: How to create infinite ListView in Flutter</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e82ff55efe84" width="1" height="1" alt=""><hr><p><a href="https://medium.com/nusanet/flutter-infinite-listview-e82ff55efe84">Flutter Infinite ListView</a> was originally published in <a href="https://medium.com/nusanet">Nusanet Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>