Flutter is a reactive framework, and for a developer specializing in native development, his philosophy may be unusual. Therefore, we begin with a small review.
The user interface on Flutter, as in most modern frameworks, consists of a tree of components (widgets). When any component changes, this and all its child components are re-rendered (with internal optimizations, which are described below). When the display changes globally (for example, rotates the screen), the entire tree of widgets is redrawn.
This approach may seem inefficient, but in fact it gives the programmer control over the speed of work. If you update the interface at the topmost level unnecessarily, everything will work slowly, but with the correct layout of the widgets, applications on Flutter can be very fast.
Flutter has two types of widgets - Stateless and Stateful. The first (analogue of Pure Components in React) do not have a state and are fully described by their parameters. If the display conditions do not change (say, the size of the area in which the widget should be shown) and its parameters, the system re-uses the previously created visual representation of the widget, so using Stateless widgets has a good effect on performance. At the same time, every time the widget is redrawn, a new object is formally created and the constructor is launched.
Stateful widgets retain some state between renders. To do this, they are described in two classes. The first of the classes, the widget itself, describes the objects that are created with each drawing. The second class describes the state of the widget and its objects are transferred to the created widget objects. Changing the stateful state of a widget is the main source of interface redrawing. To do this, change its properties inside the call to the SetState method. Thus, unlike many other frameworks, Flutter does not have implicit state tracking — any change in widget properties outside the SetState method does not redraw the interface.
Now, after describing the basics, you can start with a simple application that uses Stateless and Stateful widgets:
import 'dart:math'; import 'package:flutter/material.dart'; void main() => runApp(new MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Flutter Demo', theme: new ThemeData( primarySwatch: Colors.blue, ), home: Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new MyHomePage(), ), ); } } class MyHomePage extends StatefulWidget { @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { Random rand = Random(); @override Widget build(BuildContext context) { return new ListView.builder(itemBuilder: (BuildContext context, int index) { return Text('Random number ${rand.nextInt(100)}',); }); } }
Go ahead. The stateful state of widgets is retained between interface redrawing, but only as long as the widget is needed, i.e. really is on the screen. Let's make a simple experiment - we will place our list on the tab:
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { Random rand = Random(); TabController _tabController; final List<Tab> myTabs = <Tab>[ new Tab(text: 'FIRST'), new Tab(text: 'SECOND'), ]; @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new TabBarView( controller: _tabController, children: [ new ListView.builder(itemBuilder: (BuildContext context, int index) { return Text('Random number ${rand.nextInt(100)}',); }), Text('Second tab'), ],), bottomNavigationBar: new TabBar( controller: _tabController, tabs: myTabs, labelColor: Colors.blue, ), ); } }
At startup, you can see that when switching between tabs, the state is deleted (the dispose () method is called), and when it is returned, it is created again (the initState () method). This is reasonable since storing the state of non-displayable widgets will take away system resources. In the case when the state of the widget should experience its complete hiding, several approaches are possible:
First, you can use separate objects (ViewModel) to store the state. Dart-level language supports factory constructors that can be used to create factories and singleltons that store the necessary data.
I prefer this approach, because it allows you to isolate business logic from the user interface. This is especially true due to the fact that Flutter Release Preview 2 has added the ability to create pixel-perfect interfaces for iOS, but this should be done, of course, on the appropriate widgets.
Secondly, you can use the state-raising approach, familiar to programmers of React, when data is stored in components located higher in the tree. Since Flutter redraws the interface only when calling the setState () method, this data can be changed and used without rendering. This approach is somewhat more complicated and increases the connectivity of the widgets in the structure, but allows you to point the level of data storage.
Finally, there are state storage libraries, for example flutter_redux .
For simplicity, we use the first approach. Let's make a separate class ListData, singleton, storing values for our list. When mapping we will use this class.
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { TabController _tabController; final List<Tab> myTabs = <Tab>[ new Tab(text: 'FIRST'), new Tab(text: 'SECOND'), ]; @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new TabBarView( controller: _tabController, children: [ new ListView.builder(itemBuilder: ListData().build), Text('Second tab'), ],), bottomNavigationBar: new TabBar( controller: _tabController, tabs: myTabs, labelColor: Colors.blue, ), ); } } class ListData { static ListData _instance = ListData._internal(); ListData._internal(); factory ListData() { return _instance; } Random _rand = Random(); Map<int, int> _values = new Map(); Widget build (BuildContext context, int index) { if (!_values.containsKey(index)) { _values[index] = _rand.nextInt(100); } return Text('Random number ${_values[index]}',); } }
If you twist the list from the previous example down, then switch between tabs, it is easy to see that the scroll position is not saved. This is logical, since it is not stored in our ListData class, and the widget's own state does not experience switching between tabs. We implement the storage of the scrolling state manually, but for the sake of interest we add it not into a separate class or ListData, but to a higher level state to show how to work with it.
Check out the ScrollController and NotificationListener widgets (as well as the previously used DefaultTabController). The concept of widgets that do not have their own display should be familiar to developers working with React / Redux - in this bundle container components are actively used. In Flutter, non-mapped widgets are typically used to add functionality to child widgets. This allows you to leave the visual widgets themselves lightweight and do not handle system events where they are not needed.
The code is based on the solution proposed by Marcin Szałek at Stakoverflow ( https://stackoverflow.com/questions/45341721/flutter-listview-inside-on-a-tabbarview-loses-its-scroll-position ). The plan is:
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { double listViewOffset=0.0; TabController _tabController; final List<Tab> myTabs = <Tab>[ new Tab(text: 'FIRST'), new Tab(text: 'SECOND'), ]; @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new TabBarView( controller: _tabController, children: [new ListTab( getOffsetMethod: () => listViewOffset, setOffsetMethod: (offset) => this.listViewOffset = offset, ), Text('Second tab'), ],), bottomNavigationBar: new TabBar( controller: _tabController, tabs: myTabs, labelColor: Colors.blue, ), ); } } class ListTab extends StatefulWidget { ListTab({Key key, this.getOffsetMethod, this.setOffsetMethod}) : super(key: key); final GetOffsetMethod getOffsetMethod; final SetOffsetMethod setOffsetMethod; @override _ListTabState createState() => _ListTabState(); } class _ListTabState extends State<ListTab> { ScrollController scrollController; @override void initState() { super.initState(); //Init scrolling to preserve it scrollController = new ScrollController( initialScrollOffset: widget.getOffsetMethod() ); } @override Widget build(BuildContext context) { return NotificationListener( child: new ListView.builder( controller: scrollController, itemBuilder: ListData().build, ), onNotification: (notification) { if (notification is ScrollNotification) { widget.setOffsetMethod(notification.metrics.pixels); } }, ); } }
Saving information at the time of the application is good, but often you want to keep it between sessions, especially considering the habit of operating systems to close background applications when memory is low. The main options for permanent data storage in Flutter are:
For the demonstration, we will make the scrolling state preserved in Shared preferences. To do this, we add the restoration of the scroll position during the initialization of the _MyHomePageState state and save it when scrolling.
Here it is necessary to dwell on the asynchronous Flutter / Dart model, since all external services run on asynchronous calls. The principle of operation of this model is similar to node.js - there is one main thread of execution (thread), which is interrupted by asynchronous calls. At each subsequent interrupt (and the UI makes them constantly), the results of completed asynchronous operations are processed. At the same time, it is possible to run heavyweight calculations on background threads (via the compute function).
So, writing and reading in SharedPreferences are done asynchronously (although the library allows synchronous reading from the cache). First, let's deal with reading. The standard approach to asynchronous data acquisition looks like this - start an asynchronous process, when completed, execute SetState, writing the values obtained. As a result, the user interface will be updated using the received data. However, in this case we work not with the data, but with the scroll position. We do not need to update the interface, we just need to call the ScrollController jumpTo method. The problem is that the result of processing an asynchronous request can return at any time and it is not at all necessary to be what and where to scroll. To ensure that we perform the operation on a fully initialized interface, we need to ... still scroll inside the setState.
We get something like this:
@override void initState() { super.initState(); //Init scrolling to preserve it scrollController = new ScrollController( initialScrollOffset: widget.getOffsetMethod() ); _restoreState().then((double value) => scrollController.jumpTo(value)); } Future<double> _restoreState() async { SharedPreferences prefs = await SharedPreferences.getInstance(); return prefs.getDouble('listViewOffset'); } void setScroll(double value) { setState(() { scrollController.jumpTo(value); }); }
The recording is more interesting. The fact is that in the process of scrolling, the events reporting about this come constantly. Starting an asynchronous write each time the value changes may cause application errors. We need to handle only the last event from the chain. In terms of reactive programming, this is called debounce and we will use it. Dart supports the basic capabilities of reactive programming through data streams (stream), respectively, we will need to create a stream from the updates of the scroll position and subscribe to it, transforming it with Debounce. For the conversion, we need the stream_transform library . As an alternative approach, you can use RxDart and work in terms of ReactiveX.
Such code turns out:
StreamSubscription _stream; StreamController<double> _controller = new StreamController<double>.broadcast(); @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); _stream = _controller.stream.transform(debounce(new Duration(milliseconds: 500))).listen(_saveState); } void _saveState(double value) async { SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.setDouble('listViewOffset', value); }
Source: https://habr.com/ru/post/424765/
All Articles