Custom Views and Libraries

In this example, we will be reimplementing the switchview as a separate library. The following code can be found at

mkdir -p $GOPATH/src/

A typical Matcha library will have the following directory structure.

* <library-name>
    * android
        * <android-studio-project>
    * ios
        * <xcode-project>
    * proto
        * <protobuf-files>
    * <go-files>...


Our switch will has two properties, the on/off value and enabled/disabled state. And when the user activates the switch, we need to somehow communicate the new on/off value back to Go. Matcha handles this by serializing views and events into protobufs. So lets create a file at proto/customview.proto with the following contents. Further details about the protobuf spec can be found here.

syntax = "proto3";
package matcha.examples.customview;

option go_package = "proto";
option objc_class_prefix = "CustomViewProto";
option java_package = "io.gomatcha.customview.proto";
option java_outer_classname = "CustomViewProto";

message View {
    bool value = 1;
    bool enabled = 2;

message Event {
    bool value = 1;

Protobuf files need to be compiled into Go, Java and ObjC in order to be used. So create a proto/gen.go file containing the below.

package proto

//go:generate bash -c "( cd $GOPATH/src && protoc --go_out=.*.proto )"
//go:generate bash -c "( cd $GOPATH/src && protoc*.proto )"
//go:generate bash -c "( cd $GOPATH/src && protoc*.proto )"

Now we can call go generate ./... to generate the supporting protobuf code.


Create a go file at customview.go.

package customview

import (

    protoview ""

type CustomView struct {
    Enabled  bool
    Value    bool
    OnSubmit func(value bool)

// NewCustomView returns an initialized CustomView instance.
func NewCustomView() *CustomView {
    return &CustomView{
        Enabled: true,

// Build implements view.View.
func (v *CustomView) Build(ctx view.Context) view.Model {
    l := &constraint.Layouter{}
    l.Solve(func(s *constraint.Solver) {
        if runtime.GOOS == "android" {
        } else {
    return view.Model{
        Layouter:       l,
        NativeViewName: "",
        NativeViewState: &protoview.View{
            Value:   v.Value,
            Enabled: v.Enabled,
        NativeFuncs: map[string]interface{}{
            "OnChange": func(data []byte) {
                event := &protoview.Event{}
                err := proto.Unmarshal(data, event)
                if err != nil {
                    fmt.Println("error", err)

                v.Value = event.Value
                if v.OnSubmit != nil {


Writing a library for Matcha is similar to building an app. We start by creating a new Xcode project, containing a Cocoa Touch Framework.


Drag the project into your app’s Workspace. Again, we need to make some changes to the Xcode project settings.

Add the Protobuf folder into your project and disable ARC by adding the -fno-objc-arc to any protobuf files in Build Phases > Compile Sources.

Custom views on iOS must conform to one of two protocols, MatchaChildView or MatchaChildViewController depending on whether you are wrapping a UIView or UIViewController. For this example we will implement a Switch by subclassing UIView.

Replace CustomView.h with…

#import <UIKit/UIKit.h>
#import <Matcha/Matcha.h>

@interface CustomView : UIView <MatchaChildView>
@property (nonatomic, strong) MatchaViewNode *viewNode;
@property (nonatomic, strong) MatchaBuildNode *node;
@property (nonatomic, strong) UISwitch *switchView;

And add file CustomView.m with…

#import "CustomView.h"
#import "Customview.pbobjc.h"

@implementation CustomView

- (id)initWithViewNode:(MatchaViewNode *)viewNode {
    if ((self = [super initWithFrame:CGRectZero])) {
        self.viewNode = viewNode;
        [self addTarget:self action: @selector(onChange:) forControlEvents: UIControlEventValueChanged];
    return self;

- (void)setNativeState:(NSData *)nativeState {
    CustomViewProtoView *view = [CustomViewProtoView parseFromData:nativeState error:nil];
    [self setOn:view.value animated:true];
    self.enabled = view.enabled;

- (void)onChange:(id)sender {
    CustomViewProtoEvent *event = [[CustomViewProtoEvent alloc] init];
    event.value = self.on;
    [self.viewNode call:@"OnChange", [[MatchaGoValue alloc]], nil];


We must register the view class with the Matcha framework, so also add the following into your implementation file.

+ (void)load {
    [MatchaViewController registerView:@"" block:^(MatchaViewNode *node){
        return [[CustomView alloc] initWithViewNode:node];


Create a new Android Studio project called CustomViewLib in the android directory. Remove the default app module and add an Android Library module named customview.


Again open your project’s settings.gradle and include the Matcha project.

include ':matcha'
project(':matcha').projectDir = new File("${System.env.GOPATH}/src/")

And open your module’s build.gradle and add $GOPATH/ to the repositories and the matcha project to the dependencies.

repositories {
    flatDir {
        dirs "${System.env.GOPATH}/src/"

dependencies {
    compile project(':matcha')

Create a new class called CustomView with the following.

package io.gomatcha.customview;

import android.content.Context;
import android.util.DisplayMetrics;
import android.widget.CompoundButton;


import io.gomatcha.bridge.GoValue;
import io.gomatcha.customview.proto.CustomViewProto;
import io.gomatcha.matcha.MatchaChildView;
import io.gomatcha.matcha.MatchaView;
import io.gomatcha.matcha.MatchaViewNode;

public class CustomView extends MatchaChildView {
    MatchaViewNode viewNode;
    SwitchCompat view;
    boolean checked;

    static {
        MatchaView.registerView("", new MatchaView.ViewFactory() {
            public MatchaChildView createView(Context context, MatchaViewNode node) {
                return new CustomView(context, node);

    public CustomView(Context context, MatchaViewNode node) {
        viewNode = node;

        float ratio = (float)context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT;
        view = new SwitchCompat(context);
        view.setPadding(0, 0, (int)(7*ratio), 0);
        view.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                if (isChecked != checked) {
                    checked = isChecked;
                    CustomViewProto.Event event = CustomViewProto.Event.newBuilder().setValue(isChecked).build();
          "OnChange", new GoValue(event.toByteArray()));

    public void setNativeState(byte[] nativeState) {
        try {
            CustomViewProto.View proto = CustomViewProto.View.parseFrom(nativeState);
            checked = proto.getValue();
        } catch (InvalidProtocolBufferException e) {