-
Notifications
You must be signed in to change notification settings - Fork 0
Provided AARs
Previously we created some plain Java classes, wrote some tests for them, and created JARs to provide them for later checkpoints. Now let's create some Android UI for our Machine Project.
We're going to make an activity for a complex multiplier-accumulator. The user will need four numeric text editors, two for each of two complex numbers to multiply. There will also need to be a button to perform the multiply-add and a label to display the current accumulated value. In the Android Studio project, fill in the activity_main
layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/aReal"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="numberSigned" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="+" />
<EditText
android:id="@+id/aImaginary"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="numberSigned" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="i *" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/bReal"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="numberSigned" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="+" />
<EditText
android:id="@+id/bImaginary"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="numberSigned" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="i" />
<Button
android:id="@+id/accumulate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Accumulate" />
</LinearLayout>
<TextView
android:id="@+id/accumulator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="0 + 0i" />
</LinearLayout>
And fill in MainActivity
with a student attempt:
package com.example.mp;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.EditText;
import android.widget.TextView;
import com.example.mp.logic.ComplexNumber;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
EditText aReal = findViewById(R.id.aReal);
EditText aImaginary = findViewById(R.id.aImaginary);
EditText bReal = findViewById(R.id.bReal);
EditText bImaginary = findViewById(R.id.bImaginary);
TextView result = findViewById(R.id.accumulator);
findViewById(R.id.accumulate).setOnClickListener(unused -> {
ComplexNumber a = new ComplexNumber(
Integer.parseInt(aReal.getText().toString()),
Integer.parseInt(aImaginary.getText().toString()
));
ComplexNumber b = new ComplexNumber(
Integer.parseInt(bReal.getText().toString()),
Integer.parseInt(bImaginary.getText().toString()
));
ComplexNumber product = a.times(b);
// Wrong! Doesn't actually accumulate
result.setText(product.getReal() + " + " + product.getImaginary() + "i");
});
}
}
Rather than adding the multiplication result to any running total, this code just replaces the last result with the new product. Let's write some Robolectric tests to exercise the activity and discover that problem. Expand Checkpoint0Test
:
package com.example.mp;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import com.example.mp.logic.ComplexNumber;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static org.junit.Assert.*;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 28)
public class Checkpoint0Test {
// the existing three tests elided for brevity
@Test
public void initialMultiply_isShown() {
MainActivity activity = Robolectric.buildActivity(MainActivity.class).create().start().resume().get();
((EditText) activity.findViewById(R.id.aReal)).setText("8");
((EditText) activity.findViewById(R.id.aImaginary)).setText("0");
((EditText) activity.findViewById(R.id.bReal)).setText("-3");
((EditText) activity.findViewById(R.id.bImaginary)).setText("0");
activity.findViewById(R.id.accumulate).performClick();
TextView results = activity.findViewById(R.id.accumulator);
assertEquals("-24 + 0i", results.getText().toString());
}
@Test
public void subsequentMultiplies_accumulate() {
MainActivity activity = Robolectric.buildActivity(MainActivity.class).create().start().resume().get();
for (int i = 0; i < 2; i++) {
((EditText) activity.findViewById(R.id.aReal)).setText("2");
((EditText) activity.findViewById(R.id.aImaginary)).setText("0");
((EditText) activity.findViewById(R.id.bReal)).setText("3");
((EditText) activity.findViewById(R.id.bImaginary)).setText("0");
activity.findViewById(R.id.accumulate).performClick();
}
TextView results = activity.findViewById(R.id.accumulator);
assertEquals("12 + 0i", results.getText().toString());
}
}
Run Checkpoint0Test
and see that the new initialMultiply_isShown
test passes (regardless of which components are provided—it's weak) but subsequentMultiplies_accumulate
fails. You can observe the incorrect behavior in the emulator too.
Let's make a provided component for this activity so the student can see how it's supposed to work. We're going to make an Android library, which requires Android Studio. Create a new Android Studio project, this time with No Activity. Let's name the project "Accumulator Activity" and set the package to com.example.mp.provided.accumulator
. This doesn't restrict us to creating classes only in that subpackage; it just sets where the library's R
class is autogenerated. It's important to use a different package than the actual app so that the R
classes don't conflict. Stick with Java and SDK 24.
The newly created project should open in a new window. Once it syncs, switch the view of the left pane to Project. Android Studio created the project for us as a full standalone app, but we actually want it to be a library. Open the app build.gradle
file and make some changes:
- Use the
com.android.library
plugin (for an Android library) instead ofcom.android.application
(for an app). - Remove the
applicationId
because libraries can't specify an app ID. - Remove the
testInstrumentationRunner
for tidiness. - Add a
compileOptions
block that enables Java 8 features. - Change all implementation dependencies to
compileOnly
so they won't end up in the compiled library. - Remove test dependencies because this project doesn't have its own tests.
apply plugin: 'com.android.library'
android {
compileSdkVersion 29
buildToolsVersion "29.0.3"
defaultConfig {
minSdkVersion 24
targetSdkVersion 29
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility = '8'
targetCompatibility = '8'
}
}
dependencies {
compileOnly fileTree(dir: 'libs', include: ['*.jar'])
compileOnly 'androidx.appcompat:appcompat:1.1.0'
compileOnly 'androidx.constraintlayout:constraintlayout:1.1.3'
}
If you need to use any other libraries in your component, you can add them as additional compileOnly
dependencies. Our activity relies on the ComplexNumber
class, so we need to copy-paste that provided arithmetic.jar
into the libs
folder under app
. It will be added to the project through that fileTree
dependency.
Now we can provide a working MainActivity
. Though the library's package is different, the fully-qualified class name must be the same. Delete the com.example.mp.provided.accumulator
package from the Java sources and recreate com.example.mp
. Inside, create a new Empty Activity named MainActivity
to match the one we're replacing. The student's layout resource is called activity_main
, but if we name ours that, the two versions will conflict and the potentially broken student version will usually win, leading to breakage in the provided activity. In general provided Android components should have their own copies of all resources except view IDs. I like to prefix provided resource names with a short identifier unique to the segment, so let's call this layout aa_activity_main
. Android Studio might "helpfully" update the activity name as well, so put that back to MainActivity
. Make sure the language is set to Java and create the activity.
The newly created activity class will have a compilation failure. It can't find its own R
because the class's package is different than the project's. That's alright—just import it and everything will be fine. Fill in the layout file with the same views as before. If you copy-paste the XML, you'll need to eliminate the tools:context
attribute because of the library package discrepancy. While we're here, let's make a visible but non-functional change so that we can see when the activity is replaced. I'll change the button's background
to an eye-searing #00FF00
. Fill in the Java activity class with a correct implementation:
package com.example.mp;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.EditText;
import android.widget.TextView;
import com.example.mp.logic.ComplexNumber;
import com.example.mp.provided.accumulator.R;
public class MainActivity extends AppCompatActivity {
private ComplexNumber total = new ComplexNumber(0, 0);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.aa_activity_main);
EditText aReal = findViewById(R.id.aReal);
EditText aImaginary = findViewById(R.id.aImaginary);
EditText bReal = findViewById(R.id.bReal);
EditText bImaginary = findViewById(R.id.bImaginary);
TextView result = findViewById(R.id.accumulator);
findViewById(R.id.accumulate).setOnClickListener(unused -> {
ComplexNumber a = new ComplexNumber(
Integer.parseInt(aReal.getText().toString()),
Integer.parseInt(aImaginary.getText().toString()
));
ComplexNumber b = new ComplexNumber(
Integer.parseInt(bReal.getText().toString()),
Integer.parseInt(bImaginary.getText().toString()
));
ComplexNumber product = a.times(b);
total = total.plus(product);
result.setText(total.getReal() + " + " + total.getImaginary() + "i");
});
}
}
There are a few other things we need to clean up. Since this is a library rather than an app, it doesn't need to declare application properties, though it still needs to declare its activity. Eliminate all the attributes from the <application>
tag:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.mp.provided.accumulator">
<application>
<activity android:name="com.example.mp.MainActivity" />
</application>
</manifest>
Even though MainActivity
is going to be the startup activity in the app, we don't need to declare an intent filter here. Because the app manifest, which has higher priority in the merging process, declares an activity with the same class name, it will win and keep its intent filter. That process is independent of Java compilation, so the original activity declaration can unwittingly refer to our provided version of MainActivity
.
Delete all non-layout resources, including the miscellaneous files inside the values
resources folder, though keep the values
folder itself around. Inside it, create a new values resource file called public.xml
. This allows us to specify which resources are private to the library (i.e. hidden from autocomplete when working on the app) and which are shared with the app. View IDs expected by test suites should be public, but if you have additional views not expected from student code, you can prefix their IDs to avoid collision and keep them private. For each shared/public ID, add a <public>
tag:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<public name="aReal" type="id" />
<public name="aImaginary" type="id" />
<public name="bReal" type="id" />
<public name="bImaginary" type="id" />
<public name="accumulate" type="id" />
<public name="accumulator" type="id" />
</resources>
Now we can compile the Android library. Open the Gradle pane on the right, expand the project, then "app", then Tasks to get to Android-specific tasks. In the "other" category, run assembleDebug
. (If you want to obfuscate but don't have a custom obfuscator, you can use assembleRelease
instead to run ProGuard.) This will generate a file called app-debug.aar
in app/build/outputs/aar
. Copy that file into the Machine Project's provided
folder, renaming it to accumulator.aar
along the way. Let's create an eMPire segment for this component:
register("accumulator") {
addAars("accumulator")
removeClasses("MainActivity")
}
Notice that we use addAars
to add Android libraries. That directive should still be paired with a removeClasses
to exclude the student version from compilation. Since the activity class is outside our logic subpackage, we just pass the class name. Make this new segment used by adding it to the segments
directives in checkpoints 1
and demo
. Try setting the student configuration YAML to Checkpoint 1 with provided components enabled. Run Checkpoint0Test
and see that all tests pass! You can also try the app in the emulator and see the incredibly green button from our provided layout.
With useProvided
still enabled, try looking through R.
autocomplete in the student version of MainActivity
. Even though the AAR is currently present as a dependency, its private aa_activity_main
layout doesn't appear in the R.layout.
suggestions. Its public IDs, on the other hand, do show up in the R.id.
suggestions even if you remove/change them in the student activity_main
layout. (If you're asking students to create views that will be looked up by your test suites, see Appendix B.)
In the next chapter we'll add an app feature that requires the student to adjust the manifest.