Flutter API Get using Bloc state management and http plugin

資料來源 => 我的 IT 幫幫忙 文章。
GitHub => WenYeh GitHub

資料夾結構

|-- lib
    |-- bloc // bloc 資料夾
        |-- restaurant_bloc.bloc.dart
        |-- restaurant_bloc.event.dart
        |-- restaurant_bloc.state.dart
    |-- data // 用來裝 respository、model
        |-- model
            |-- api_result_model.dart
        |-- respository
            |-- restaurant_respository.dart
    |-- res
        |-- string
            |-- strings.dart
    |-- ui
        |-- pages
            |-- about_page.dart
            |-- home_page.dart
    |-- main.dart

JSON 格式

{
  "success" : true,
  "message" : "SUCCESS",
  "data" : [ {
    "id" : 0,
    "name" : "八方雲集沙鹿靜宜店",
    "description" : "營業時間:10:00 – 21:00n中午不外送",
    "phone" : "0426321128",
    "address" : "台中市沙鹿區英才路17號",
    "lowest_price" : 300
  }, {
    "id" : 1,
    "name" : "偉哥鹹酥雞台中靜宜店",
    "description" : "營業時間:15:30 – 12:15n沒有提供外送服務",
    "phone" : "0423809838",
    "address" : "臺中市沙鹿區北勢東路517之1號",
    "lowest_price" : 0
  } ]
}

介紹套件

Api

我是使用 http 的套件來串接 API。
這邊是官網介紹 https://flutter.dev/docs/cookbook/networking/fetch-data
另外一種也是很多人用的 API 串接方式。(當然還有很多,在這邊不一一介紹)。
Dio

Bloc 相關套件

flutter_bloc
equatable

JSON 轉成物件的小技巧

我是使用 quicktype.io 來幫我自動轉成我要的物件。

Bloc 懶人包

Bloc 精髓

Event 事件
就是當你要取得資料====> 事件。
當你與畫面進行互動====> 事件。

State 狀態
取得資料中 => 狀態
取資料成功 => 狀態
取資料失敗 => 狀態

Bloc 處理事件屬於什麼狀態
去做判斷
如果這個事件 = 取得資料 那就屬於什麼狀態

進入主題開始寫程式

main.dart

void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Api-Get',
      home: BlocProvider(
        create: (context) => RestaurantBloc(restaurantRepository: RestaurantRepository()),
        child: HomePage(),
      ),
    );
  }
}

Bloc

restaurant_event.dart

abstract class RestaurantEvent extends Equatable{
  @override
  List Object> get props =>[];
}
class FetchRestaurantEvent extends RestaurantEvent {}

restaurant_state.dart

abstract class RestaurantState extends Equatable {
  @override
  ListObject> get props => [];
}
class RestaurantLoadingState extends RestaurantState {
}
class RestaurantSuccessState extends RestaurantState {
  final RestaurantModel restaurantModel;
  RestaurantSuccessState({@required this.restaurantModel});
  @override
  // TODO: implement props
  ListObject> get props => [restaurantModel];
}
class RestaurantFailState extends RestaurantState {
  final String message;
  RestaurantFailState({@required this.message});
  @override
  // TODO: implement props
  ListObject> get props => [message];
}

restaurant.bloc

class RestaurantBloc extends BlocRestaurantEvent, RestaurantState> {
  RestaurantRepository restaurantRepository;
  RestaurantBloc({@required this.restaurantRepository}) : super(RestaurantLoadingState());
  RestaurantState get initialState => RestaurantLoadingState();
  @override
  StreamRestaurantState> mapEventToState(RestaurantEvent event) async* {
    if (event is FetchRestaurantEvent) {
      yield RestaurantLoadingState();
      try {
        RestaurantModel restaurantModel = await restaurantRepository.getRestaurantData();
        print("Bloc Success");
        yield RestaurantSuccessState(restaurantModel: restaurantModel);
      } catch (e) {
        print(  await restaurantRepository.getRestaurantData());
        yield RestaurantFailState(message: "???");
      }
    }
  }
}

Repository

restaurant_result.dart

class RestaurantRepository  {
  FutureRestaurantModel> getRestaurantData() async {
    final response =  await http.get(AppStrings.cricArticleUrl);
    if (response.statusCode == 200) {
      Listint> bytes = response.body.toString().codeUnits;
      var responseString = utf8.decode(bytes);
      return RestaurantModel.fromJson(jsonDecode(responseString));
    } else {
      print("EEEE");
      throw Exception();
    }
  }
}

model

api_result.model.dart
這部分請參考 quicktype 轉成物件。

UI 介面

home_page.dart

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}
class _HomePageState extends StateHomePage> {
  RestaurantBloc _restaurantBloc;
  Size size;
  @override
  void initState() {
    super.initState();
    _restaurantBloc = BlocProvider.ofRestaurantBloc>(context);
    _restaurantBloc.add(FetchRestaurantEvent());
  }
  @override
  Widget build(BuildContext context) {
    size = MediaQuery.of(context).size;
    return MaterialApp(
      home: Builder(
        builder: (context) {
          return Material(
            child: Scaffold(
              appBar: AppBar(
                title: Text("Api-Get"),
                centerTitle: true,
                actions: Widget>[
                  IconButton(
                    icon: Icon(Icons.refresh),
                    onPressed: () {
                      _restaurantBloc.add(FetchRestaurantEvent());
                    },
                  ),
                  IconButton(
                    icon: Icon(Icons.info),
                    onPressed: () {
                      navigateToAboutPage(context);
                    },
                  )
                ],
              ),
              body: Container(
                child: BlocListenerRestaurantBloc, RestaurantState>(
                  listener: (context, state) {
                    if (state is RestaurantFailState) {
                      Scaffold.of(context).showSnackBar(
                        SnackBar(
                          content: Text(state.message),
                        ),
                      );
                    }
                  },
                  child: BlocBuilderRestaurantBloc, RestaurantState>(
                    builder: (context, state) {
                      if (state is RestaurantLoadingState) {
                        return _buildLoading();
                      } else if (state is RestaurantSuccessState) {
                        return buildArticleList(state.restaurantModel);
                      } else if (state is RestaurantFailState) {
                        return _buildErrorUi(state.message);
                      }
                      return Container();
                    },
                  ),
                ),
              ),
            ),
          );
        },
      ),
    );
  }
  Widget _buildLoading() {
    return Center(
      child: CircularProgressIndicator(),
    );
  }
  Widget _buildErrorUi(String message) {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Text(
          message,
          style: TextStyle(color: Colors.red),
        ),
      ),
    );
  }
  Widget buildArticleList(RestaurantModel restaurantModel) {
    return Container(
        child: new ListView.builder(
          padding: EdgeInsets.only(top: 30),
          itemCount: restaurantModel.data.length,
          itemBuilder: (context, int index) =>
              buildCustomItem(context, index, restaurantModel.data),
        ));
  }
  void navigateToAboutPage(BuildContext context) {
    Navigator.push(context, MaterialPageRoute(builder: (context) {
      return AboutPage();
    }));
  }
  /// 餐廳 listView 的 Item 畫面
  Widget buildCustomItem(BuildContext context, int index, ListDatum> data) {
    return Container(
        padding:
        EdgeInsets.only(left: size.width * 0.1, right: size.width * 0.1),
        // height: 500,
        width: size.width,
        decoration: BoxDecoration(
          borderRadius: BorderRadius.all(Radius.circular(10.0)),
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Container(
            //   height: size.height * 0.3,
            //   decoration: BoxDecoration(
            //       borderRadius: BorderRadius.all(Radius.circular(25.0))),
            //   child: Image.network(RestaurantList[index].storeLink),
            //   // child: _ImgaeNetWorkStyle(RestaurantList[index].storeLink)
            // ),
            Padding(
              padding: EdgeInsets.only(top: 16, right: 8.0, left: 8.0),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Text(
                    data[index].name,
                    style: TextStyle(
                        fontSize: 18,
                        fontWeight: FontWeight.bold,
                        color: Colors.cyan[700]),
                  ),
                  Text(
                    "最低$" + data[index].lowestPrice.toString() + "外送",
                    style: TextStyle(
                        fontSize: 18,
                        fontWeight: FontWeight.bold,
                        color: Colors.cyan[700]),
                  ),
                ],
              ),
            ),
            Padding(
              padding: EdgeInsets.all(8),
              child: Text(data[index].description,
                  style: TextStyle(fontSize: 15, color: Colors.cyan[700])),
            ),
            Padding(
              padding: EdgeInsets.only(bottom: 16, right: 8.0, left: 8.0),
              child: Text(data[index].address,
                  style: TextStyle(fontSize: 15, color: Colors.cyan[700])),
            ),
          ],
        ));
  }
}

about_page.dart

class AboutPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("About"),
      ),
      body: Container(
        alignment: Alignment.center,
        child: Text("Developer : wayne900204@gmail.com"),
      ),
    );
  }
}

結論

這是以上我寫 Flutter 並使用 Bloc 來串接 API 的實際例子。
這是我的 Github 開源碼 Flutter-Api-Get-Bloc 可以參考這裡。