面向React Native开发者的Flutter——Props、本地存储、路由、手势监听、HTTP请求、表单输入

1,409 阅读8分钟

一、Props

在React Native中,大多数组件在创建时都可以使用不同的参数或属性进行自定义,称为props。这些参数可以在子组件中使用this.props

// React Native
class CustomCard extends React.Component {
    render() {
        <View>
            <Text> Card {this.props.index} </Text>
            <Button
                title='Press'
                onPress={() => this.props.onPress(this.props.index)}
            />
        </View>
    }
}

class App extends React.Component {

    onPress = index => {
        console.log('Card', index);
    };
    
    render() {
        return (
            <View>
                <FlatList
                    data={[ ... ]}
                    renderItem={({ item }) => (
                        <CustomCard onPress=({this.onPress} index={item.key}/>
                    )};
                />
            </View>
        );
    }
}

在Flutter中,您可以分配一个布局变量或函数,该变量或函数final有在参数化构造函数中接手到的属性。

import 'package:flutter/material.dart';

class CustomCard extends StatelessWidget {
  const CustomCard({
    Key? key,
    required this.index,
    required this.onPress,
  }) : super(key: key);

  final int index;
  final void Function() onPress;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Column(
        children: <Widget>[
          Text('Card $index'),
          TextButton(
            onPressed: onPress,
            child: const Text('Press'),
          ),
        ],
      ),
    );
  }
}

class UseCard extends StatelessWidget {
  final int index;

  const UseCard({Key? key, required this.index}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return CustomCard(
        index: index,
        onPress: () {
          print('Card $index');
        });
  }
}
  • Android

image.png

  • iOS

image.png

二、本地存储

如果您不需要存储大量数据,并且不需要结构,您可以使用shared_preferences它来读取和写入基本数据类型的持久键值对:布尔值、浮点数、整数、长整数和字符串。

2.1、如何存储应用程序全局的持久键值对?

在React Native中,您可以使用组件的setItemgetItem函数AsyncStorage来存储和检索对应用程序而言持久且全局的数据。

// React Native
await AsyncStorage.setItem('counterkey', json.stringf(++this.state.counter));
AsyncStorage.getItem('counterkey').then(value => {
   if (value != null ) {
       this.setState({ counter: value });
   }
});

在Flutter中,使用shared_preferences插件来存储和检索对应用程序而言持久且全局的键值数据。该shared_preferences插件包装iOS的NSUserDefaults和Android的SharedPreferences,为简单数据持久存储。要使用该插件,请将shared_preferences其作为依赖项pubspec.yaml文件中,然后将包导入到Dart文件中。

dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^2.0.13
import 'package:shared_preferences/shared_preferences.dart';

要实现持久数据,请使用SharedPreferences类提供的setter方法。Setter方法可用于各种基本类型,例如setIntsetBoolsetString。要读取数据,请使用SharedPreferences类提供的适当的getter方法。对于每个setter都有一个对应的getter方法,例如getIntgetBoolgetString

Future<void> updateCounter() async {
    final prefs = await SharedPreferences.getInstance();
    int? counter = prefs.getInt('counter');
    if (counter is int) {
        await prefs.setInt('counter', ++counter);
    }
    setState(() {
        _counter = counter;
    });
}

三、路由

大多数应用程序包含多个屏幕,用于显示不同类型的信息。例如,您可能有一个显示图像的产品屏幕,用户可以在其点击产品图像以在屏幕上获取有关该产品的更多信息。

在Android中,新屏幕是Activity。在iOS中,新屏幕是新的ViewController。在Flutter中,屏幕就是Widget! 要导航到Flutter中的新屏幕,请使用Navigator小部件。

3.1、如何在屏幕之间导航?

在React Native中,主要有三个导航器:StackNavigator、TabNavigator和DrawerNavigator。每个都提供了一种配置和定义屏幕的方法。

// React Native
const MyApp = TabNavigator(
    { Home: { screen: HomeScreen }, Natifications: { screen: tabNavScreen} },
    { tabBarOptions: { activeTintColor: '#e91e63' }}
);
const SimpleApp = StackNavigator({
    Home: { screen: MyApp },
    stackScreen: { screen:StackScreen }
});
export default (MyApp1 = DrawerNavigaror({
    Home: {
        screen: SimpleApp
    },
    Screen2: {
        screen: drawerScreen
    }
}));

在Flutter中,有两个主要哦的小部件用于在屏幕之间导航:

  • Router是应用程序平或页面的抽象。
  • Navigator是管理路由的小部件。

Navigator被定义为小部件,它管理一组具有堆栈规则的小部件。导航器管理Route对象堆栈并提供管理堆栈的方法,例如Navigator.pushNavigator.pop。路由列表可以在MaterialApp小部件中指定,或者它们可以动态构建,例如,在Hero animation。以下示例指定MaterialApp小部件中的命名路由。

注意:  对于大多数应用程序,不再推荐使用命名路由。有关详细信息,请参阅 导航概述页面中的限制。

import 'package:flutter/material.dart';

class NavigationApp extends StatelessWidget {
   // This widget is the root of your  applicatuib. 
   const NavigationApp({Key? key}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        // ...
        routes: <String, WidgetBuilder>{
          '/a': (context) => const UsualNavScreen(),
          '/b': (context) => const DrawerNavScreen().
        },
      ),
    );
  }
}

要导航到命名路由,该Navigator.of()方法用于指定BuildContext(小部件树中小部件位置的句柄)。路由的名称被传递给pushNamed函数以导航到指定的路由。

Navigatoor.of(context).pushNamed('/a');

您还可以使用push方法,Navigator该方法将given添加Route到最紧密包围given的导航器的历史记录中BuildContext,并过渡到它。在下面的示例中,MaterialPageRoute小部件是一个模拟路由,它用平台自适应转换替换整个屏幕。它需要一个WidgetBuilder作为必须的参数。

Navigator.push(
    context,
    MaterialPageRoute(
        builder: (context) => const UsualNavScreen(),
    ),
);

3.2、如何使用标签到哦行和抽屉导航?

在Material Design应用程序中,Flutter导航有两个主要选项:选项卡和抽屉。当没有足够的空间来支持选项卡时,抽屉提供了一个很好的选择。

3.2.1、标签导航

在React Native中,createBottomTabNavigator用于TabNavigation选项卡和用于选项卡导航。

// React Native
import { createBottomTabNavigator } from 'react-navigation';

const MyApp = TabNavigator(
    { Home: { screen: HomeScreen }, Notifications: { screen: tabNavScreen } },
    { tabBarOptions: { activeTintColor: '#e91e63' } }
);

Flutter为抽屉和选项卡导航提供了几个专门的小部件: TabController: 协调TabBar和TabBarView之间的选项卡选择。 TabBar: 显示一行水平的选项卡。 Tab: 创建material design Tab bar。 TabBarView: 显示与当前所选选项卡对应的小部件。


import 'package:flutter/material.dart';

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  late TabController controller = TabController(length: 2, vsync: this);
  
  @override
  Widget build(BuildContext context) {
    return TabBar(
      controller: controller,
      tabs: const <Tab>[
        Tab(icon: Icon(Icons.person)),
        Tab(icon: Icon(Icons.email)),
      ],
    );
  }  
}

需要TabController来协调TabBarTabBarView之间的选项卡选择。TabController构造函数的长度参数是选项卡总数。每当框架触发状态更改时,需要TickerProvider来触发通知。TickerProvider是垂直同步的。每当您创建新的TabController时,将vsync:this传递给TabController构造函数。

TickerProvider是由可以实现Ticker对象类的接口。每当帧触发时必须同追任何对象都可以使用Tickers,但它们最常通过AnimationController间接使用。AnimationContrllers需要一个TicjerProvider来获取它们Ticker。如果您从State创建AnimationCoontrooller,则可以使用高TickerProviderStateMixinSingleTickerProviderStateMixin类来获取合适的TickerProvider

Scaffold小部件包装了一个新的TabBar小部件并创建了两个选项卡。TabBarView小部件作为Scaffold小部件的body参数传递。与TabBar小部件的选项卡对应的所有屏幕都是TabBarView小部件的子项以及相同的TabController

import 'package:flutter/material.dart';

class _NavigationHomePageState extends State<NavigationHomePage> with SingleTickerProviderStateMixin {
  late TabController controller = TabController(length: 2, vsync: this);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      bottomNavigationBar: const Material(
        color: Colors.blue,
        child: TabBar(
          tabs: <Tab>[
            Tab(icon: Icon(Icons.person),),
            Tab(icon: Icon(Icons.email),),
          ],
        ),
      ),
      body: TabBarView(
        controller: controller,
        children: const <Widget>[
          HomeScreen(),
          TabScreen(),
        ],
      ),
    );
  }
  
}

3.2.2、抽屉导航

在React Native中,导入所需的react-navigation包,然后使用createDrawerNavigatorDrawerNavigation

// React Native
export default (MyApp1 = DrawerNavigator({
    Home: {
        screen: SimpleApp
    },
    Screen2: {
        screen: drawerScreen
    }
}));

在Flutter中,我们可以结合Drawer小部件和Scaffold来创建带有Material Design抽屉的布局。要将抽屉添加到应用程序,请将其包装在Scaffold小部件中。Scaffold小部件为遵循Material Design准则的应用程序提供一致的视觉结构。它还支持特殊的Material Design组件,例如DrawersAppBarsSnackBars

Drawer小部件是一个Material Design面板,它从Scaffold的边缘水平滑入以显示应用程序中的导航链接。您可以提供一个ElevatedButtonText小部件或项目列表以显示为Drawer小部件的子项。在以下示例中,ListTile小部件提供点击导航。

@override
Widget build(BuildContext context) {
  return Drawer(
    elevation: 2.0,
    child: ListTile(
      leading: const Icon(Icons.change_history),
      title: const Text('Screen2'),
      onTap: () {
        Navigator.of(context).pushNamed('/b');
      },
    ),
  );
}

Scaffold小部件还包括一个AppBar小部件,当Scaffold中有可用的Drawer时,它会自动显示适当的IconButton以显示抽屉。Scaffold自动处理边缘滑动手势以显示Drawer

@override
Widget build(BuildContext context) {
  return Scaffold(
    drawer: Drawer(
      elevation: 2.0,
      child: ListTile(
        leading: const Icon(Icons.change_history),
        title: const Text('Screen2'),
        onTap: () {
          Navigator.of(context).pushNamed('/b');
        },
      ),
    ),
    appBar: AppBar(title: const Text('Home'),),
    body: Container(),
  );
}
  • Android

navigation.gif

  • iOS

navigation (1).gif

四、手势监测和触摸时间处理

为了监听和相应手势,Flutter支持点击、拖动和缩放。Flutter中的手势系统有两个独立层,第一层包括原始指针时候ii教案,它描述指针在屏幕的位置和移动(例如触摸、鼠标和手写笔移动)。第二层包括手势,它描述了由一个或多个指针移动组成的语义动作。

4.1、如何向小部件添加点击或按下监听器?

在React Native中,使用PanResponder或组件将监听器添加到Touchable组件。

// React Native
<TouchableOpacity
    onPress={() => {
        console.log('Press');
    }}
    onLongPress={() ={
        console.log('Long Press')
    }}
>
    <Text>Tap or Long Press</Text>
</TouchableOpacity>

对于更复杂的手势和将多个触摸组合成一个手势,PanResponder使用。

// React Native
class App extends Component {
    componentWillMount() {
        this._panResponder = PanResponder.create({
            onMoveShouldSetPanResponder: (event, gestureState) => !!getDirection(gestureState),
            onPanResponderMove: (event, gestureState) => true,
            onPanResponderRelease: (event, gestureState) => {
                const drag = getDirection(gestureState);
            },
            onPanSponderTerminationRequest: (event, gestureState) => true
        });
    }
    
    render() {
        return (
            <View style={styles.containre} {...this._panResponder.panHanders}>
                <View style={styles.center}>
                    <Text>Swipe Horizontally or Verucally</Text>
                </View>
            </View.
        );
    }
}

在Flutter中,要向小部件添加点击(或按下)监听器,请使用按钮或具有onPress: field。或者,通过将其包装在GestureDetector.

@override
Widget build(BuildContext context) {
  return GestureDetector(
    child: Scaffold(
      appBar: AppBar(title: const Text('Gestures'),),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: const <Widget>[
            Text('Tap, Long Press, Swipe Horizontally o Vertically')
          ],
        ),
      ),
    ),
    onTap: () {
      print('Tapped');
    },
    onLongPress: () {
      print('Long Pressed');
    },
    onVerticalDragEnd: (value) {
      print('Swiped Vertically');
    },
    onHorizontalDragEnd: (value) {
      print('Swiped Horizontally');
    },
  );
}

有关更多信息,包括 FlutterGestureDetector回调列表,请参阅GestureDetector 类

  • Android

image.png

  • iOS

image.png

五、发出HTTP网络请求

从Internet获取数据大多数应用程序来说很常见。在Flutter中,http包提供了从互联网获取数据的最简单方法。

5.1、如何从API调用获取数据?

React Native为网络提供了Fetch API——您发出一个获取请求,然后接收相应以获取数据。

// React Native
_getIpAddress = () => {
    fetch('https://httpbin.org/ip')
        .then(response => response.json())
        .then(responseJson => {
            this.setState({ _ipAddress: responseJson.origin });
        })
        .catch(error => {
            console.error(error);
        });
};

Flutter使用http包。要安装http包,请将其添加到pubspec.yaml的依赖项部分。

dependencies:
  flutter:
    sdk: flutter
  http: <latest_version>

Flutter使用dart.io核心HTTP支持客户端。要创建HTTP客户端,请导入dart:io.

import 'dart:io';

客户端支持以下HTTP操作:GET、POST、PUT和DELETE。

final url = Uri.parse('https://httpbin.org/ip');
final httpClient = HttpClient();

Future<void> getIPAddress() async {
    final request = await httpClient.getUrl(url);
    final response = await request.close();
    final responseBody = await response.transform(utf8.devoder).join();
    final String ip = jsonDecode(responseBody)['origin'];
    setState(() {
        _ipAddress = ip;
    });
}
  • Android

api-calls.gif

  • iOS

api-calls (1).gif

六、表单输入

文本字段允许用户在您的应用程序中键入文本,以便它们可用于构造表单、消息传递应用程序、消息传递应用程序、搜索体验等。Flutter提供了两个核心的文本字段小部件:TextFieldTextFormField.

6.1、如何使用文本字段小部件?

在React Native中,要输入文本,您可以使用TextInput组件来显示文本输入框,然后使用回调将值存储在变量中。

// React Native
<TextInput
    placeholder="Enter your Password"
    onChangeText={password => this.setState({ password })}
/>
<Button title="Submit" onPress={this.validata} />

在Flutter中,使用TextEditingController类来管理TextField小部件。每当修改文本字段时,控制器都会通知其监听器。

监听器读取文本和选择属性以及了解用户在字段中键入的内容过。您可以通过控制器TextField的属性访问文本.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

final TextEditingController _controller = TextEditingController();

class AA extends StatelessWidget {
   const AA({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          controller: _controller,
          decoration: const InputDecoration(
            hintText: 'Type something',
            labelText: 'Text Field'
          ),
        ),
        ElevatedButton(
            onPressed: () {
              showDialog(context: context, builder: (context) {
                return AlertDialog(
                  title: const Text('Alert'),
                  content: Text('You typed ${_controller.text}'),
                )
              });
            }, 
            child: const Text('Submit'),
        )
      ],
    );
  }
}

在此示例中,当用户点击提交按钮时,一个金高对话框会显示在文本字段中输入的当前文本。这是使用AlertDialog显示警报消息的小部件实现的,TextField中的文本由TextEditingController的文本属性访问。

6.2、如何使用表单小部件

在Flutter中,使用Form小部件,其中TextFormField小部件和提交按钮作为子项传递。该TextFormField小部件有一个名为onSaved的参数。它接受回调并在保存表单时执行。FormState对象用于保存、重置或验证作为此Form子项的每个FormField,您可以将Form.of()与其父类为Form的上下文一起使用,或者将GlobalKey传递给Form构造函数并调用GlobalKey.currentState()

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

final TextEditingController _controller = TextEditingController();

class AA extends StatelessWidget {
   const AA({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Form(
      key: formKey,
      child: Column(
        children: <Widget>[
          TextFormField(
            validator: (value) {
              if (value != null && value.contains('@')) {
                return null;
              }
              return 'Not a valid email';
            },
            onSaved: (val) {
              _email = val;
            },
            decoration: const InputDecoration(
              hintText: 'Enter your email',
              labelText: 'Eamil',
            ),
          ),
          ElevatedButton(
              onPressed: _submit, 
              child: const Text('Login'),
          )
        ],
      ),
    );
  }
}

以下示例显示如何使用Form.save()formKey(这是一个GlobalKey)在提交时保存表单。

void _submit() {
  final form = formKey.currentState;
  if (form != null && form.validate()) {
    form.save();
    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: const Text('Alert'),
          content: Text('Email: $_email, password: $_password')
        );
      }
    );
  }
}
  • Android

input-fields.gif

  • iOS

input-fields (1).gif