Flutter: Is it possible to substitute widget itself? - layout

I have the following situation:
The main widget with Row 1,2,3
In Row 2, I initially set the Widget A
What I want:
replace the Widget A with Widget B once I have a state change in Widget A
handle the replacement in Widget A and not in the main Widget (so I don't want to use callbacks from Widget A or observing a global state and react on it in the main Widget)
Replacement means: Widget B is still a child of the main Widget (in my case stays in the second row and doesn't go fullscreen as it is the case when you use routes/Navigator?)
My reasoning for what I want:
in each row of the main widget the user can interact with a sub menue which e.g. in row 2 consists of WidgetA -> [user interaction] -> WidgetB [user interaction] -> WidgetC
I don't want to manage all this different states from the main widget
What I tried:
//in Widget A -> in order to switch to Widget B
Navigator.push(context, new MaterialPageRoute(
builder: (_) => new WidgetB(),
));
This doesn't do the job because the Widget B doesn't stay in the WidgetTree of the main Widget
If this is not possible I would like to know what is the flutter way of achieving what I want :-)

You need to have some parent that knows of this subnavigation. You can't really replace a view by itself, its parent is what has the reference to that widget in the tree, so the parent is what needs to convey this to flutter.
That said, you can make a custom widget whose job is to listen for child events, and change the child accordingly.
App
/ \
Row Row
|
WidgetParent
|
WidgetA
Here WidgetParent could keep e.g. a Listenable and pass a ChangeNotifier to WidgetA. When WidgetA decides WidgetX should come to screen it can emit that to the parent.
class WidgetParent extends AnimatedWidget {
WidgetParent() : super(listenable: ValueNotifier<Widget>(null));
ValueNotifier<Widget> get notifier => listenable as ValueNotifier<Widget>;
Widget build(BuildContext context) {
return new Center(child: notifier.value ?? WidgetA(notifier));
}
}
Now child widgets can notify who's next.
class WidgetA extends StatelessWidget {
WidgetA(this.notifier);
final ValueNotifier<Widget> notifier;
#override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () => notifier.value = WidgetB(notifier),
child: Text("This is A, go to B"),
);
}
}
class WidgetB extends StatelessWidget {
WidgetB(this.notifier);
final ValueNotifier<Widget> notifier;
#override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () => notifier.value = WidgetA(notifier),
child: Text("This is B, go to A"),
);
}
}

I am fairly new to Flutter but one workaround I could think is - You use State with the widget. For example, if you want to display two rows depending on what use does then you set height of one row what you desire and set height of second row to zero. And swap it on demand.
Of course, you need to empty the contents as well. e.g. setting text to '' etc.
This may not be the ideal solution but could work.

Related

Is there a way to fix the "Function expressions can't be named" error in Flutter?

I watched a video tutorial for learning flutter and I encountered an error using the map method, can you help me fix it?
This is the link of the video I watched and followed along : https://www.youtube.com/watch?v=x0uinJvhNxI&t=18425s
This is the code :
import 'package:flutter/material.dart';
import './question.dart';
import './answer.dart';
class Quiz extends StatelessWidget {
final List<Map<String, Object>> questions;
final int questionIndex;
final Function answerQuestion;
Quiz(\{required this.questions, required this.answerQuestion,required this.questionIndex});
#override
Widget build(BuildContext context) {
return Column(
children: [
Question(
questions[questionIndex]['questionText'] as String,
/* answerQuestion(Named function) is a pointer that is passed as a value to onPressed but not the result of the function name ;*/ // onpressed attribute requires a function
),
...(questions[questionIndex]['answers'] as List<Map<String,Object>>).map(answer){ //Creating a list of widgets
return Answer(() => answerQuestion(answer['score']),answer['text']); // Return a widget
}.toList() // Based on the old List, it creates a new List
],
);
}
}

Flutter: final field must be a one of the following

We have a widget class that has a final property of this.type. Currently this is a String and if the String matches one of the available types it returns the correct content via that widget’s builder.
Is there a way that instead of asking for a String, we ask for one of the available potential options.
Eg. Only Block, Fixed or None are acceptable strings. Can we make sure that the use of this widget can only accept those terms.
You can use enums. First declare an enum using the enum keyword:
enum AcceptableOptions { Block, Fixed, None }
Then in your widget, use AcceptableOptions instead of String
class TestWidget extends StatelessWidget {
const TestWidget({
required AcceptableOptions option,
});
#override
Widget build(BuildContext context) {
return Container();
}
}
TestWidget(option:AcceptableOptions.Block)

Is there a way to parse multiple words in a string?

I'm working on a flutter search bar where the results from the buildSuggestions are used to populate the buildResults widget. The example from the flutter tutorial (boring flutter show) generates a string. I want to be able to select and print from the string both id and season. I tried substring but that won't always work since the colors have different length.
import 'package:flutter/material.dart';
final colors = [
{'id': 'red', 'season': 'winter',},
{'id': 'green', 'season': 'spring',}
];
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: ListView.builder(
itemBuilder: (context, index) => ListTile (
title: Card(
child: Text (colors[index].toString()))),
itemCount: colors.length))),
);}
}
OUTPUT
{id: blue, season: winter}
{id: green, season: spring}
Thanks.
Colors[index] will give out the content of the item at that index since that is a list. The data at the index however is a map, hence why you get the "{id: blue, season: winter}"
Try using this code for your Text widget as this extracts the data from the maps that are within your list.
Text("${colors[index]['id']} and ${colors[index]['season']}")

HowTo write a QML-Keyboard with return value

I'm using lot's of TextInputs within ListViews in my QML-Application. To modify the value I'm providing a virtual QML-Keyboard, that also contains a TextInput.
When I click on a TextInput in the ListView, the TextInput within the QML-Keyboard get's focus and the user can start editing. When finished the text should be sent to the TextInput within the ListView.
The problem I have is, that I don't know how to copy the text of the keyboard's TextInput to the ListView's-TextInput because the focus is lost when starting my virtual keyboard.
Each delegate item in a ListView is univocally identified by its index attached property. The first delegate in the list has index 0, then 1 and so on.
A ListView has a property currentIndex referring to the currently selected item delegate. When the currentIndex is set to a specific delegate also the currentItem property is set to the corresponding delegate object.
Given these properties, you can exploit them to obtain the desired behaviour. When you select a TextInput to edit it, you can set the currentIndex of the list to the index of the TextInput delegate. In this way also currentItem is set and it can later be used to refer the delegate (and the TextInput inside) when the editing in the virtual keyboard is finished.
Here is an example that better explains my point:
import QtQuick 2.3
import QtQuick.Window 2.0
import QtQuick.Layouts 1.1
Window {
width: 200
height: 300
visible: true
Component {
id: myDelegate
Rectangle {
width: myList.width; height: 20
color: myList.currentIndex === index ? "lightgreen" : "lightgray" // distinguish the selecte delegate
onFocusChanged: textInput.forceActiveFocus()
property alias textInput: textInput // alias the textInput // (1)
TextInput {
id: textInput
anchors.fill: parent
onCursorVisibleChanged: {
if(cursorVisible)
{
myList.currentIndex = index // (2)
keyboardInput.text = textInput.getText(0, textInput.text.length) // (3)
keyboardInput.forceActiveFocus()
}
}
}
}
}
ColumnLayout {
anchors.fill: parent
ListView {
id: myList
model: 5
delegate: myDelegate
spacing: 10
Layout.fillWidth: true
Layout.fillHeight: true
}
Rectangle {
Layout.alignment: Qt.AlignBottom
color: "steelblue"
Layout.preferredWidth: parent.width
Layout.preferredHeight: 40
TextInput {
anchors.centerIn: parent // simulate the keyboard
id: keyboardInput
width: parent.width
font.pixelSize: parent.height
onEditingFinished: {
myList.currentItem.textInput.text = getText(0, text.length) //(4)
}
}
}
}
}
This is a simplified example in which your virtual keyboard is substituted by a blue TextInput with id keyboardInput. Each time a TextInput in the list is focused, the focus is passed to keyboardInput for editing text.
First of all the TextInput in the delegate is aliased (1) to be accessible from outside the delegate. When a TextInput in the list get focused for editing (in my case I consider the CursorVisibleChanged event), the list currentIndex is set to the index of the delegate (2) and also the current text in the TextInput is copied inside keyboardInput (3). When the editing is finished, the text inside keyboardInput is copied back to the currently selected TextInput via currentitem (4).
This approach applies even with more than one ListView: simply store the current list in a variable at an higher scope and refer that variable in (4) to set the text in the correct delegate.

When are child views added to Layout/ViewGroup from XML

My question is :
I want to know when does a xLayout (or ViewGroup in general) add a child view from XML ? And by "when" I mean at what point of code, in what "pass" of the "traversal" of the UI toolkit ?
Which method of xLayout or ViewGroup should I override ?
I have done my homework : I have watched the "Writing Custom Views For Android" presented (by Adam Powell and Romain Guy) in the last Google I/O and I have read Adam Powell comments on this Google+ post.
Looking for the exact point in Android's source code where children are added.
We can look at what setContentView(R.layout.some_id) is doing under the hood.
setContentView(int) calls PhoneWindow#setContentView(int) - PhoneWindowLink is a concrete inplementation of Window:
#Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else {
mContentParent.removeAllViews();
}
mLayoutInflater.inflate(layoutResID, mContentParent);
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
The method LayoutInflater#inflate(layoutResID, mContentParent) eventually calls ViewGroup#addView(View, LayoutParams) on mContentParent. In between, child views
I want to know what happens exactly after I set content view to an XML file that contains a custom view. Afer the constructor there has to be a part in the code where the custom view "parse/read/inflate/convert" XML-declared child views to actual views ! (comment by JohnTube)
Ambiquity: From JohnTube's comment, it seems he is more interested in knowing how a custom view is inflated. To know this, we will have to look at the workings of LayoutInflaterLink.
So, the answer to Which method of xLayout or ViewGroup should I override ? is ViewGroup#addView(View, LayoutParams). Note that, at this point, the inflation of all regular/custom Views has already taken place.
Inflation of custom views:
The following method in LayoutInflater is where the addView(View, LayoutParams) is called on the parent/root:
Note: The call mLayoutInflater.inflate(layoutResID, mContentParent); in PhoneWindow#setContentView(int) chains to this. Here mContentParent is the DecorView: the view that's accessible through getWindow().getDecorView().
// Inflate a new view hierarchy from the specified XML node.
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)
// Recursive method used to descend down the xml hierarchy and instantiate views,
// instantiate their children, and then call onFinishInflate().
void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException
The call of interest in this method(and in the recursive rInflate(XmlPullParser, View, AttributeSet, boolean)) is:
temp = createViewFromTag(root, name, attrs);
Let's see what createViewFromTag(...) is doing:
View createViewFromTag(View parent, String name, AttributeSet attrs) {
....
....
if (view == null) {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
}
....
}
The period(.) decides whether onCreateView(...) or createView(...) is called.
Why this check? Because a View defined in android.view, android.widget or android.webkit package is accessed through its class name. For example:
android.widget: Button, TextView etc.
android.view: ViewStub. SurfaceView, TextureView etc.
android.webkit: WebView
When these views are encountered, onCreateView(parent, name, attrs) is called. This method actually chains to createView(...):
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
This would deal with SurfaceView, TextureView and other views defined in android.view package. If you are interested in knowing how TextView, Button etc. are dealt with, look at PhoneLayoutInflaterLink - it extends LayoutInflater and overrides onCreateView(...) to check if android.widget and android.webkit are the intended package names. In fact, the call getLayoutInflater() gets you an instance of PhoneLayoutInflater. This is why if you were to subclass LayoutInflater, you couldn't even inflate the simplest of layouts - because LayoutInflater can only deal with views from android.view package.
Anyway, I digress. This extra bit happens for regular Views - which don't have a period(.) in their definition. Custom views do have a period in their names - com.my.package.CustomView. This is how the LayoutInflater distinguishes between the two.
So, in case of a regular view(say, Button), a prefix such as android.widget will be passed as the second argument - for custom views, this will be null. The prefix is then used along with the name to obtain the constructor for that particular view's class. Custom views don't need this because their name is already fully qualified. I guess this has been done for convenience. Else, you would have been defining your layouts in this way:
<android.widget.LinearLayout
...
... />
(Its legal though...)
Also, this is why views coming from a support library (eg. <android.support.v4.widget.DrawerLayout.../>) have to use fully qualified names.
By the way, if you did want to write your layouts as:
<MyCustomView ../>
all you have to do is to extend LayoutInflater and add your package name com.my.package. to the list of strings that are checked during inflation. Check PhoneLayoutInflater for help with this.
Let's see what happens in the final stage for both custom and regular views - createView(...):
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
// Try looking for the constructor in cache
Constructor<? extends View> constructor = sConstructorMap.get(name);
Class<? extends View> clazz = null;
try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
....
// Get constructor
constructor = clazz.getConstructor(mConstructorSignature);
sConstructorMap.put(name, constructor);
} else {
....
}
Object[] args = mConstructorArgs;
args[1] = attrs;
// Obtain an instance
final View view = constructor.newInstance(args);
....
// We finally have a view!
return view;
}
// A bunch of catch blocks:
- if the only constructor defined is `CustomView(Context)` - NoSuchMethodException
- if `com.my.package.CustomView` doesn't extend View - ClassCastException
- if `com.my.package.CustomView` is not found - ClassNotFoundException
// All these catch blocks throw the often seen `InflateException`.
}
... a View is born.
If you're talking about a ViewGroup defined in XML, it's children are added when the view is inflated. This can be when you inflate explicitly with a LayoutInflater or when you set the content view of an activity. (There are probably a few other times as well, particularly if you are using stub views.)
If you want to add the children yourself to a ViewGroup that is not inflated, you can do that in the view's constructor.
EDIT: If you want to see how the children are added when a view is inflated, this occurs in the call to LayoutInflater.inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot). The source for android.view.LayoutInflater is included in the Android SDK distributions; on-line versions can be found in many places (here at GrepCode, for instance). This method ends up being called when, for instance, you call setContentView(int) for an Activity or when you explicitly inflate a layout resource.
The children are actually added in the call to rInflate(parser, root, attrs, false); ("recursive inflate"), which might be called from a couple of different places in the inflate() method, depending on what the inflater found as the root tag. You can trace through the code logic yourself. An interesting point is that a child is not added to its parent until its own children have been recursively inflated and added to it.
The other interesting method, used by both inflate and rInflate, is createViewFromTag. This might rely on an installable LayoutInflater.Factory (or .Factory2 object) to create the view, or may end up calling createView. There you can see how the call to the view's two-argument constructor ((Context context, AttributeSet attrs)) is made.

Resources