LatteKit

Additional

Language
Kotlin
Version
0.9.3 (Apr 11, 2016)
Created
Mar 28, 2016
Updated
Feb 14, 2017 (Retired)
Owner
Maan Najjar (maannajjar)
Contributor
Maan Najjar (maannajjar)
1
Activity
Badge
Generate
Download
Source code

What is LatteKit

It's a framework for building Android UI in Kotlin code by using the concept of virtual views and reactive data flow, the goal is to reduce boilerplate while retaining the same Android layout constructs.

Quick Example

package io.lattekit.helloworld
import android.app.Activity
import android.os.Bundle
import android.view.View
import android.widget.EditText
import io.lattekit.annotation.Bind
import io.lattekit.plugin.css.declaration.css
import io.lattekit.render
import io.lattekit.view.LatteView

class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        render("<io.lattekit.helloworld.MyApp />")
    }
}

open class MyApp : LatteView() {
    @Bind("@id/myText") var myText : EditText? = null;

    init {
        css("""
            .question { font-size: 20sp; font-weight: bold;  }
            .input { font-size: 14sp; margin-top:8dp;  width: match_parent; }
            .answer { font-size: 20sp; font-weight: bold; margin-top: 10dp; color: #00AADE; }
        """)
        // or: css("com.my.package/file.css") 
    }

    override fun layout() = xml("""
        <LinearLayout padding="30dp" orientation="vertical">
            <TextView text="What's your name?" class="question"/>
            <EditText id="@+id/myText" hint="Type your name here"
                onTextChanged=${{ notifyStateChanged() }} class="input"/>
            <TextView text=${"Hello ${myText?.text}"}
                visibility=${if (myText?.text?.toString() == "") View.GONE else View.VISIBLE} class="answer"/>
        </LinearLayout>
    """)

}

The above layout code is not "string", it's layout representation. The actual XML code will be parsed and compiled by the LatteKit gradle plugin. The use of string interpolation is to make it easier to reference scope variables in the layout. You can use any kotlin expression to set a property value, including lambdas.

Other Samples

For more samples, view the samples at lattekit-samples folder. But before viewing the sample, please read the core concept below to understand the sample better.

How It Works

1- Virtual Views (LatteViews)

The core concept of LatteKit is to define virtual views. A virtual view is subclass of LatteView that defines its own layout. The layout may contain native Android views and other virtual views. It can also receive properties passed to it from the rendering view. For example

open class MyApp : LatteView() {
    var currentUser : User? = null;
    override fun layout() = xml("""
        <LinearLayout padding="30dp" orientation="vertical">
            <views.UserDetailsView user=${currentUser} />
        </LinearLayout>
    """)
}

open class UserDetailsView : LatteView() {
    @Prop var user : User? = null;
    // Or @Prop("customPropertyName")

    override fun layout() = xml("""
        <LinearLayout orientation="horizontal" paddingTop="10dp" paddingBottom="10dp">
            <ImageView src=${user?.avatarUrl} layout_width="50dp" layout_height="50dp" />
            <TextView text=${user?.username} layout_gravity="center_vertical" layout_height="wrap_content"/>
        </LinearLayout>
    """)
}

Here, MyApp & UserDetailsView are virtual views. MyApp contains UserDetailsView which expects property user. When the native view tree is built, UserDetailsView will be replaced by the actual layout. Properties are automatically assigned to variables with @Prop annotation. In the above example, MyApp passed currentUser as user propertiy in UserDetailsView.

2- Data Binding & Maintaining View States

As demonstrated in the quick example, you can use any variable you want in your layout code. Just use string interpolation to pass any value to properties. You can use lambdas and reference other views in the layout code. Any variables used in the layout is considered state variable. If some variable's value changes, you will need to notify the view to update its layout by calling notifyStateChanged(). When notified, the virtual view will update its layout and all property changes will flow through the virtual layout tree. In the previous example, if MyApp changes currentUser for any reason (for example due to API call), all it needs to do is call notifyStateChanged() after the change. This will update user property in UserDetailsView which will then update its layout. Calling notifyStateChanged() will always ensure that the layout tree reflects the correct state of the notified view.

Binding Views

If you need to have reference to views from your layout code, you can use @Bind annotation. This is similar to what you do when you call findViewById. By default, the annotation will look for a view with the same id as the variable name. But you can specify the id in the annotation too:

@Bind var saveButton : Button? = null;
// Or
// @Bind("@id/saveButton")  saveButton : Button? = null;

override fun layout() = xml("""
 <Button id="@+id/saveButton" />
""")

You can also refernece those views inside your layout, as demonstrated in the first example.

ListView/RecyclerView/ViewPager

Those views have special treatment inside the layout definition. LatteKit will create an adapter for you, all you have to do is provide the views it should render. You can basically think of it as a for loop that iterates through data set (it's not for loop though, it just implements an Adapter behind the scene). For example:

open class MyListView : LatteView() {
   var myData : List<Any> = listOf(...)
   
    override fun layout() = xml("""
        <ListView data=${myData} layout_width="match_parent" layout_height="match_parent">
   <views.AdItemView when=${{ it : Any -> it is AdData }} />        
   <views.FoodItemView when=${{ it : Any -> it is FoodData }} defaultView="true" />
        </ListView>
    """)
}
open class FoodItemView : LatteView() {
 @Prop("model") var foodDeatils: FoodData? = null;
   override fun layout() = xml("""<TextView text=${foodDeatils?.title} />""")
}
open class AdItemView : LatteView() {
 @Prop("model") var adDetails: AdData? = null;
   override fun layout() = xml(""" .... """)
}

Here, the ListView will render myData dataset. The dataset contains two different kinds of objects: AdData and FoodData. Each needs to have different view (FoodItemView or AdItemView). The adapter will determine which template to use by calling the lambda specified via when property. If none of the templates matche, it will fallback to the first template that has defaultView=true. It will then render the template and pass the property model which will contain the corrospending item in the myData.

The template will use model property passed by the adapter.

Lifecycle

If you need to initialize your component, or need to initialize Android views outside the layout. You can override onViewCreated. Also, if you need to need to perform any action before the view is removed from the layout tree, you can override onViewWillDetach().

open class MyListView : LatteView() {
 var myData : List<Any> = emptyList();
 ..
 ..
 override fun onViewCreated() {
        ApiManager.getFeed()
     .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe {  response ->
                myData = response
                notifyStateChanged()
            }
 }
 override fun onViewWillDetach() {
  backgroundMusic?.stop();
 }
 
    override fun layout() = xml("""
        <ListView data=${myData} layout_width="match_parent" layout_height="match_parent" dividerHeight="0">
   <views.AdItemView when=${{ it is AdData }} />        
   <views.FoodItemView when=${{ it is FoodData }} defaultView="true" />
        </ListView>
    """)
}

CSS Styling

There are two ways to use CSS styling. 1) Local CSS, which are CSS delcarations that are defined inside LatteView. Those declarations don't cascade down to other virtual views. or 2) Global css, which are defined in a separate css file. The css file should be just inside any java package. Here how to use them:

open class MyView : LatteView() {
    @Bind("@id/myText") var myText : EditText? = null;

    init {
      // Global CSS
     css("com.my.package/myfile.css")
     
     // Local CSS 
        css("""
            .myclass { padding: 20dp; }
    """)
    }

    override fun layout() = xml("""
        <LinearLayout class="myclass">
        </LinearLayout>
    """)

}

CSS styling is not 100% complete, most used properties are already implemented. I'll keep adding more support in later releases. I'll expand this section later to explain what special cases of CSS properties. For the meaning time, you can view more css examples at lattekit-samples

Getting Started

1- Add the gradle plugin class path to buildscript

buildscript {
    dependencies {
        classpath 'io.lattekit.tools:gradle-plugin:0.9.3'
    }
}

2- Apply the plugin

// The plugins must be in that order
apply plugin: 'com.android.application'
apply plugin: 'lattekit' 
apply plugin: 'kotlin-android'

3- Add runtime lib to dependency to build.gradle

    compile 'io.lattekit:lattekit-core:0.9.3'

4- Define your virtual views as explained above

it's important that LatteView subclasses are declared open class. Gradle plugin will throw an error if it sees layout code inside non-open (final) class.

5- To render LatteView within activity:

import io.lattekit.render 

class MyActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Note: props here are passed as map and not in xml
        render("<com.package.MyView />",props=mutableMapOf("title" to "MyApp"))
    }
}

6- If you just want the native View object, you can call buildView().

var virtualView = Latte.render("<com.package.MyView />",props=mutableMapOf("title" to "MyApp"));
var androidView = virtualView.buildView(myActivity,LayoutParams(..))
// You can later update props using:
virtualView.props.put("prop",value);
virtualView.notifyStateChanged();

7- To show LatteView as an activity form another LatteView

Latte.showActivity(this,"<com.package.UserProfile />", mutableMapOf("user" to user))

8- To show LatteView as a Dialog form another LatteView

Latte.showDialog(this,"<com.package.UserProfile />", mutableMapOf("user" to user))

License

The MIT License (MIT)

Copyright (c) 2016 Maan Najjar

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.