Tutorial Flutter dengan Laravel Rest API #3: Menampilkan Data di Flutter dari Rest API

Halo teman – teman pada seri artikel kali ini kita akan melanjutkan tutorialnya, setelah kalian sudah install depedensi dan konfigurasi untuk komunikasi rest API, langkah selanjutnya yaitu kita akan menampilkan data dari Rest API.

Langkah 1 – Buat Layout Post

Sekarang teman – teman buat file dengan nama post_view.dart pada folder component , lalu kalian buat class dengan nama PostView dengan extends StatelessWidget, kalian bisa ketik aja stl nanti akan muncul sugest untuk pembuat class seperti gambar berikut:

lalu lengkapi kode berikut:

import 'package:flutter/material.dart';
import 'package:flutter_pemula/model/post.dart';

class PostView extends StatelessWidget {
  final Post post;

  const PostView({
    super.key,
    required this.post,
  });

  @override
  Widget build(BuildContext context) {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisAlignment: MainAxisAlignment.start,
      children: [
        Image.network(
          post.image,
          height: 150,
          width: 150,
          fit: BoxFit.cover,
        ),
        const SizedBox(
          width: 16,
        ),
        Expanded(
          flex: 3,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                post.title,
                maxLines: 1,
                style: Theme.of(context).textTheme.bodyLarge,
              ),
              Text(post.content),
            ],
          ),
        )
      ],
    );
  }
}

Langkah 2 – Membuat Tampilan Home

Setelah kalian sudah membuat tampilan post sekarang kita membuat screen untuk menampilkan data dari json, sekarang kita buat class dengan nama home.dart pada folder screen, lalu kalian buat class dengan nama Home dengan extends StatefulWidget.

Perhatian:

Apa perbedaan StatelessWidget dengan StatefulWidget secara singkat StatelessWidget adalah widget yang besifat tidak berubah immutable. Artinya, sekali widget ini dibuat, ia tidak akan berubah selama siklus hidupnya. Sedangkan, StatefulWidget adalah widget yang bersifat dapat berubah mutable. Artinya, widget ini bisa berubah selama siklus hidupnya melalui perubahan state (keadaan).

Ketik kode berikut:

import 'package:flutter/material.dart';

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

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  @override
  Widget build(BuildContext context) {
    return const Placeholder();
  }
}

Lagkah 3 – Membuat Request GET Data dari API

Langkah selajutnya buat request get data dari API lalu di tampilkan, sehingga kode secara lengkap menjadi berikut:

import 'package:flutter/material.dart';
import 'package:flutter_pemula/api/repository.dart';
import 'package:flutter_pemula/component/post_view.dart';
import 'package:flutter_pemula/model/post.dart';

class Home extends StatefulWidget {
  final String title;

  const Home({
    super.key,
    required this.title,
  });

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  final ScrollController _scrollController = ScrollController();
  final Repository _apiService = Repository();

  final List<Post> _posts = [];
  bool _isLoading = false;
  bool _hasMore = true;
  int _currentPage = 1;

  @override
  void initState() {
    super.initState();
    _loadPosts();
    _scrollController.addListener(() {
      if (_scrollController.position.pixels ==
          _scrollController.position.maxScrollExtent) {
        _loadMorePosts();
      }
    });
  }

  Future<void> _loadPosts() async {
    setState(() {
      _isLoading = true;
    });

    try {
      final result = await _apiService.fetchPosts(_currentPage);
      setState(() {
        _currentPage++;
        _posts.addAll(result['posts']);
        _hasMore = result['nextPageUrl'] != null;
      });
    } catch (e) {
      throw Exception(e.toString());
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  Future<void> _loadMorePosts() async {
    if (_isLoading || !_hasMore) return;

    setState(() {
      _isLoading = true;
    });

    try {
      final result = await _apiService.fetchPosts(_currentPage);
      setState(() {
        _currentPage++;
        _posts.addAll(result['posts']);
        _hasMore = result['nextPageUrl'] != null;
      });
    } catch (e) {
      throw Exception(e.toString());
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: _buildPostList(),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          //TODO action to add and edit post
        },
        child: const Icon(Icons.add),
      ),
    );
  }

  Widget _buildPostList() {
    return ListView.builder(
      controller: _scrollController,
      itemCount: _posts.length + (_hasMore ? 1 : 0),
      itemBuilder: (context, index) {
        if (index == _posts.length) {
          return const Center(child: CircularProgressIndicator());
        }
        final post = _posts[index];

        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Card.filled(
            clipBehavior: Clip.hardEdge,
            child: InkWell(
              onTap: () {
                // Todo action ke detail post
              },
              child: Stack(
                children: [
                  PostView(post: post),
                  Positioned(
                    top: 2,
                    right: 2,
                    child: IconButton(
                      onPressed: () {
                        // TODO action to edit post
                      },
                      icon: const Icon(Icons.edit_rounded),
                    ),
                  ),
                  Positioned(
                    bottom: 2,
                    right: 2,
                    child: IconButton(
                      onPressed: () {
                        //TODO action delete post
                      },
                      icon: const Icon(Icons.delete_rounded),
                    ),
                  )
                ],
              ),
            ),
          ),
        );
      },
    );
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }
}

Berikut adalah penjelasan kode yang diberikan:

Import Packages

import 'package:flutter/material.dart';
import 'package:flutter_pemula/api/repository.dart';
import 'package:flutter_pemula/component/post_view.dart';
import 'package:flutter_pemula/model/post.dart';
  • flutter/material.dart: Mengimpor paket Flutter dasar untuk membangun antarmuka pengguna (UI).
  • flutter_pemula/api/repository.dart: Mengimpor file repository.dart yang berisi kelas untuk mengambil data dari API.
  • flutter_pemula/component/post_view.dart: Mengimpor file post_view.dart yang berisi widget untuk menampilkan post.
  • flutter_pemula/model/post.dart: Mengimpor file post.dart yang berisi model data Post.

Home Widget

class Home extends StatefulWidget {
  final String title;

  const Home({
    super.key,
    required this.title,
  });

  @override
  State<Home> createState() => _HomeState();
}
  • Home adalah StatefulWidget yang menerima title sebagai parameter.
  • State<Home> adalah state yang terkait dengan widget Home.

_HomeState

class _HomeState extends State<Home> {
  final ScrollController _scrollController = ScrollController();
  final Repository _apiService = Repository();

  final List<Post> _posts = [];
  bool _isLoading = false;
  bool _hasMore = true;
  int _currentPage = 1;
  • _scrollController: Untuk mengontrol scroll pada list view.
  • _apiService: Instance dari Repository untuk mengambil data dari API.
  • _posts: List yang menyimpan post yang diambil dari API.
  • _isLoading: Boolean yang menunjukkan apakah sedang mengambil data.
  • _hasMore: Boolean yang menunjukkan apakah masih ada data yang bisa diambil.
  • _currentPage: Menyimpan nomor halaman saat ini untuk paginasi.

initState

@override
void initState() {
  super.initState();
  _loadPosts();
  _scrollController.addListener(() {
    if (_scrollController.position.pixels ==
        _scrollController.position.maxScrollExtent) {
      _loadMorePosts();
    }
  });
}
  • initState: Dipanggil sekali saat widget dibuat. Mengambil data post awal dan menambahkan listener untuk mendeteksi ketika mencapai bagian bawah list untuk mengambil lebih banyak post.

_loadPosts dan _loadMorePosts

Future<void> _loadPosts() async {
  setState(() {
    _isLoading = true;
  });

  try {
    final result = await _apiService.fetchPosts(_currentPage);
    setState(() {
      _currentPage++;
      _posts.addAll(result['posts']);
      _hasMore = result['nextPageUrl'] != null;
    });
  } catch (e) {
    throw Exception(e.toString());
  } finally {
    setState(() {
      _isLoading = false;
    });
  }
}
  • _loadPosts: Mengambil post dari API, menambahkannya ke list _posts, dan mengatur status loading.
  • _loadMorePosts: Mirip dengan _loadPosts, tapi hanya dipanggil ketika user scroll ke bawah.

build

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      title: Text(widget.title),
    ),
    body: _buildPostList(),
    floatingActionButton: FloatingActionButton(
      onPressed: () {
        //TODO action to add and edit post
      },
      child: const Icon(Icons.add),
    ),
  );
}
  • build: Membuat UI dengan AppBar, ListView untuk menampilkan post, dan FloatingActionButton untuk menambah atau mengedit post.

_buildPostList

Widget _buildPostList() {
  return ListView.builder(
    controller: _scrollController,
    itemCount: _posts.length + (_hasMore ? 1 : 0),
    itemBuilder: (context, index) {
      if (index == _posts.length) {
        return const Center(child: CircularProgressIndicator());
      }
      final post = _posts[index];

      return Padding(
        padding: const EdgeInsets.all(8.0),
        child: Card.filled(
          clipBehavior: Clip.hardEdge,
          child: InkWell(
            onTap: () {
              // Todo action ke detail post
            },
            child: Stack(
              children: [
                PostView(post: post),
                Positioned(
                  top: 2,
                  right: 2,
                  child: IconButton(
                    onPressed: () {
                      // TODO action to edit post
                    },
                    icon: const Icon(Icons.edit_rounded),
                  ),
                ),
                Positioned(
                  bottom: 2,
                  right: 2,
                  child: IconButton(
                    onPressed: () {
                      //TODO action delete post
                    },
                    icon: const Icon(Icons.delete_rounded),
                  ),
                )
              ],
            ),
          ),
        ),
      );
    },
  );
}
  • _buildPostList: Membuat ListView dengan item Card untuk setiap post. Jika masih ada data yang bisa diambil (_hasMore), tambahkan indikator loading di bagian bawah.

dispose

@override
void dispose() {
  _scrollController.dispose();
  super.dispose();
}
  • dispose: Membersihkan controller ketika widget dihapus untuk menghindari kebocoran memori.

Ini adalah penjelasan tentang kode yang diberikan, yang merupakan sebuah halaman Flutter untuk menampilkan daftar post yang diambil dari API dengan fitur paginasi dan scroll infinite.

Setelah kalian sudah membuat tampilan home atau tampilan pertama sekarang kita edit pada file main.dart dan memanggil tampilan home, sehingga kode pada main.dart menjadi berikut:

import 'package:flutter/material.dart';
import 'package:flutter_pemula/screen/home.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const Home(title: 'Tutorial Flutter Pemula'),
    );
  }
}

setelah itu kalian run pada emulator atau device kalian masing- masing sehingga tampilannya menjadi berikut:

Kesimpulan

Pada artikel kali ini kita banyak belajar hal mulai dari membuat layout untuk menampilkan post dan mengetahui apa itu StatelessWidget dan StatefulWidget. untuk selanjutnya kita akan belajar cara menambah data dan mengirim gambar ke rest API.

Tutorial Flutter dengan Laravel Rest API #4: Insert dan Upload di Flutter dengan Rest API


Membangun Website dan Aplikasi Android Desa Dengan Laravel, React.js dan React Native

Membangun Aplikasi dan Website News Dengan Laravel, React.js dan Android

Faisal Mahadi
Faisal Mahadi

Mobile Apps Developer | Android Enthusiast | Keep Learning | Android Dev
Serta Owner Hariankoding.com

Articles: 8