Flutter API Get using Bloc state management and http plugin

Flutter API Get using Bloc state management and http plugin

这是我第一次写(不专业技术文,请大家多多包涵),希望大家会喜欢。可以留言问我问题,或是给我一点写技术文件的意见。我有看到都会回复呦~


资料夹结构

|-- 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:00\n中午不外送",
    "phone" : "0426321128",
    "address" : "台中市沙鹿区英才路17号",
    "lowest_price" : 300
  }, {
    "id" : 1,
    "name" : "伟哥咸酥鸡台中静宜店",
    "description" : "营业时间:15:30 – 12:15\n没有提供外送服务",
    "phone" : "0423809838",
    "address" : "台中市沙鹿区北势东路517之1号",
    "lowest_price" : 0
  } ]
}

介绍套件

API 串接相关套件

我是使用 http 的套件来串接 API。
官网介绍
另外一种也是很多人用的 API 串接方式。(当然还有很多,在这边不一一介绍)。
Dio

Bloc 相关套件

flutter_bloc
equatable


JSON 转成物件的小技巧

我是使用 quicktype 来帮我自动转成我要的物件。


Bloc 懒人包

Bloc 精髓

Bloc 分成 3 部分,Event Bloc State
https://ithelp.ithome.com.tw/upload/images/20210224/20134548lbQEDixUjg.png

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
  List<Object> get props => [];
}

class RestaurantLoadingState extends RestaurantState {

}

class RestaurantSuccessState extends RestaurantState {

  final RestaurantModel restaurantModel;

  RestaurantSuccessState({@required this.restaurantModel});

  @override
  // TODO: implement props
  List<Object> get props => [restaurantModel];
}

class RestaurantFailState extends RestaurantState {

  final String message;

  RestaurantFailState({@required this.message});

  @override
  // TODO: implement props
  List<Object> get props => [message];
}

restaurant.bloc

class RestaurantBloc extends Bloc<RestaurantEvent, RestaurantState> {

  RestaurantRepository restaurantRepository;

  RestaurantBloc({@required this.restaurantRepository}) : super(RestaurantLoadingState());
  
  RestaurantState get initialState => RestaurantLoadingState();

  @override
  Stream<RestaurantState> 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  {

  Future<RestaurantModel> getRestaurantData() async {
    final response =  await http.get(AppStrings.cricArticleUrl);

    if (response.statusCode == 200) {
      List<int> 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 转成物件。
https://ithelp.ithome.com.tw/upload/images/20210224/201345480Scp1BA2AH.png

UI 介面

home_page.dart

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  RestaurantBloc _restaurantBloc;
  Size size;
  @override
  void initState() {
    super.initState();
    _restaurantBloc = BlocProvider.of<RestaurantBloc>(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: BlocListener<RestaurantBloc, RestaurantState>(
                  listener: (context, state) {
                    if (state is RestaurantFailState) {
                      Scaffold.of(context).showSnackBar(
                        SnackBar(
                          content: Text(state.message),
                        ),
                      );
                    }
                  },
                  child: BlocBuilder<RestaurantBloc, 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, List<Datum> 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 : [email protected]"),
      ),
    );
  }
}

结论

这是以上我写 Flutter 并使用 Bloc 来串接 API 的实际例子。
这是我的 Github 开源码 Flutter-Api-Get-Bloc 可以参考这里。


<<:  如何在 SSMS 查寻资料库的复原模式 - 心得分享

>>:  SEO:关於 Custom Campaign Tracking

风险评监三步曲

风险评监首步曲:资讯资产盘点 标题没打错呦...建立资安制度前,请先确认自已的资安范围有多大。 实务...

第4车厢-老师在说你有没有在听?浅谈CSS选择器及优先权

怎麽选取到元素改变网页原有外观呢?本篇主要整理CSS选取器分类及CSS优先权 切版学习途中,你是否...

Day 26 Ruby Symbol

在 Ruby 内有符号(Symbol)这个物件,他跟字串的用法蛮像的,但本质上则不一样。 究竟 Sy...

[Day 30] 从此人人都可以是tinyML食神

「食神归位!」,从天上传来一个声音,「你本来是掌管烧菜的神仙,因触犯天条,而被罚落凡间受三十六劫、七...

Day 7 - 拯救落後的专案能撑一天是一天(前端篇)

一个大包的专案程序码解压缩後看着满满的程序码思考着我可以实现计画案的目标吗...。接下来这三天会将专...