Skip to content

Data API

Andy Williams edited this page Aug 2, 2020 · 13 revisions

The objective of this page is to define a Data Binding API that could be used by all widgets that need to handle data like Select, Radio, Checkbox, List and Table.

The List and Table widgets have not been designed / implemented yet, but some discussion have been done with regard to existing widgets and are summarized in the next sections.

Requirements

Following recent conversations we have refined a little what the requirements for data binding might be. The main points agreed are as follows:

  • The main aim is to make it simpler to bind data sources to widget content. We will need to handle primitive types, complex data and large data sets, without a complex API.
  • The API should be consistent with our approach to a clean, clear API with lots of compile checking (including type safety)
  • Support the design of direct field access and simple widget creation for the base cases with the ability to bind in addition to the main functionality
  • When data changes occur we should execute the calculations in a reliable order and avoid race conditions
  • Data types should be convertible (i.e. a float being displayed in a label (string) - with the possibility of developer controlled formatting

Introducing the DataItem, DataMap and DataList interfaces

The DataItem interface defines functions to add and remove listeners. These listeners provide the opportunity to hook in to be informed of change events so that widgets can update accordingly. Type specific implementations of this interface will provide access to the actual data. The callback is of a defined type rather than an anonymous API so that it can be removed later without additional index parameters.

type DataItemListener interface {
	DataChanged(DataItem)
}

type DataItem interface {
	AddListener(DataItemListener)
	RemoveListener(DataItemListener)
}

The DataMap interface is like a DataItem except that it has many items each with a name. The change listener is called when an item (or multiple) within the map is changed.

type DataMapListener interface {
	DataChanged(DataMap)
}

type DataMap interface {
	AddListener(DataMapListener)
	RemoveListener(DataMapListener)
	Get(string) DataItem
}

The DataList interface defines an interface that returns multiple DataItems. You can consider it like []DataItem except that it can support lazy loading and advanced features like paging. The change listener is notified if the number if items in the source changes - an addition or deletion - but not if items within it change.

type DataListListener interface {
	DataChanged(DataList)
}

type DataList interface {
	AddListener(DataListListener)
	RemoveListener(DataListListener)
	Length() int
	Get(int) DataItem
}

Future extensions such as PagedDataList may be added to bundle more complex data behaviours within the toolkit.

Data Types

The bindings provide various implementations to handle standard types: bool, float64, int, string. These types all follow the same pattern, we use string to illustrate:

type String interface {
	DataItem
	Get() string
	Set(string)
}

func NewString() String {
	blank := ""
	return &stringBind{&blank}
}

func BindString(s *string) String {
	return &stringBind{val: s}
}

You will see that type bindings can be created using a new anonymous variable, or by binding to an existing variable using a pointer.

Converting types

As a strictly typed API the data you wish to bind may not match the expected type of the widget. For these situations there will be conversion helpers that can format or parse one type from another.

If we updated the DataItem (or a sub-interface like ValueItem) includes a new function func Get() interface{} then we could do some conversions that take any binding, for example:

func Format(string, DataItem) String // takes a format string (like fmt.Sprintf) and a data binding to return a string binding
func ParseFloat(String) Float // takes a string binding and parses the value to create a float binding, in error uses zero value
func Trunc(Float) Int // takes a float binding and converts it to an int binding

If we decide to stick with the sapecific type binding we will need to generate many variations of each (which could make a very complex API). Although the Parse and Trunc examples don't become multiples, the format would - as follows:

func FormatIntToString(string, Int) String // convert int to string bindng
func IntToString(Int) String // because the type is known we could remove the format requirement
func FormatFloatToString(string, Float) String
func FloatToString(Float) String

Having said that this is a more complex API, it does mean that we could more easily make these two-way bindings, such that IntToString which normally would render ints as a string, using "%d" format, would be able to convert that string back to an int if it is changed.

Impact on existing widgets

The introduction of the DataItem and DataList interfaces will break the current API for the Radio and Checkbox widgets. The break is related to the type changes for the Selected, Options and OnChanged fields.

Example implementation for the Select widget

package widget

import "fmt"

type Select struct {
	baseWidget
	Selected binding.String
	Options  binding.StringList

	OnChanged func(String)
	hovered   bool
	popover   *PopOver
}

func NewSelect(options binding.StringList, changed func(binding.DataItem)) *Select {
	combo := &Select{baseWidget{}, nil, options, changed, false, nil}
	options.AddListener(func(binding.StringList) {
		combo.Refresh()
	}())

	Renderer(combo).Layout(combo.MinSize())
	return combo
}

Welcome to the Fyne wiki.

Project documentation

Getting involved

Platform details

Clone this wiki locally