Skip to content

Data API

Andy Williams edited this page Apr 2, 2024 · 13 revisions

Note

This document is archived as it refers to a design process. The live documentation is available at https://docs.fyne.io/explore/binding

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 functions that can format or parse one type from another.

func (Int) String() String // convert int to string binding
func (Int) Format(string format) String // format into to a string binding

func (String) ToInt() Int
func (String) ParseInt(string format) Int

The returned types of DataItem will two-way bindings for the new type, so using ToInt on &String{"5"} would create a binding that can render ints. When the value of the String changes it will be reflected in the derived Int, but also changing the Int value will reflect the change in a string representation.

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
}