-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Data API 2: Binding Package
The objective of this page is to define a Data Binding API that could be used by all widgets that need to update data and also be updated whenever the data is changed;
- Button
- Check
- Entry
- Hyperlink
- Icon
- Label
- List*
- ProgressBar
- Radio
- Select
- Slider
- Table*
- Tree*
*The List, Table, and Tree widgets have not been designed / implemented yet, however an initial proposal for list is available (List Proposal).
A UI is responsible for displaying information and providing ways to interact with the underlying data. However this data could be changed at any time by any system including the UI, in fact the data may still be loading from the file system or network.
As the data changes, the UI should be updated. Similarly, when the UI changes the data, other systems that use the data should be updated. This behaviour is called a two-way binding.
Implementing a two-way binding correctly requires the developer to write a lot of boilerplate which requires more effort to test and maintain. The goal of this project is to implement a simple and intuitive API for two-way data binding.
In a previous iteration (Data API) the reflection package was used to establish a binding between a widget and a piece of data, however this had some shortcoming that this new API is intended to resolve;
- Implicity < Explicity - reflection was used to establish a binding by setting a function field in a widget and would perform type conversion behind the scenes. This opacity made it difficult for developers to understand, and harder to test and debug.
- Single Binding Only - since reflection would lookup the function field by name, it was only possible to have one binding per widget.
- Performance and Deadlock Susceptibility - when a binding was updated it would trigger all listeners on the same thread, and a circular dependency could cause locking issues and race conditions.
This two-way data binding API encompasses the data in an wrapper that provides access to the data and will notify registered listeners whenever the data is changed.
The API is designed to work with many types of data, and many types of data structures without requiring the developer to use many type casts. Since GoLang doesn't support generics, these type-specific bindings are generated programmatically.
The developer creates a binding that wraps a piece of data and then binds that to a widget. The widget will register itself as a listener of the binding and will get notified when the binding changed. Other systems can also register as listeners to respond appropriately to changes.
Notifiable is a simple interface that represents an object that can be notified.
type Notifiable interface {
Notify(Binding)
}
NotifyFunction is a type that implements Notifiable by called the encompassed function field, which makes it possible for a function closure to listen to a binding.
Binding is the base interface of the Data Binding API and defines the functions to add and remove the listeners notified when the bound data changes.
type Binding interface {
AddListener(Notifiable)
DeleteListener(Notifiable)
}
Base implements the basic behaviour of a binding, including managing and notifying listeners.
type Base struct {...}
func (b *Base) AddListenerFunction(listener func(Binding)) *NotifyFunction
func (b *Base) AddListener(listener Notifiable)
func (b *Base) DeleteListener(listener Notifiable)
Base is extended by Typed Bindings to provide type-specific constructors, getters, setters, and listeners for a single data item.
type String struct {...}
func NewString(value string) *String
func (b *String) Get() string
func (b *String) Set(string)
func (b *String) AddStringListener(func(string)) *NotifyFunction
- Bool
- Float64
- Int
- Int64
- fyne.Resource
- Rune
- String
- *url.URL
- Byte
- Float32
- Int8, Int16, Int32
- Uint, Uint8, Uint16, Uint32, Uint64
List encompasses a list of bindings and provides methods to get the Length of the list, Append to the list, and get/set individual elements of the list. List will notify its listeners whenever the list is append to, or an index is set. List will not notify its listeners when an individual element is updated, instead the UI is expected to bind to List as well as the individual element bindings within it.
type List struct {...}
func (b *List) Length() int
func (b *List) Get(int) Binding
func (b *List) Append(Binding)
func (b *List) Set(int, Binding)
Map is similar to List but encompasses a map of string to binding and provides methods to get the Length of the map, and get/set individual elements of the map. Map will notify its listeners whenever a key/value is set. Map will not notify its listeners when an individual element is updated, instead the UI is expected to bind to Map as well as the individual element bindings within it.
type Map struct {...}
func (b *Map) Length() int
func (b *Map) Get(string) Binding
func (b *Map) Set(string, Binding)
The Data Binding API extends the current Widget API to provide Bind methods for individual parts of the Widget. For example, Radio has BindOptions(List) and BindSelected(String), ProgressBar has BindMin(Float64), BindMax(Float64), BindStep(Float64), and BindValue(Float64).
This example demonstrates how to bind an Entry to a Label so that the Label displays the number of characters in the Entry.
text := binding.NewString("")
entry := widget.NewEntry().BindText(text)
label := widget.NewLabel("0")
text.AddStringListener(func(s string) {
label.SetText(strconv.Itoa(len(s)))
})