Introduction

As you may have noticed, there are a lot of popular online form creation platforms offering a modular creation process and ease of use. This makes a lot of parties that should’ve benefited more from a more native approach decide to use those online platforms instead. And the easy process of developing a native app is getting more and more non-beneficial to even casual creators.

Native implementation of a form inside your Android app, for example, can give you a lot of advantages. Be it implementing as an extra on your existing apps (preventing users getting out of your ecosystem), control & customization, a more professional approach, etc. Only beaten hard in the ease-of-creation aspect.

This project intends to set up the structure to modularly create your forms inside an Android app. Even if you’re not trying to implement one in Android, the methods & main idea can be taken elsewhere. Basically, any expansion or modification to the forms will only take place in the layout (.xml), and no further main program adjustment necessary. You can copy-paste the layout components, adjust some things there, and it’ll be functional — modularity achieved.


Glossary

Some new terms I learned while doing this project:

  • HTTP Request – A packet of information that one computer sends to another computer to communicate; such as from you as a client, to a web-app as a server.
  • Web App – A computer program that utilizes web browsers and web technology to perform tasks over the Internet.

Requirements

  • Android Studio (used here is 4.0 Canary 2).
  • Phone or AVD (Android Virtual Device) for testing.
  • Google account.

No need to exactly match the version used here, those are just numbers to take in mind as there might be big differences between versions. The project file used in this tutorial is available in this GitHub repository.


Step-by-step

Without further ado, let’s start the steps to achieve this goal.

0 – Basics

0a – Starting The Project

Skip this step if you’re planning to implement this to an existing application project. This will guide you to some basics for creating your first Android Studio project. You may follow other step-by-step for this part, but make sure to enable “Use legacy android.support libraries” to be able to copy-paste XML (layout) files presented here. It allows you to use older XML syntax in the new Android Studio (you don’t need to do this in older versions).

Here are the parameters I use personally during the creation of a new project:

  • Template: Scrolling Activity
  • Name: Synced Form
  • Package Name: com.arsenicbismuth.syncedform
  • Language: Java
  • Minimum SDK: API 21 – Android 5.0 (Lollipop)
  • Use legacy android.support libraries

0b – The Files

The files you want to focus on are present on the project structure on the left window. Inside the java folder is the main program to be executed, if you’re following our initial project creation it’d be called ScrollingActivity (the extension is hidden, but it’s .java), or MainActivity if you use some of the other templates.

The layout for editing the components appear to the viewer/user is placed in the layout folder. Meanwhile, external resources for more organized value management are placed in values folder. And last but not least, the AndroidManifest up top for formal declaration such as permissions, package name, app name, etc.

Project structure – files to be edited are highlighted blue by us.

For a detailed explanation, please always refer to the complete documentation already presented by the Android community.


1 – Main Program

The entire program is placed on a single activity. In my case, that’d be ScrollingActivity.java . You may want to use intent if you planned to add this to an existing app. The main idea here is that the program would search for certain components in the layout (including its children), get their data, grouping them all, and last send it to the Google Sheet.

As a result, you won’t need to modify anything in the program (.java file) every time you add a new input box/dialog. Thus you can modify the layout as if it’s a modular form. The input components covered here are EditText (your common editable box for numbers or text), RadioButton (single-choice), CheckBox (multi-choice), and EditText using DatePicker (calendar).

First off, the libraries used. Thanks to Android Studio, those aren’t necessary to put beforehand, as it can automatically import during the writing of your code. But just in case you’re confused about which is being used, since there might be multiple versions.

import android.app.DatePickerDialog;
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.util.Log;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.DatePicker;
import android.widget.EditText;
import android.widget.RadioButton;

import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.StringRequest;
import com.android.volley.toolbox.Volley;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;

The first part of the main program would be the data compilation. The methods/subprograms are designed to get every input-able components in the layout. This process is already started during the initialization of the activity.

Below is the process during initialization. As you can see we’re trying to look for every child in our layout. To do this, any View components in the bottom of the tree (that is, no more children to that component) will be acquired. This way, any input-able components (such as EditText) encapsulated by layout grouper, organizer, or wrapper can still be obtained.

public class ScrollingActivity extends AppCompatActivity implements DatePickerDialog.OnDateSetListener {

    // Contains every component IDs inside content_scrolling.xml
    private static ArrayList<View> components = new ArrayList<View>();

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

        ViewGroup main = findViewById(R.id.lay_main);
        components = getChildren(main);
    }

    private ArrayList<View> getChildren(View v) {

        // Anything with child(s) is a ViewGroup, end recursion if not
        if (!(v instanceof ViewGroup)) {
            ArrayList<View> viewArrayList = new ArrayList<View>();
            viewArrayList.add(v);
            return viewArrayList;
        }

        ArrayList<View> result = new ArrayList<View>();

        // Loop inside current group, and compile results from every child
        ViewGroup vg = (ViewGroup) v;
        Log.i("ChildCount",String.valueOf(vg.getChildCount()));
        for (int i = 0; i < vg.getChildCount(); i++) {

            View child = vg.getChildAt(i);

            ArrayList<View> viewArrayList = new ArrayList<View>();
            viewArrayList.add(v);
            viewArrayList.addAll(getChildren(child));   // Recursion

            result.addAll(viewArrayList);
        }

        // Return to parent
        return result;
    }

The function will then return the list of those lowest components in the layout as a View and store it in the components variable.

Now to handle every radio button (single-choice ie. Male or Female) and checkbox (multi-choice), we add two methods below. The data stored here are a pair of view tag & its input into a Map (similar to a dictionary in Python).

Note that method onRadioClicked onCheckClicked must be called when the respective components are clicked. Thus, they must be added into every respective RadioButton & CheckBox component in the layout. This will be explained later.

    // Class variables for storing radioButton states
    private Map<String, String> allRadio = new HashMap<>();
    private Map<String, String> allCheck = new HashMap<>();

    // Assigned to every RadioButton onClick parameter.
    public void onRadioClicked(View view) {
        // Check if button currently checked
        boolean checked = ((RadioButton) view).isChecked();

        // Tag naming format: group_pick. Ex: sex_female
        String[] tag = view.getTag().toString().split("_");
        String group = tag[0];
        String pick = tag[1];

        // Put all data
        if(checked) {
            allRadio.put(group, pick);
        }
    }

    // Assigned to every check box onClick parameter.
    public void onCheckClicked(View view) {
        boolean checked = ((CheckBox) view).isChecked();

        // Applies for every check, each must contains tag
        if (checked) {
            allCheck.put(view.getTag().toString(), "v");
        } else {
            allCheck.put(view.getTag().toString(), "");
        }
    }

Lastly for the data compilation part is DatePicker. It’s similar to the method above, but here the input component in the layout is using EditText. Hence it’ll be acquired by the getChildren method before, and requires no special case.

Here, every time the special EditText is clicked, it’d open a DatePicker dialog (in the form of a calendar) instead of manually inputting the data. Note you can modify the formatting by changing the date variable assignment (here we set it to day/month/year).

Do note for some reason the month is started from 0 (January) in my case, that’s why I added it by one.

    EditText picked;
    // Assigned to every date EditText onClick parameter.
    public void showDateDialog(View view) {
        picked = (EditText) view;   // Store the dialog to be picked

        DatePickerDialog datePickerDialog = new DatePickerDialog(
                this, this,
                Calendar.getInstance().get(Calendar.YEAR),
                Calendar.getInstance().get(Calendar.MONTH),
                Calendar.getInstance().get(Calendar.DAY_OF_MONTH));
        datePickerDialog.show();
    }

    @Override
    public void onDateSet(DatePicker datePicker, int y, int m, int d) {
        // If done picking date
        String date = d + "/" + (m+1) + "/" + y;
        picked.setText(date);
    }

In the last part of the main program, we get all the data from EditText in getData method. After combining that data with the allRadio and allCheck which stores RadioButton and CheckBox information, we post the data into Google Sheet by using a simple HTTP POST command.

Here Google Script is used as an interface to your Google Sheet. Hence why the variable url MUST be edited to the one provided by your own Google Script project. It’ll be covered later.

    private Map<String, String> getData() {
        // Collect all input data
        Map<String, String> result = new HashMap<>();

        for (View comp : components) {
            // Get every EditText's tag & text.
            if (comp instanceof EditText) {
                result.put(comp.getTag().toString(), ((EditText) comp).getText().toString());
            }
        }
        return result;
    }

    // Assigned to the fab (floating action button) onClick parameter.
    public void postSheet(final View view) {
        // Instantiate the RequestQueue.
        RequestQueue queue = Volley.newRequestQueue(this);
        String url ="https://script.google.com/macros/s/MODIFY_YOURSELF";

        // Collect all data to send
        final Map<String, String> allData = getData();
        allData.putAll(allRadio);  // Combine with radio data
        allData.putAll(allCheck);  // Combine with check data

        // Request a string response from the provided URL.
        StringRequest stringRequest = new StringRequest(Request.Method.POST, url,
                new Response.Listener<String>() {
            @Override
            public void onResponse(String response) {
                Snackbar.make(view, "Response: " + response, Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Snackbar.make(view, "No response", Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
            }
        }) {
            @Override
            protected Map<String, String> getParams() {
//                Map<String, String> params = new HashMap<>();
                return allData;
            }
        };

        // Add the request to the RequestQueue.
        queue.add(stringRequest);
    }

For adding internet permission, we need to add this line to the AndroidManifest.xml, just between “package” & “<application” lines.

<uses-permission android:name="android.permission.INTERNET" />

That’s all for the program. We can now move on to the layout & the general procedure for adding a new component.


2 – Layout

Generally, it’s easier for you to just copy-paste the following XML code to the content_scrolling.xml file, you must enter code view beforehand by clicking one of the menus on the top right. This way, you can just modify & follow my custom procedure to add every component to be used as an input.

Layout Preview – Code, both, & design.

Here’s the code. We do always try to create everything as beautiful as possible. Hence visual aspect such as margin, padding, theme consistency, etc aren’t neglected.

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView 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"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context=".ScrollingActivity"
    tools:showIn="@layout/activity_scrolling">
    
    <LinearLayout
        android:id="@+id/lay_main"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <android.support.design.widget.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:paddingTop="@dimen/text_margin">

            <EditText
                android:id="@+id/edit_name"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="@dimen/text_margin"
                android:layout_weight="1"
                android:autofillHints=""
                android:ems="10"
                android:hint="@string/h_name"
                android:inputType="textPersonName"
                android:tag="name" />
        </android.support.design.widget.TextInputLayout>

        <android.support.design.widget.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1">

            <EditText
                android:id="@+id/edit_phone"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="@dimen/text_margin"
                android:autofillHints=""
                android:ems="10"
                android:hint="@string/h_phone"
                android:inputType="phone"
                android:tag="phone"
                android:textAlignment="viewStart" />
        </android.support.design.widget.TextInputLayout>

        <TextView
            android:id="@+id/textView3"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="@dimen/text_margin"
            android:layout_marginRight="@dimen/text_margin"
            android:layout_marginBottom="@dimen/option_margin"
            android:layout_weight="1"
            android:ems="10"
            android:paddingLeft="4sp"
            android:paddingRight="4sp"
            android:text="@string/t_sex"
            android:textAppearance="@style/TextAppearance.Design.Counter"
            android:textColor="?android:attr/textColorHint" />

        <RadioGroup
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_marginLeft="@dimen/text_margin"
            android:layout_marginRight="@dimen/text_margin"
            android:orientation="horizontal"
            android:paddingBottom="@dimen/text_margin">

            <RadioButton
                android:id="@+id/radio_male"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="@dimen/text_margin"
                android:layout_weight="1"
                android:onClick="onRadioClicked"
                android:tag="sex_male"
                android:text="@string/t_male" />

            <RadioButton
                android:id="@+id/radio_female"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:onClick="onRadioClicked"
                android:tag="sex_female"
                android:text="@string/t_female" />

        </RadioGroup>

        <android.support.design.widget.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1">

            <EditText
                android:id="@+id/edit_add"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="@dimen/text_margin"
                android:layout_weight="1"
                android:autofillHints=""
                android:ems="10"
                android:hint="@string/h_address"
                android:inputType="text"
                android:tag="address"
                android:textAlignment="viewStart" />
        </android.support.design.widget.TextInputLayout>

        <android.support.design.widget.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1">

            <EditText
                android:id="@+id/edit_date"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="@dimen/text_margin"
                android:layout_weight="1"
                android:cursorVisible="false"
                android:autofillHints=""
                android:ems="10"
                android:focusable="false"
                android:hint="@string/h_date1"
                android:inputType="date"
                android:onClick="showDateDialog"
                android:tag="start_date" />

        </android.support.design.widget.TextInputLayout>

        <android.support.design.widget.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1">

            <EditText
                android:id="@+id/edit_date2"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="@dimen/text_margin"
                android:layout_weight="1"
                android:cursorVisible="false"
                android:autofillHints=""
                android:ems="10"
                android:focusable="false"
                android:hint="@string/h_date2"
                android:inputType="date"
                android:onClick="showDateDialog"
                android:tag="end_date" />

        </android.support.design.widget.TextInputLayout>

        <TextView
            android:id="@+id/textView4"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="@dimen/text_margin"
            android:layout_marginRight="@dimen/text_margin"
            android:layout_marginBottom="@dimen/option_margin"
            android:layout_weight="1"
            android:ems="10"
            android:paddingLeft="4sp"
            android:paddingRight="4sp"
            android:text="@string/t_check"
            android:textAppearance="@style/TextAppearance.Design.Counter"
            android:textColor="?android:attr/textColorHint" />

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="@dimen/text_margin"
            android:layout_marginRight="@dimen/text_margin"
            android:layout_weight="1"
            android:orientation="horizontal"
            android:paddingBottom="@dimen/text_margin">

            <CheckBox
                android:id="@+id/check_work"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="@dimen/text_margin"
                android:onClick="onCheckClicked"
                android:tag="work"
                android:text="@string/c_work" />

            <CheckBox
                android:id="@+id/check_smoke"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="@dimen/text_margin"
                android:onClick="onCheckClicked"
                android:tag="smoke"
                android:text="@string/c_smoke" />

            <CheckBox
                android:id="@+id/check_gym"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="@dimen/text_margin"
                android:onClick="onCheckClicked"
                android:tag="workout"
                android:text="@string/c_gym" />

        </LinearLayout>

    </LinearLayout>
</android.support.v4.widget.NestedScrollView>

Tho, in case the code above isn’t compatible with your project build settings (such as not ticking the “Use legacy android.support libraries” on newer Android Studio), you can try to follow the layout structure below. Then edit the parameters to match the parameters presented in the above code.

Content Layout Strcuture

Below is the resulting app. You’ll notice that every EditText in the layout structure above is wrapped inside a TextInputLayout. This is to add the special effect already created by material design from Google. The effect can be seen on both screenshots below.

Empty, unfocused EditText will show the hint in the box without any title (prevent any repetition). When clicked/focused, the hint text moves smoothly into above the input box turning into a title, and turned pink (accent color). It’ll be back to normal if they’re still empty, but stay the same with only color reverted if it’s filled but isn’t focused anymore.

You can modify the color.xml inside res/values/ folder to customize the app colors.

Meanwhile below is the preview of the date picker. It’s functioning as you’d expect from any date picker from the official Android app, all done without much hassle. Truly the power of easily integrated libraries. You can tap the year up top to move between years, or click the arrows to move between months, etc.

The date picker is implemented by setting the onClick parameter of an EditText to showDateDialog , and also prevent direct editing to the text. This way, when a user clicks on the box, they’ll be presented by this picker instead of manually typing them. The resulting data is still your usual text in an EditText, thus no special case necessary for reading the data.

App Preview – Date picker.

Now that all component examples are created, we can use those as a base for creating your form, just by modifying this same layout data. The procedure will be given on step 4 below after you’ve finished managing the Google Sheet & Script. An important thing to notice is the tag parameters given to every input components, it’ll be the one used for matching the data into Google Sheet.


3 – Google Sheet

Let’s move on to the Google Sheet part for handling the data. As already mentioned before, having the data stored in a Google Sheet makes a lot of the process easier. No need to create an account/login management (you don’t want just anyone to be able to see the data), direct & easy data handling, Google Script integration, etc.

First off, create a new Sheet, fill the first row with texts similar to the image below (ignore the data under it for now). Those are Timestamp, name, phone, address, sex, work, smoke, workout, start_date, end_date. You can also just copy that text, paste, then split to columns.

Sheet Preview – Notice the header and compare them to the tags from the components from the layout example.

Those headers will be the ones used as a match to our tags we’ve given to every input component. In a way, it’s just a confirmation as to which data to be saved in the form.

Next, open up Tools > Script Editor > Accept any permission if asked. Here the Google Script will be paired to your Google Sheet instead of just being independent. Copy-paste the following code below. The only thing you need to change is the SHEET_NAME.

// original from: http://mashe.hawksey.info/2014/07/google-sheets-as-a-database-insert-with-apps-script-using-postget-methods-with-ajax-example/
// original gist: https://gist.github.com/willpatera/ee41ae374d3c9839c2d6 
// Caution: Must be assigned to a sheet, not as an independent script.

// Enter sheet name where data is to be written below
var SHEET_NAME = "Sheet1";
var EMPTY = ""; // Data filled if there's no such input

var SCRIPT_PROP = PropertiesService.getScriptProperties(); // new property service

// If you don't want to expose either GET or POST methods you can comment out the appropriate function
function doGet(e){
  return handleResponse(e);
}

function doPost(e){
  return handleResponse(e);
}

function handleResponse(e) {
  // shortly after my original solution Google announced the LockService[1]
  // this prevents concurrent access overwritting data
  // [1] http://googleappsdeveloper.blogspot.co.uk/2011/10/concurrency-and-google-apps-script.html
  // we want a public lock, one that locks for all invocations
  var lock = LockService.getPublicLock();
  lock.waitLock(30000);  // wait 30 seconds before conceding defeat.
  
  try {
    // next set where we write the data - you could write to multiple/alternate destinations
    var doc = SpreadsheetApp.openById(SCRIPT_PROP.getProperty("key"));
    var sheet = doc.getSheetByName(SHEET_NAME);
    
    // we'll assume header is in row 1 but you can override with header_row in GET/POST data
    var headRow = e.parameter.header_row || 1;
    var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
    var nextRow = sheet.getLastRow()+1; // get next row
    var row = []; 
    
    // loop through the header columns
    for (i in headers){
      if (headers[i] == "Timestamp"){ // special case if you include a 'Timestamp' column
        row.push(new Date());
        
      } else { // else use header name to get data
        
        // check if undefined
        var data = e.parameter[headers[i]];
        if (data === undefined) {
          row.push(EMPTY);
        } else {
          row.push(data);
        }
      }
    }
    
    // more efficient to set values as [][] array than individually
    sheet.getRange(nextRow, 1, 1, row.length).setValues([row]);
    // return json success results
    return ContentService
          .createTextOutput(JSON.stringify({"result":"success", "row": nextRow}))
          .setMimeType(ContentService.MimeType.JSON);
    
  } catch(e){
    
    // if error return this
    return ContentService
          .createTextOutput(JSON.stringify({"result":"error", "error": e}))
          .setMimeType(ContentService.MimeType.JSON);
    
  } finally { //release lock
    lock.releaseLock();
  }
}

function setup() {
    var doc = SpreadsheetApp.getActiveSpreadsheet();
    SCRIPT_PROP.setProperty("key", doc.getId());
}

Now go to Publish > Deploy as web app > Copy the URL presented > Set the bottom-most option “Who has access to the app” to “Anyone, even anonymous” > Update. That’s it. Now to give a data to your sheet, you can just do a regular HTTP Request directed towards that URL.

Now paste that web-app URL into the url variable inside our Android main program. In case you missed the URL, you can view it again by following the same process. No need to click Update, just copy the URL.


4 – Testing

4a – HTTP Request

You can test the web-app we’ve just deployed by using generic HTTP Request websites such as https://www.apirequest.io/.

  1. Open that link or any other tester of your choice.
  2. Paste the web-app URL given from the Google Script.
  3. Click on the “+ Params” to add a new parameter.
  4. Input the left side (key) to any of the headers in the Google Sheet, such as “name” (case-sensitive).
  5. Input the right side (value) to any value you wanted to add in that column.
  6. Add another parameter as necessary.
  7. Click send, and you’ll be given a response from our web-app.
  8. A successful request will give you a “success” result in the body section of the response.
  9. Otherwise, you might want to redeploy your web-app again.
HTTP Response – Success

4b – Android App

Testing the Android side means you have to be able to deploy your app. To do so, you may refer to another article online. Basically, all you have to do are either creating an AVD or Android Virtual Device (emulator) or plug-in your phone in Developer Mode to your computer.

Now, here is the step where you’ll stumble upon some errors. Try to slowly work some of those out, as there might be some adjustments needed since we’re not in the same development environment.

If done, the app will show up in your phone similar to the preview in Android Studio or our images above. Just fill in the form and send it. There’ll be a toast message giving you the HTTP Response similar to our testing step 4a above.

Response – Success

4c – Adding New Components

The procedures are as follow for creating every new component in the form:

  1. Copy-paste the components you want to add, be it directly from the XML visual view or the code view. The ID for the components aren’t important, just make sure they’re unique from each other (or just leave them blank).
  2. Modify the text and hint parameters (no need to edit text for EditText), you may edit them directly for easy testing (hard-coded) or use the resources (similar to the examples) for a more complete implementation.
  3. An important part: modify the tag parameter, make sure it’s unique from each other. This is the text for the header in Google Sheet.
  4. Special case: for radio buttons, set the tag to <group>_<data>. For example the sex radio group, there’re male & female. Set them to sex_male & sex_female. The result will be placed in the same column as the group, sex.
  5. Create a new column in the Google Sheet with header (first row) matching the tag you’ve assigned to the new component.

That’s it! You can add a lot of new components and the app wouldn’t need any further main program modification. All in all, the crucial steps are only assigning the tag for each component and modifying the visual elements such as text & hint. If those simple steps seemingly long, just try them out a couple of times and everything will feel perfectly modular.


Learning Tools

No direct example regarding this since a lot of similar projects are either superbly hard-coded or simplified just to get the “App-to-Sheet” functionalities working. Overall I took a lot of examples from Stack Overflow for more detailed problems, syntax, and/or functionalities to implement the universal data acquisition from the layout.


Learning Strategy

No major problem during the whole process of this project. The incremental testing steps help tremendously in understanding which part of the process is broken, as there’re many “parties” involved here.

Focusing on aesthetics also proved to be a very good refreshing chance to learn something outside of the technicalities. This might be something that can significantly boost time in creating another Android app, as there are already certain “feels” obtained from fine-tuning the parameters and knowing which to change.


Reflective Analysis

Usually, ease of usage is done at the expense of customizability. That’d be the case with your mainstream online forms such as Google Forms and Typeform. Now, following the same idea, we can try to balance both of them. The modularity of creating forms just in the layout side, and of course the full ability of native Android app which you can modify as much as you like. Implementing Google Sheet and Script also helps to skip technicalities of user management and authentication.


Conclusion & Future

A modular design method is not only simplistic and universal in implementation, but also gives app developers a way to expand with ease. Of course, the potential in such a method is unlimited, future development could be adding more components compatibility, a more streamlined process, etc at the expense of adding a little more complexity to your main program.


Ask away any question in the comment section, any feedback is also appreciated. As stated above, the everything is available in this GitHub repository.

You can check my other blog here about scraping contextual Reddit conversation.

Project duration + article creation: 14 hours.