Title: Flutter: Complete a picture APP

Since the launch of Flutter, there has been a lot of attention, both positive and negative. As a mobile developer, I naturally want to try Flutter (but its nesting method is really uncomfortable). In line with the attitude that learning something must be done, I usually like to see pictures of cats and dogs. I just want to make an APP that loads pictures of cats and dogs. The interface is as follows (the interface is not very nice).

The main module

NetWork

The API. Dart file defines two classes, DogApi and CatApi, respectively. One is used to process the cat image, and the other is used to process the dog image.

The http_request.dart file encapsulates Http requests for sending and receiving data.

The url.dart file encapsulates the REQUIRED Api and is written for convenience and unified management.

The Models folder defines the Models for the data returned by the different APIS.

The picture page

Waterfall streams use the Flutter_STAGgered_grid_view library, and the author defines the Delegate calculation layout, which is very easy to use.

Widget scene = new StaggeredGridView.countBuilder(
      physics: BouncingScrollPhysics(),
      itemCount: this.breedImgs ! =null ? this.breedImgs.urls.length : 0,
      mainAxisSpacing: 4.0,
      crossAxisSpacing: 4.0,
      crossAxisCount: 3,
      itemBuilder: (context, index) {
        return new GestureDetector(
          onTapUp: (TapUpDetails detail) {
            // Display the related information of this breed
            dynamic breed = this.breeds[this.selectedIdx].description;
            // TODO:Take the current click and then all the subsequent ones
            List<String> unreadImgs = new List<String> ();for (int i = index; i < this.breedImgs.urls.length; i++) {
              unreadImgs.add(this.breedImgs.urls[i]);
            }
            AnimalImagesPage photoPage = new AnimalImagesPage(
              listImages: unreadImgs,
              breed: this.breeds[this.selectedIdx].name,
              imgType: "Cat",
              petInfo: this.breeds[this.selectedIdx],
            );
            Navigator.of(context)
                .push(new MaterialPageRoute(builder: (context) {
              return photoPage;
            }));
          },
          child: new Container(
            width: 100,
            height: 100,
            color: Color(0xFF2FC77D), //Colors.blueAccent,
            child: new CachedNetworkImage(
              imageUrl: this.breedImgs.urls[index],
              fit: BoxFit.fill,
              placeholder: (context, index) {
                return new Center(child: newCupertinoActivityIndicator()); },),),); },// This property controls the amount of space currently occupied by the Cell to achieve the feel of a waterfall
      staggeredTileBuilder: (int index) =>
          new StaggeredTile.count(1, index.isEven ? 1.5 : 1));Copy the code
  • The assemblyPickerView

The default PickerView will call back every time the system switches, and there is no confirm and cancel events. If you use it directly, it will cause frequent network requests and consume too much memory. Therefore, the assembly of the PickerView will add confirm and cancel to execute network requests, so this problem is solved.

    Widget column = Column(
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        new Row(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            new Container(
              width: MediaQuery.of(context).size.width,
              height: 40,
              child: new Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: <Widget>[
                  new Padding(
                    padding: EdgeInsets.only(left: 10.0),
                    child: new GestureDetector(
                      onTapUp: (detail) {
                        // Click ok to exit the current page
                        Navigator.of(context).pop();
                        // Callback operation
                        this.submit(this.selectedIndex);
                      },
                      child: new Text(
                        "Sure",
                        style: TextStyle(
                            decoration: TextDecoration.none,
                            color: Colors.white,
                            fontSize: 18),),),),new Padding(
                    padding: EdgeInsets.only(right: 10.0),
                    child: new GestureDetector(
                      onTapUp: (detail) {
                        // Click ok to exit the current page
                        Navigator.of(context).pop();
                      },
                      child: new Text(
                        "Cancel",
                        style: TextStyle(
                            decoration: TextDecoration.none,
                            color: Colors.white,
                            fontSize: 18() (() [() [() [() [[()new Container(
          height: 1,
          color: Colors.white,
        ),
        // Picker
        new Expanded(
          child: new CupertinoPicker.builder(
              backgroundColor: Colors.transparent,
              itemExtent: 44,
              childCount: this.names.length,
              onSelectedItemChanged: (int selected) {
                this.selectedIndex = selected;
                this.onSelected(selected);
              },
              itemBuilder: (context, index) {
                return new Container(
                  width: 160,
                  height: 44,
                  alignment: Alignment.center,
                  child: new Text(
                    this.names[index],
                    textAlign: TextAlign.right,
                    style: new TextStyle(
                        color: Colors.white,
                        fontSize: 16, decoration: TextDecoration.none), ), ); }),)],);Copy the code
Details page
  • ColumncontainsListView

In the detail page, the top is a picture, the bottom is related information about the variety, and the bottom of the cat is a display of the attributes obtained through THE API. It should be noted that if Column encapsulates the scroll control of MainAxis in the same direction, Width/Height must be set. Similarly, Row also needs to be aware of this.

What I’m doing here is wrapping the ListView in a Container.

new Container(
    margin: EdgeInsets.only(bottom: 10, top: 10),
    height: MediaQuery.of(context).size.height - MediaQuery.of(context).size.width / 1.2 - 80,
     width: MediaQuery.of(context).size.width,
     child: listView,
),
Copy the code
  • Images animation

This part a little more complicated, first of all need to monitor the sliding distance, to transform images, finally according to whether to reach the threshold for switching animation, here I didn’t realize at the last one and the first picture to switch that can be an infinite loop rolling, I just stopped on boundary threshold next animation.

I’m using Matrix4 to set the properties in different positions, and it also simulates 3D effects,

Tween is in charge of all the transformations in the animation.

  void _initAnimation() {
    // Transparency animation
    this.opacityAnimation = new Tween(begin: 1.0, end: 0.0).animate(
        new CurvedAnimation(
            parent: this._nextAnimationController, curve: Curves.decelerate)) .. addListener(() {this.setState(() {
          // Tell Fluter Engine to redraw
        });
      });
    // Flip the animation
    // The third value is Angle
    varstartTrans = Matrix4.identity().. setEntry(3.2.0.006);
    varendTrans = Matrix4.identity() .. setEntry(3.2.0.006)
      ..rotateX(3.1415927);
    this.transformAnimation = new Tween(begin: startTrans, end: endTrans)
        .animate(new CurvedAnimation(
            parent: this._nextAnimationController, curve: Curves.easeIn)) .. addListener(() {this.setState(() {});
          });
    / / zoom
    varsaveStartTrans = Matrix4.identity().. setEntry(3.2.0.006);
    // Pan and scale
    varsaveEndTrans = Matrix4.identity() .. setEntry(3.2.0.006)
      ..scale(0.1.0.1)
      ..translate(20.0.20.0); // MediaQuery.of(context).size.height
    this.saveToPhotos = new Tween(begin: saveStartTrans, end: saveEndTrans)
        .animate(new CurvedAnimation(
            parent: this._saveAnimationController, curve: Curves.easeIn)) .. addListener(() {this.setState(() {});
          });
  }
Copy the code

The Widget references this property to perform the animation.

Widget pet = new GestureDetector(
      onVerticalDragUpdate: nextUpdate,
      onVerticalDragStart: nextStart,
      onVerticalDragEnd: next,
      child: new Transform(
        transform: this.dragUpdateTransform,
        child: Container(
          child: new Transform(
            alignment: Alignment.bottomLeft,
            transform: transform,
            child: new Opacity(
              opacity: opacity,
              child: Container(
                width: MediaQuery.of(context).size.width / 1.2,
                height: MediaQuery.of(context).size.width / 1.5 - 30,
                child: new Padding(
                  padding: EdgeInsets.all(0),
                  child: new CachedNetworkImage(
                    imageUrl: this.widget.listImages[item],
                    fit: BoxFit.fill,
                    placeholder: (context, content) {
                      return new Container(
                        width: MediaQuery.of(context).size.width / 2.0 - 40,
                        height: MediaQuery.of(context).size.width / 2.0 - 60,
                        color: Color(0xFF2FC77D),
                        child: new Center(
                          child: newCupertinoActivityIndicator(), ), ); }, ((), ((), ((), ((), ((), (();Copy the code
Firebase_admob

Note: you need to go to Firebase’s official website to register the APP, and then download the iOS and Android configuration files to the specified location, otherwise the program will blink back when starting.

IOS Info. plist: GADApplicationIdentifier also needs to be configured, although the ID is registered at startup in Dart.

Android Manifst.xml also needs to be configured

<meta-data
 android:name="com.google.android.gms.ads.APPLICATION_ID"
 android:value=""/>
Copy the code

Here is the problem caused by my personal coding. I tried to control the AD display by myself, added a skip button (to force the viewer to watch for a period of time), click skip to set setState, but in the build method, the AD was requested again, resulting in an endless loop. At last, due to too many requests, the account was suspended without setting the device as the test device or the test ID used. Therefore, you should avoid this problem when using the device and try to add your own device to the test device.

Easy to use (the official demo code directly copy can also be used).

class AdPage {
  MobileAdTargetingInfo targetingInfo;

  InterstitialAd interstitial;

  BannerAd banner;

  void initAttributes() {
    if (this.targetingInfo == null) {
      this.targetingInfo = MobileAdTargetingInfo(
          keywords: ["some keyword for your app"].// Prevent clicks and displays deemed invalid by Google.
          testDevices: ["Your Phone"."Simulator"]);

      bool android = Platform.isAndroid;

      this.interstitial = InterstitialAd(
        adUnitId: InterstitialAd.testAdUnitId,
        targetingInfo: this.targetingInfo,
        listener: (MobileAdEvent event) {
          if (event == MobileAdEvent.closed) {
            // Click close
            print("InterstitialAd Closed");
            this.interstitial.dispose();
            this.interstitial = null;
          } else if (event == MobileAdEvent.clicked) {
            / / close
            print("InterstitialAd Clicked");
            this.interstitial.dispose();
            this.interstitial = null;
          } else if (event == MobileAdEvent.loaded) {
            / / load
            print("InterstitialAd Loaded");
          }
          print("InterstitialAd event is $event"); });// this.banner = BannerAd(
// targetingInfo: this.targetingInfo,
// size: AdSize.smartBanner,
// listener: (MobileAdEvent event) {
// if (event == MobileAdEvent.closed) {
// // Click close
// print("InterstitialAd Closed");
// this.interstitial.dispose();
// this.interstitial = null;
// } else if (event == MobileAdEvent.clicked) {
/ / / / closed
// print("InterstitialAd Clicked");
// this.interstitial.dispose();
// this.interstitial = null;
// } else if (event == MobileAdEvent.loaded) {
/ / / / loading
// print("InterstitialAd Loaded");
/ /}
// print("InterstitialAd event is $event");
/ /});}}@override
  void show() {
    // Initialize the data
    this.initAttributes();
    // Then control the jump
    if (this.interstitial ! =null) {
      this.interstitial.load();
      this.interstitial.show(
            anchorType: AnchorType.bottom,
            anchorOffset: 0.0,); }}}Copy the code

The project is relatively simple, but I also encountered many problems in the process of writing, and learned a lot in the process of slowly solving them.

Some resources

Public APIs

The code address