Skip to content

Tutorial

Michael Chavinda edited this page Apr 25, 2018 · 5 revisions

Writing applications in froid

Hello Activities

We will begin by creating and running a simple two-activity application. The application will be the same example application given in the Google developers tutorial. I suggest reading both the Google tutorial and this tutorial. Doing so will give you a sense of the differences between froid and android proper. A fluent knowledge of these differences will help you port over existing code bases and small code sections easily from plain Java to Frege. Unlike the Google tutorial, this tutorial supposes some basic knowledge of the Android ecosystem (how to create activities/fragments, work with intents, and Java programming).

Setting up the application

Create a new project in Android studio (I'm using Android Studio 3.1) by selecting File -> New -> New Project. A dialog box with 3 text fields should pop up. Change the application name to Hello Activities and the company domain to example.com. You package name should automatically change to com.example.helloactivities.

Click on the next button.

The next dialog asks what kinds of devices you would like your application to support. Phone and tablet are selected by default and we are content with just those devices so go ahead and click next.

The next dialog asks what we want the application to start off with. We'll start off with a basic activity. After you've selected that, click next.

Now you are asked to type in what you basic Activity will be called. The suggested name is MainActivity and since programmers like "main" so much we'll go with that. We'll change the title though - MainActivity isn't a good name for a page title. Rename the title to "My First App". After changing the title click Finish. We won't worry about the "Use Fragment" checkbox for now.

Android Studio will build the app and then take you to the layout tool. Don't mind that for now. As a sanity check, run the application on a device or emulator.

The plumbing for froid

Add the line apply from: 'https://raw.githubusercontent.com/mchav/froid/master/froid.gradle' to your app's build.gradle file.

Resync your gradle and if everything is in tact then you've successfully set up froid in Android Studio. Since froid is setup in the gradle file, you'll need to resync gradle to compile changes in froid. Annoyingly, this recompiles the whole application. This is because we aren't producing class files so the Frege compiler will rebuild all the files and relink them.

NB: there might be some smarter ways to do some of these things, indeed it might be easier to make a plugin but I have limited experience with Android Studio's plugin ecosystem.

Changing the UI

At this point you have a simple application built for you that says "Hello World" in the middle of the screen. Now we would like to make our simple view. Since this part is language agnostic I'll defer to the Google tutorial on how to do it. The impatient can just copy the following to their content_main.xml file, replacing the already existing Textbox.

<EditText
        android:id="@+id/editText"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="16dp"
        android:layout_marginRight="0dp"
        android:layout_marginTop="16dp"
        android:hint="Text to send."
        android:inputType="textPersonName"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/button"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="16dp"
        android:layout_marginRight="16dp"
        android:text="Send"
        app:layout_constraintBaseline_toBaselineOf="@+id/editText"
        app:layout_constraintLeft_toRightOf="@+id/editText"
        app:layout_constraintRight_toRightOf="parent" />

THE CODE!

Now, go to app/src/main/frege and create a file called MainActivity.fr. This file will mirror the already registered MainActivity in the java source file and eventually replace it. To ensure that it replaces the current MainActivity we'll make it compile to the same package name as the current implementation.

The first line of your file should therefore be:

module com.example.helloactivities.MainActivity where

For now we'd just like it to setup the view like the current main activity does so we give it a similar implementation - notice the differences.

MainActivity.java

package com.example.helloactivities;

import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
            }
        });
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }
}

MainActivity.fr

module com.example.helloactivities.MainActivity where

import froid.os.Bundle
import froid.support.design.widget.FloatingActionButton
import froid.support.design.widget.Snackbar
import froid.support.v7.app.AppCompatActivity
import froid.support.v7.widget.Toolbar
import froid.view.View
import froid.view.Menu
import froid.view.MenuItem

native module type AppCompatActivity where {}

pure native rActivityMain "R.layout.activity_main" :: Int
pure native rToolbar "R.id.toolbar" :: Int
pure native rFab "R.id.fab" :: Int
pure native rActionSettings "R.id.action_settings" :: Int
pure native rMenuMain "R.menu.menu_main" :: Int

onCreate :: AppCompatActivity -> Maybe Bundle -> IO ()
onCreate this savedInstanceState = do
        this.setContentView rActivityMain
        this.setOnCreateOptionsMenu (onCreateOptionsMenu this)
        this.setOnOptionsItemSelected (onOptionsItemSelected this)
        toolbar <- Toolbar.fromView this.findViewById rToolbar
        this.setSupportActionBar toolbar
        fab <- FloatingActionButton.fromView this.findViewById rFab
        fab.onClick (\view -> Snackbar.make view "Replace with your own action" Snackbar.lengthLong
            >>= _.setAction "Action" (\_ -> return ()) >>= _.show)

onCreateOptionsMenu :: AppCompatActivity -> Menu -> IO Bool
onCreateOptionsMenu this menu = do
    inflater <- this.getMenuInflater
    inflater.inflate rMenuMain menu
    return true

onOptionsItemSelected :: AppCompatActivity -> MenuItem -> IO Bool
onOptionsItemSelected this item = do
    {- Handle action bar item clicks here. The action bar will
       automatically handle clicks on the Home/Up button, so long
       as you specify a parent activity in AndroidManifest.xml. -}
    itemId <- item.getItemId
    return (itemId == rActionSettings)

Let's look at th differences between the two implementation with the aim of understanding the froid implemntation. The imports are pretty much the same except that android.** is written as froid.**.

We want this Activity to inherit from AppCompatActivty so we include the line:

native module type AppCompatActivity where {}

This is how Frege makes an entire file a subclass of AppCompatActivity. Froid provides an implementation of AppCompatActivity that looks for an onCreate method with type AppCompatActivity -> Maybe Bundle -> IO (). The function needn't have an explicit type declaration and could appear as onCreate this bundle. The two arguments must be assumed to be an AppCompatActivity and a Maybe Bundle. If there is an error in the function signature your app will crash. This is the only part of the Froid that isn't typesafe and will fail at runtime but it's a minor kink that allows us to change the program entry point.

onCreate is slightly different. It doesn't contain a call to the super class like the java version does. Froid handles that for you under the hood since this call is ALWAYS required. There are two extra lines in the function that are prefixed this.setOn*. Since onCreate is Android's de facto main, we use it set lambdas that will run when the other parts of the activity lifecycle are called. this.setOnCreateOptionsMenu is called on (onCreateOptionsMenu this) purely for consistency. It could have been called on anything else.

Finding views is slightly different. Each subclass X of View provides a fromView method which takes in a function from Int to View and an Int then returns a view. Casting is done under the hood. The function takes a lambda from a resource to a view so that it can work on any Context object's findViewById function. If you want to convert a normal view from an X you can use X.fromView (\_ -> view) 0. Regardless of what the int parameter is this function returns view.

You may also have noticed all the definitions at the top written as pure native .... these are all the resources used. They are prefixed with the letter r so it is explicit that they are resource integers. To define them you write pure native rResourceName "<application_id>.R.<resource_type>.<resource_id>" :: Int.

The rest of the code looks pretty similar except that the FloatingActionButton has a method onClick that takes in a lambda from View to IO (). This is the case for all classes that subclass view. Froid abandons listeners where it can in favour of lambdas.

Now that the boilerplate is out of the way let's start writing application code.

Let's begin by declaring resource ids for our two widgets in content_main.xml. As explained in the paragraph about resources they will be defined as:

pure native rEditText "R.id.editText" :: Int
pure native rButton "R.id.button" :: Int

We proceed by first importing the required widgets and then extracting them in onCreate.

...
import froid.widget.Button
import froid.widget.EditText
...
onCreate this savedInstanceState = do
    ...
    editText <- EditText.fromView this.findViewById rEditText
    button <- Button.fromView this.findViewById rButton
    ...

Now we would like to add an onClick lambda on the button. Let's make it do nothing for now.

button.onClick (\_ -> return ())

We'd like to add another activity. Again we will mirror an existing activity. So we begin by adding the Activity via Android Studio. Got to File -> New -> Activity -> Empty Activity. Let's call the activity DisplayActivity. In the dialog, ensure that backward compatibility is enabled. We won't be using this any feature that requies backward compatibility but it's good practice to always use the compat libraries.

Now, let's go to our Frege source and create a file called DisplayActivity.fr. Copy the following boilerplateinto the file:

module com.example.helloactivities.DisplayActivity where

import froid.support.v7.app.AppCompatActivity
import froid.os.Bundle

native module type AppCompatActivity where {}

pure native rActivityDisplay "R.layout.activity_display" :: Int

onCreate :: AppCompatActivity -> Maybe Bundle -> IO ()
onCreate this bundle = do
    this.setContentView rActivityDisplay

The format should look familiar at this point. This activity has all the elements of the first activity. Now let's go back to MainActivity and try and open this Activity by pressing the "Send" button. We'll anticipate the function's use and call it sendMessage. We open the activity using intents. Send message creates a new intent and then starts an ctivity based on that intent. An intent requires a context object and a class to start. For the context object we'll use AppCompatActivity and well define the class file similar to how we define resource. Classes will be prefixed with 'c' rather than 'r'. Let's import the Intent class and set up the code to start the activity.

...
import froid.content.Intent
...

pure native cDisplayActivity "com.example.helloactivities.DisplayActivity.class" :: Class a

...

sendMessage :: AppCompatActivity -> IO ()
sendMessage this = do
    intent <- Intent.new this cDisplayActivity
    this.startActivity intent

But now we need to pass the text in editText to the activity. We can pass editText into the function, take it's string value and put it in the intent. Thus, we make a slight change to sendMessage and include a tag for our intent as a top level function. Also, don't forget to change button.onClick to take in sendMessage as a lambda.

extraMessage :: String
extraMessage = "com.example.helloactivities.MESSAGE"

sendMessage :: AppCompatActivity -> EditText -> IO ()
sendMessage this editText = do
    intent <- Intent.new this cDisplayActivity
    message <- editText.getTextString
    intent.putExtra extraMessage message
    this.startActivity intent

Now we need to configure the other activity to geet the text from this intent and display it in a textbox. We want to be able to specify that DisplayActivity is a child of MainActivity. Doing so includes a back button on the app bar that returns to MainActivity if DisplayActivity is called from MainActivity. Include the following in your Android Mainfest file:

<activity android:name=".DisplayActivity"
          android:parentActivityName=".MainActivity" >
    <!-- The meta-data tag is required if you support API level 15 and lower -->
    <meta-data
        android:name="android.support.PARENT_ACTIVITY"
        android:value=".MainActivity" />
</activity>

DisplayActivity will need a textview to display the text given to it. Define the following textview in activity_display.xml.

    <TextView
        android:id="@+id/textView"
        android:text="No text given"
        android:textSize="36dp"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="16dp"
        android:layout_marginRight="0dp"
        android:layout_marginTop="16dp"/>

And the accompanying widget in the Activity as well as the code for getting the text from the intent:

import froid.widget.TextView

import com.example.helloactivities.MainActivity as MainActivity()
...
pure native rTextView "R.id.textView" :: Int
...

onCreate this savedInstanceState = do
    ...
    textView <- TextView.fromView this.findViewById rTextView
    intent <- this.getIntent
    message <- intent.getStringExtra MainActivity.extraMessage
    textView.setText message
    ...

Run the app and verify that pressing entering a message and pressing the send button indeed adds it to the textview. If it does, well done, you've made it to the end of the tutorial.If it doesn't make sure you've set everything up as written and be sure to read accompanying error messages.

MainActivity.fr

module com.example.helloactivities.MainActivity where

import froid.content.Intent
import froid.os.Bundle
import froid.support.design.widget.FloatingActionButton
import froid.support.design.widget.Snackbar
import froid.support.v7.app.AppCompatActivity
import froid.support.v7.widget.Toolbar
import froid.view.View
import froid.view.Menu
import froid.view.MenuItem
import froid.widget.Button
import froid.widget.EditText

native module type AppCompatActivity where {}

pure native rActivityMain "R.layout.activity_main" :: Int
pure native rToolbar "R.id.toolbar" :: Int
pure native rFab "R.id.fab" :: Int
pure native rActionSettings "R.id.action_settings" :: Int
pure native rMenuMain "R.menu.menu_main" :: Int
pure native rEditText "R.id.editText" :: Int
pure native rButton "R.id.button" :: Int
pure native cDisplayActivity "DisplayActivity.class" :: Class a


onCreate :: AppCompatActivity -> Maybe Bundle -> IO ()
onCreate this savedInstanceState = do
        this.setContentView rActivityMain
        this.setOnCreateOptionsMenu (onCreateOptionsMenu this)
        this.setOnOptionsItemSelected (onOptionsItemSelected this)
        editText <- EditText.fromView this.findViewById rEditText
        button <- Button.fromView this.findViewById rButton
        button.onClick (\_ -> sendMessage this editText)
        toolbar <- Toolbar.fromView this.findViewById rToolbar
        this.setSupportActionBar toolbar
        fab <- FloatingActionButton.fromView this.findViewById rFab
        fab.onClick (\view -> Snackbar.make view "Replace with your own action" Snackbar.lengthLong
            >>= _.setAction "Action" (\_ -> return ()) >>= _.show)

extraMessage :: String
extraMessage = "com.example.helloactivities.MESSAGE"

sendMessage :: AppCompatActivity -> EditText -> IO ()
sendMessage this editText = do
    intent <- Intent.new this cDisplayActivity
    message <- editText.getTextString
    intent.putExtra extraMessage message
    this.startActivity intent

onCreateOptionsMenu :: AppCompatActivity -> Menu -> IO Bool
onCreateOptionsMenu this menu = do
    inflater <- this.getMenuInflater
    inflater.inflate rMenuMain menu
    return true

onOptionsItemSelected :: AppCompatActivity -> MenuItem -> IO Bool
onOptionsItemSelected this item = do
    {- Handle action bar item clicks here. The action bar will
       automatically handle clicks on the Home/Up button, so long
       as you specify a parent activity in AndroidManifest.xml. -}
    itemId <- item.getItemId
    return (itemId == rActionSettings)

DisplayActivity.fr

module com.example.helloactivities.DisplayActivity where

import froid.support.v7.app.AppCompatActivity
import froid.os.Bundle
import froid.widget.TextView

import com.example.helloactivities.MainActivity as MainActivity()

native module type AppCompatActivity where {}

pure native rTextView "R.id.textView" :: Int
pure native rActivityDisplay "R.layout.activity_display" :: Int

onCreate :: AppCompatActivity -> Maybe Bundle -> IO ()
onCreate this bundle = do
    this.setContentView rActivityDisplay
    textView <- TextView.fromView this.findViewById rTextView
    intent <- this.getIntent
    message <- intent.getStringExtra MainActivity.extraMessage
    textView.setText message