This is a project that rewrote the original APP for practical operation after two official demos. Although this app basically only has one page, which is not complex, it can be said that it is rich in content and involves many common functions. After spending two days of the three-day holiday to finish most of the content, I still feel that I have learned a lot of knowledge points, so I will make some summaries to avoid forgetting them in a few days.

Project address: friday_today, because the whole thing is not completely completed, the body code is not divided into several files, all in main.dart, 800 lines in total

Here’s the original native code, all written in Kotlin, which is basically inside an Activity: FridayActivity and the layout file activity_Friday.xml

Here are two more screenshots of the Flutter version above and the native Android version below:

Generally speaking, the layout of Flutter is similar to that of ReactNative, with State controlling State, Flex layout and so on. Flutter contains widgets that describe controls and widgets that describe layouts. Therefore, we have to face a lot of nesting. We miss the convenience of ConstraintLayout when we write layouts, but we find that this layout has one advantage after we write many times: It is very easy to write a method that returns a generic widget, and then just call the method to build similar layouts over and over again, although in ndroid you can use include tags (which can only be reused) or custom layouts (requiring new classes is a hassle). But it is not as convenient and quick to use as the flutter, which can be encapsulated by a direct method.

Next, from the root layout, take a look at the widgets used and some potholes:

Root layout: Stack, Position, and AspectRatio

App interface is mainly divided into two parts, used to display interface (including the background and text), and is used to control the interface (i.e., black translucent with a pile of button on the drawing section), which shows part of the full screen and cut for the square two modes, so no matter how the two parts must be overlapping, I use two methods to build the two parts of components, The Stack is then used to hold the two parts (the outer body is the content of the Scaffold) :

body: Stack(
 alignment: Alignment.bottomCenter,
  children: <Widget>[
  // The display section should be completely centered
    Center(
    // This layer of RepaintBoundary is used for screenshots, which will be discussed later
      child: RepaintBoundary(
        key: screenKey,
        child: screenType == 1
	        // The width and height ratio is one
            ? AspectRatio(
                aspectRatio: 1 / 1,
                child: Container(
                  color: bgColor,
                  child: _buildShowContent(),
                ),
              )
             // Full screen
            : Container(
                color: bgColor,
                child: _buildShowContent(),
              ),
      ),
    ),
    _buildControlPanel(),
  ],
));
Copy the code

At first I thought that setting alignment to Botton in the sub-layout would fix the controlPanel part at the bottom, but it didn’t work. The content flew to the top and the translucent background didn’t appear, presumably because the controlPanel content occupied the height of the screen. You can’t even tell if it’s fixed at the bottom. So the solution is to add the maximum mainAxisSize on the vertical axis to Column in the controlPanel: mainAxissie.min (see the code for the _buildControlPanel() method). In the first few random attempts, I found a way to achieve the same effect by accident, which is to use Pisitioned to fix.

// Hold this part in the bottom of the toy, then the left and right are 0 to expand the position to give the effect of match_parent
Positioned(
  bottom: 0,
  left: 0,
  right: 0,
  child: _buildControlPanel(),
)
Copy the code

AspectRatio is used to limit the width and height of sub-layouts to a fixed AspectRatio.

Text display section, Column, BoxDecoration

Part of the layout shown is very simple, just put a few words from top to bottom, which can be achieved by using Column, which is centered on the main axis. Instead of allowing margin and padding for all Android views, flutter should have a Container attached to it, which can then be modified by adding various properties to the Container setting. On the other hand, BoxDecoration can easily set the background color, rounded corners, borders, and shadows for the child elements of containers. I think it is more practical to have a semicircle rounded corner method for the Button class. Otherwise you have to wait until the rendering is finished and then set the rounded corners at half the height.

/// Draw the middle part of the display
_buildShowContent() {
  return Column(
        mainAxisAlignment: MainAxisAlignment.center, // The child layout is centered on the horizontal axis
        children: <Widget>[
          Container(
            margin: EdgeInsets.only(bottom: 20.0),
// padding: EdgeInsets.symmetric(vertical: 15, horizontal: 20),
            height: 60.0.// There is no ready - made semicircle method, set the fixed height and add rounded corners
            decoration: BoxDecoration(
              color: bubbleColor, // Set the bubble (text background) color
              borderRadius: BorderRadius.circular(30.0),
            ),
            child: Center(// Center text as a whole
              widthFactor: 1.3.// The width is 1.3 times the width of the text
              child: Text(
                langType == 0 ? "Is it Friday?" : "Is today Friday?",
                style: TextStyle(
                    fontSize: 25, color: textColor, fontFamily: fontName),
              ),
            ),
          ),
          Text(
            today.weekday == 5
                ? langType == 1 ? "YES!" : "Yes"
                : langType == 1 ? "NO" : "Not",
            style:
                TextStyle(fontSize: 90, color: textColor, fontFamily: fontName),
          ),
          Container(
            margin: EdgeInsets.only(top: 20.0),
            child: Text(
              "${weekdayToString(today.weekday)} ${today.year}.${today.month}.${today.day}",
              textAlign: TextAlign.center,
              style: TextStyle(color: textColor, fontFamily: fontName),
            ),
          ),
        ],
      );
}
Copy the code

The first line of text has a white rounded background as the bubble, and the text needs to be centered in the bubble. In Android, I use Gravity = Center, then set the padding, and finally set the rounded corner according to the entire height of the text after rendering, but here I fixed the background height. The solution is to surround the Text with a Center. The width of the Text is set by widthFactor to a multiple of the Text width. The operation of setting the width of the flutter with a multiple makes me feel strange.

The control panel

The control panel is composed of various buttons, and the whole is a Column with 7 rows from top to bottom. Row is used to arrange the buttons in each Row, which is a relatively regular arrangement. Because the buttons have many similarities, several methods are used here to encapsulate different buttons:

// draw the entire control panel
_buildControlPanel() {
  return Container(
    padding: EdgeInsets.all(12.0),
    color: Color.fromARGB(30.0.0.0), // Translucent black background
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start, // The child layout is left aligned on the horizontal axis
      mainAxisSize: MainAxisSize.min, // Height keep the minimum height to be fixed at the bottom
      children: <Widget>[
        // The first three lines are the background, bubble and text color controls, wrapped in one method
        _buildColorController(0),
        _buildColorController(1),
        _buildColorController(2),
        // Display a selection of fonts
        Container(
          height: 30.0,
          margin: EdgeInsets.only(bottom: 8.0),
          // Use ListView instead of Row
          child: new ListView(
            padding: EdgeInsets.all(0.0),
            scrollDirection: Axis.horizontal, // Set horizontal scrolling
            children: _buildFontRow(langType), // Generate a group of buttons to switch fonts based on the font type
          ),
        ),
        Container(
          margin: EdgeInsets.only(bottom: 8.0),
          child: Row(
            // A row of four buttons is generated in a generic way, and the click event is also passed in externally
            mainAxisAlignment: MainAxisAlignment.start,
            children: <Widget>[
              _buildCommonButton(
                  Text(
                    FridayLocalizations.of(context).square,
                    style: TextStyle(color: FridayColors.jikeWhite),
                  ),
                  25.0,
                  () => setState(() {
                        screenType = 1;
                      })),
              _buildCommonButton(
                  Text(
                    FridayLocalizations.of(context).full,
                    style: TextStyle(color: FridayColors.jikeWhite),
                  ),
                  25.0,
                  () => setState(() {
                        screenType = 0;
                      })),
              _buildCommonButton(
                  Text(
                    FridayLocalizations.of(context).titleCn,
                    style: TextStyle(color: FridayColors.jikeWhite),
                  ),
                  25.0,
                  () => {_changeLangType(0)}),
              _buildCommonButton(
                  Text(
                    FridayLocalizations.of(context).titleEn,
                    style: TextStyle(color: FridayColors.jikeWhite),
                  ),
                  25.0,
                  () => {_changeLangType(1)})],),),// The four function buttons are 2X2, with two rows
        Container(
          margin: EdgeInsets.only(bottom: 8.0),
          child: Row(
            children: <Widget>[
              _buildCommonButton(
                  Text(
                    FridayLocalizations.of(context).wallpaper,
                    style: TextStyle(
                        color: FridayColors.jikeWhite, fontSize: 14.0),),40.0,
                  () => {_capturePng(1)}),
              _buildCommonButton(
                  Text(
                    FridayLocalizations.of(context).titleShare,
                    style: TextStyle(
                      color: FridayColors.jikeWhite,
                      fontSize: 14.0,),),40.0,
                  () => {_capturePng(2)}),
            ],
          ),
        ),
        Container(
          margin: EdgeInsets.only(bottom: 8.0),
          child: Row(
            children: <Widget>[
              _buildCommonButton(
                  Text(
                    FridayLocalizations.of(context).group,
                    style: TextStyle(
                        color: FridayColors.jikeWhite, fontSize: 14.0),),40.0,
                  _toJike),
              _buildCommonButton(
                  Text(
                    FridayLocalizations.of(context).titleSave,
                    style: TextStyle(
                      color: FridayColors.jikeWhite,
                      fontSize: 14.0,),),40.0,
                  () => {_capturePng(0(}), [(), [(), [(); }Copy the code

Line: Expanded, Button

Next draw the first three lines, because they are in the same format, encapsulated in the following method:

/// Draw three lines of [type] background/bubble/font to switch colors
_buildColorController(int type) {
  return Container(
    margin: EdgeInsets.only(bottom: 8.0),
    child: Row(
      mainAxisAlignment: MainAxisAlignment.start,  // From left to right on the spindle
      mainAxisSize: MainAxisSize.max, // The width is full
      children: <Widget>[
        Text(
          getTitleByType(type, context), // The title is obtained by type
          style: TextStyle(
            fontSize: 12.0,),),// Generate color buttons
        _buildColorClickDot(type, FridayColors.jikeWhite),
        _buildColorClickDot(type, FridayColors.jikeYellow),
        _buildColorClickDot(type, FridayColors.jikeBlue),
        _buildColorClickDot(type, FridayColors.jikeBlack),
        // Expanded fills the remaining width
        Expanded(
          child: Container(
            height: 30.0,
            padding: EdgeInsets.all(2.0), // This padding is used to squeeze the zoom button itself
            margin: EdgeInsets.only(left: 8.0),
            child: RaisedButton(// Button with raised shadow effect
              child: Text(
                FridayLocalizations.of(context).moreColor,
                style: TextStyle(fontSize: 12.0, color: Colors.white),
              ),
              onPressed: () => {_showPickColorDialog(type)},
              color: Colors.black26,
              shape: StadiumBorder(), // Semicircular background
            ),
          ),
        ),
        Expanded(
          child: Container(
            height: 30.0,
            padding: EdgeInsets.all(2.0),
            margin: EdgeInsets.only(left: 8.0),
            child: RaisedButton(
              padding: EdgeInsets.all(0.0),
              child: Text(
                FridayLocalizations.of(context).customColor,
                style: TextStyle(fontSize: 12.0, color: Colors.white),
              ),
              onPressed: () => {_showCustomColorDialog(type)},
              color: Colors.black26,
              shape: StadiumBorder(),
            ),
          ),
        ),
      ],
    ),
  );
}
Copy the code

StadiumBorder() : shape: StadiumBorder() StadiumBorder: shape: StadiumBorder() StadiumBorder: shape: StadiumBorder() StadiumBorder: shape: StadiumBorder()

/// Draw a small dot to switch colors
_buildColorClickDot(int type, Color color) {
  return Container(
    margin: EdgeInsets.only(left: 8.0),
    child: Container(
      // Limit the width of the button
      width: 20.0,
      height: 20.0,
      child: RaisedButton(
// onPressed: _changeColor(type, color)
        onPressed: () => {_changeColor(type, color)},
        color: color,
        // Set the shape to a circle button, otherwise the shape is a 20 by 20 square
        StadiumBorder() : StadiumBorder() StadiumBorder() : StadiumBorder(
        shape: CircleBorder(
            side: BorderSide(
          color: Colors.transparent,
          width: 0(), ((), ((), ((); }Copy the code

Initially I wanted to set up a click event with a colored View as I would have done native, but not everything can be clicked inside the Flutter. If the Flutter is not a Button widget, I need to set up a GestureDetector widget to add the click event (which will be used later). So I used Button directly, and the stereo effect looked better than the original. When I used Button, I felt very troublesome at first because it had some built-in padding and so on, which made it difficult to control the size. Finally, I found that it was ok to set the width and height by putting a Container on the outer surface. I was mentally retarded.

The last two text buttons of a line were also used to have padding to make the text not fit, and even the buttons themselves exceeded the screen. Finally, they were Expanded to make their width self-adaptive. However, the text might not fit on a small screen, so we reduced the word count →_→ of the text

All the other lines have similar structures, so I won’t repeat them.

Pop-up section: AlertDialog

A dialog pops up when more colors and custom colors are selected. The Flutter has existing Dialog-type controls. Generally, the Flutter uses SimpleDialog to display a multi-line list of options, while the AlertDialog displays custom content and has several buttons at the bottom. Flutter has a built-in showDialog method that takes parameters and uses a Builder to generate a Dialog:

/// display a custom colored dialog
Future<void> _showCustomColorDialog(int type) async {
  return showDialog<void>(
    context: context,
    barrierDismissible: false.// user must tap button!
    builder: (BuildContext context) {
      return AlertDialog(
        contentPadding: EdgeInsets.all(16.0), title: Text(getCustomColorTitleByType(type, context)), content: ... ./ / content widget
        actions: <Widget>[
          FlatButton(
            child: Text('OK'), onPressed: () { _handleSubmitted(type, _inputController.text); Navigator.of(context).pop(); },),,); }); }Copy the code

Here is the content section of dialog:

Slider widgets: GridView, SingleChildScrollView, Wrap

One Dialog needs to show more than 500 color list, the original I use a slide SingleChildScrollViewcontent, which put more than 500 button, from left to right in line arrangement, with Wrap more suitable than ListView

content: SingleChildScrollView(/ / slide
  child: Center( // Set the center to avoid asymmetrical white space on both sides
    child: Wrap(
      spacing: 5.0,
      runSpacing: 5.0,
      children: getColorRows(type),
    ),
  ),
),
Copy the code

But after looking at the source code for SingleChildScrollView, which recommended a bunch of other controls, there were so many that the Flutter provided was confusing. After a few attempts, I found that the use of GridViet. count worked perfectly, similar to the use of Wrap:

content: GridView.count(
  crossAxisCount: 8.// The number of buttons in a row
  crossAxisSpacing: 5.0./ / column spacing
  mainAxisSpacing: 5.0./ / line spacing
  children: getColorRows(type),
),
Copy the code

Input and prompt: TextField, Snackbar

The other Dialog displays a text input box for a six or eight digit color value. This involves text input and control. There are examples in the official Demo, so we can use them directly:

final TextEditingController _inputController = newTextEditingController(); . content: TextField( controller: _inputController, decoration: InputDecoration( hintText: FridayLocalizations.of(context).hintInputColor, hintStyle: TextStyle(fontSize:12.0)),
),
actions: <Widget>[
  FlatButton(
    child: Text('OK'),
    onPressed: () {
      _handleSubmitted(type, _inputController.text); // Submit when you click OK
      Navigator.of(context).pop(); / / dialog is hidden},),],...void _handleSubmitted(int type, String text) {
 _inputController.clear(); // Clear the input text
  if(text.length ! =8&& text.length ! =6) {
    // The input format is incorrect, a message is displayed
    (scaffoldKey.currentState as ScaffoldState).showSnackBar(new SnackBar(
      content: new Text(FridayLocalizations.of(context).noticeWrongInput),
    ));
  } else {
    // Set the color
    _changeColor(
        type, Color(int.parse(text.length == 8 ? "0x$text" : "0xFF$text"))); }}Copy the code

Scaffold. Of (context). ShowSnackBar (SnackBar); Of () called with a context that does not contain a Scaffold. This mistake was made because I didn’t know enough about the context of the flutter. After looking up some information, I had a general idea of what widgets to remove from the flutter so that the context could be found (see this article). However, I didn’t want to do this to display a snackBar, and finally found another solution. ScaffoldState ScaffoldState ScaffoldState ScaffoldState ScaffoldState ScaffoldState ScaffoldState ScaffoldState ScaffoldState ScaffoldState ScaffoldState ScaffoldState ScaffoldState ScaffoldState ScaffoldState ScaffoldState ScaffoldState ScaffoldState ScaffoldState ScaffoldState

GlobalKey<ScaffoldState> scaffoldKey = GlobalKey(); .@override
  Widget build(BuildContext context) {
    returnScaffold( key: scaffoldKey, body: ... ) ; }... . scaffoldKey.currentState.showSnackBar(new SnackBar(
      content: newText(FridayLocalizations.of(context).noticeWrongInput), )); .Copy the code

This is the general content of the whole page. Originally, I planned to complete the page and not use.jpg, but when writing this summary, I also made some attempts to explore whether there are other ways to achieve it, and finally made some optimization. In fact, as an Android developer who is used to native effects, there are many times when he is confused about how to write widgets that are easy to implement into Flutter, such as how to implement WRAP_COTENT and MATCH_PARENT (see this article) etc. Because of this, I think more layouts need to be written to get used to the code style of Flutter. The next article (if any) will describe the features of the app, including taking screenshots, saving images, sharing, jumping to other apps, SharedPreference saving data, calling native methods, multilingual, etc.

Chapter 2 Portal: Simple Project Combat Flutter