-
Notifications
You must be signed in to change notification settings - Fork 4
Tutorial
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).
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.
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.
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" />
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
Michael Chavinda 2017