Building an AI BERT Feedback Dialog: Real-Time Sentiment Analysis with Offline Support

Learn how to implement an AI-powered BERT feedback dialog in your Android app. Real-time sentiment analysis, offline support, and seamless UX design tips included


Introduction

  • Introduce the problem: Users need meaningful feedback that works both online and offline.
  • Briefly explain BERT and its advantages in sentiment analysis.
  • State the purpose of the post: To guide developers in implementing a feedback dialog with real-time AI and offline capabilities.


Step-by-Step Instructions

Step 1: Set Up Your Development Environment

Android Studio, Kotlin (or Java), and dependencies for TensorFlow Lite or PyTorch Mobile.


Include Gradle dependencies for AI models and other required libraries.


    // 🤖 TensorFlow Lite Support Library (Utilities and data handling for TensorFlow Lite)
    implementation("org.tensorflow:tensorflow-lite-support:0.4.4")
    // 🧠 TensorFlow Lite Task Text (Advanced text processing with TensorFlow Lite)
    implementation("org.tensorflow:tensorflow-lite-task-text:0.4.2")

..

Step 2: Integrate the BERT Model

Download or convert a lightweight BERT model for mobile use (TensorFlow Lite or ONNX).

Add the model file to your Android app's assets folder.

assets/mobilebert.tflite

..

Step 3: Build the Custom Dialog Layout

Design the dialog UI using XML (text input field, sentiment display, and submit button).

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/transparent"
    android:orientation="vertical"
    android:scrollbarAlwaysDrawVerticalTrack="true">

    <!-- 📝 The main container for the dialog -->
    <LinearLayout
        android:id="@+id/inputCont"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/transparent">

        <!-- 🖼️ Dialog box container with white background -->
        <RelativeLayout
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="70dp"
            android:background="@drawable/dialog_background_white"
            android:orientation="vertical"
            android:padding="5dp">

            <!-- 🛠️ Inner container for dialog content -->
            <LinearLayout
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                android:paddingTop="30dp">

                <!-- 🏷️ Title of the dialog -->
                <TextView
                    android:id="@+id/title"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="25dp"
                    android:gravity="center_horizontal"
                    android:text="We Value Your Feedback"
                    android:textAppearance="@android:style/TextAppearance.Material.Title"
                    android:textStyle="bold" />

                <!-- ✍️ Description or message for the user -->
                <TextView
                    android:id="@+id/message"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="5dp"
                    android:gravity="center_horizontal"
                    android:padding="16dp"
                    android:text="Your feedback is valuable to us. Please let us know your thoughts to help us improve." />

                <!-- 🖋️ User input field for feedback -->
                <com.google.android.material.textfield.TextInputLayout
                    android:id="@+id/userMessageLayout"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginLeft="3dp"
                    android:layout_marginRight="3dp"
                    android:maxWidth="488dp"
                    app:boxStrokeColor="@color/orange_stoke"
                    app:boxStrokeWidth="2dp"
                    app:counterEnabled="true"
                    app:counterMaxLength="150"
                    app:counterTextColor="@color/branding_orange_text"
                    app:errorEnabled="true"
                    app:hintTextColor="@color/branding_orange_text">

                    <!-- 🖍️ EditText for entering feedback -->
                    <EditText
                        android:id="@+id/userMessage"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:autofillHints="username"
                        android:ems="10"
                        android:gravity="top"
                        android:hint="@string/dialog_feedback_hint"
                        android:inputType="textMultiLine"
                        android:maxHeight="300dp"
                        android:maxLength="150"
                        android:maxLines="5"
                        android:minHeight="100dp"
                        android:scrollbars="vertical"
                        android:textColor="@color/branding_orange_text" />
                </com.google.android.material.textfield.TextInputLayout>

                <!-- 🚪 Buttons for user actions -->
                <LinearLayout
                    android:id="@+id/linearLayout"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="20dp"
                    android:orientation="horizontal"
                    android:padding="10dp">

                    <!-- 👍 Submit button -->
                    <com.google.android.material.button.MaterialButton
                        android:id="@+id/support_ok_button"
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center_horizontal"
                        android:layout_marginEnd="3dp"
                        android:layout_weight="1"
                        android:minHeight="48dp"
                        android:text="Submit"
                        android:textColor="@color/branding_orange_primary_container"
                        app:background="@color/branding_orange_text"
                        app:backgroundTint="@null" />

                    <!-- ❌ Cancel button -->
                    <com.google.android.material.button.MaterialButton
                        android:id="@+id/support_cancel_button"
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center_horizontal"
                        android:layout_marginStart="3dp"
                        android:layout_weight="1"
                        android:minHeight="48dp"
                        android:text="Cancel"
                        android:textColor="@color/branding_orange_primary_container"
                        app:background="@color/branding_orange_text"
                        app:backgroundTint="@null" />
                </LinearLayout>
            </LinearLayout>
        </RelativeLayout>
    </LinearLayout>

    <!-- 🌟 Decorative logo at the top -->
    <ImageView
        android:id="@+id/id_logo"
        android:layout_width="200dp"
        android:layout_height="300dp"
        android:layout_above="@+id/inputCont"
        android:layout_alignParentEnd="true"
        android:layout_marginBottom="-120dp"
        android:contentDescription="Logo"
        android:importantForAccessibility="no"
        app:srcCompat="@drawable/star_default" />
</RelativeLayout>

..

Step 4: Implement Real-Time Sentiment Analysis

Use a TextWatcher on the feedback input field to analyze sentiment as the user types.

inputField.addTextChangedListener(object : TextWatcher {
    override fun afterTextChanged(s: Editable?) {
        val sentiment = analyzeSentiment(s.toString())
        sentimentTextView.text = "Sentiment: $sentiment"
    }
})

..

Step 5: Add Offline Support

Ensure the BERT model is loaded locally for offline predictions.

Handle fallback logic when internet connectivity is unavailable.

val tfliteModel = FileUtil.loadMappedFile(context, "bert_model.tflite")

..

Step 6: Optimize for Performance

Use threading or coroutines for model inference to avoid blocking the main thread.


AiDialogFragment.kt

package com.boltuix.androidtemplate.ai

import android.app.Dialog
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import android.view.animation.LinearInterpolator
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.boltuix.androidtemplate.R
import com.boltuix.androidtemplate.databinding.DialogAiFragmentBinding
import com.boltuix.androidtemplate.databinding.DialogBoxAiFeedbackBinding
import com.boltuix.androidtemplate.databinding.DialogFragmentBinding
import com.boltuix.tensorflowlite.TextClassificationHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.tensorflow.lite.support.label.Category

/**
 * 💬 A fragment that demonstrates a dialog with AI-powered feedback using TensorFlow Lite.
 * This dialog allows users to provide feedback and get instant suggestions based on AI analysis.
 *
 * 🚀 Features:
 * - Interactive feedback dialog.
 * - Real-time text classification using TensorFlow Lite.
 * - Enhanced UI/UX with animations and accessibility features.
 *
 * 🧩 Dependencies:
 * - TensorFlow Lite support library.
 * - Material Components for Android.
 */
class AIDialogFragment : Fragment() {
    private lateinit var classifierHelper: TextClassificationHelper
    private var dialog: Dialog? = null
    private var _binding: DialogAiFragmentBinding? = null
    private val binding get() = _binding!!

    /**
     * Step 1: Inflate the view for the fragment.
     */
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = DialogAiFragmentBinding.inflate(inflater, container, false)
        return binding.root
    }

    /**
     * Step 2: Initialize UI components and AI helper on fragment view creation.
     */
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Step 2.1: Attach click listener to open the AI feedback dialog
        binding.buttonAiDialog.setOnClickListener {
            dialogAIFeedback()
        }

        // Step 2.2: Initialize TextClassificationHelper
        classifierHelper = TextClassificationHelper(
            context = requireContext(),
            listener = object : TextClassificationHelper.TextResultsListener {
                override fun onResult(results: List<Category>, inferenceTime: Long) {
                    val topResult = results.maxByOrNull { it.score }
                    if (topResult != null) {
                        showClassificationResult(topResult.label)
                    } else {
                        Toast.makeText(requireContext(), "No result found", Toast.LENGTH_SHORT).show()
                    }
                }

                override fun onError(error: String) {
                    Toast.makeText(requireContext(), error, Toast.LENGTH_SHORT).show()
                }
            }
        )
    }

    /**
     * Step 3: Build and display a custom feedback dialog.
     */
    private fun dialogAIFeedback() {
        dialog?.dismiss()
        dialog = Dialog(requireContext()).apply {
            requestWindowFeature(Window.FEATURE_NO_TITLE)
            val bind: DialogBoxAiFeedbackBinding = DialogBoxAiFeedbackBinding.inflate(layoutInflater)
            setContentView(bind.root)

            // Configure the dialog window
            window?.apply {
                setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT)
                setGravity(Gravity.BOTTOM)
                setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
                val params: WindowManager.LayoutParams = attributes
                params.dimAmount = 0.7f
                attributes = params
                addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
            }

            resetFeedbackUI(bind)

            // Step 3.1: Add a TextWatcher for real-time text updates
            bind.userMessage.addTextChangedListener(object : TextWatcher {
                override fun afterTextChanged(s: Editable?) {
                    val text = s.toString()
                    if (text.split("\\s+".toRegex()).size >= 3) {
                        classifyTextRealTime(text, bind)
                    } else {
                        resetFeedbackUI(bind)
                    }
                }

                override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
                override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
            })

            // Step 3.2: Handle "Submit" button click
            bind.supportOkButton.setOnClickListener {
                Toast.makeText(requireContext(), "Feedback submitted!", Toast.LENGTH_SHORT).show()
                dismiss()
            }

            // Step 3.3: Handle "Cancel" button click
            bind.supportCancelButton.setOnClickListener {
                dismiss()
            }

            setCancelable(true)
            show()
        }
    }

    /**
     * Step 4: Classify the given text using AI in real-time.
     *
     * @param text The input text to classify.
     * @param bind The binding object for updating the UI.
     */
    private fun classifyTextRealTime(text: String, bind: DialogBoxAiFeedbackBinding) {
        lifecycleScope.launch {
            withContext(Dispatchers.Default) {
                try {
                    classifierHelper.classify(text) { results ->
                        val topResult = results.maxByOrNull { it.score }
                        topResult?.let {
                            updateFeedbackUI(bind, it.label)
                        } ?: resetFeedbackUI(bind)
                    }
                } catch (e: Exception) {
                    requireActivity().runOnUiThread {
                        Toast.makeText(requireContext(), "Error during classification: ${e.message}", Toast.LENGTH_SHORT).show()
                        resetFeedbackUI(bind)
                    }
                }
            }
        }
    }

    /**
     * Step 5: Update the feedback UI based on the classification result.
     *
     * @param bind The binding object for the feedback dialog.
     * @param label The classification label.
     */
    private fun updateFeedbackUI(bind: DialogBoxAiFeedbackBinding, label: String) {
        requireActivity().runOnUiThread {
            when (label) {
                "positive" -> {
                    bind.title.text = "Thank You!"
                    bind.message.text = "We’re thrilled to hear your feedback!"
                    bind.idLogo.setImageResource(R.drawable.star_happy)
                    bind.idLogo.animate().rotationBy(360f).setDuration(500).setInterpolator(LinearInterpolator()).start()
                }
                "negative" -> {
                    bind.title.text = "We’re Sorry"
                    bind.message.text = "We’ll work on improving. Thank you!"
                    bind.idLogo.setImageResource(R.drawable.star_sad)
                }
                else -> resetFeedbackUI(bind)
            }
        }
    }

    /**
     * Step 6: Reset the feedback UI to its default state.
     *
     * @param bind The binding object for the feedback dialog.
     */
    private fun resetFeedbackUI(bind: DialogBoxAiFeedbackBinding) {
        bind.title.text = "We Value Your Feedback"
        bind.message.text = "Please let us know your thoughts."
        bind.idLogo.setImageResource(R.drawable.star_default)
    }


    /**
     * ✨ Displays the classification result in the UI.
     *
     * @param label The top classification label.
     */
    private fun showClassificationResult(label: String) {
        // 🛑 No implementation for simplicity
    }

    /**
     * Step 7: Clean up resources when the fragment is destroyed.
     */
    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
        dialog?.dismiss()
    }
}

..

TextClassificationHelper.kt

package com.boltuix.tensorflowlite

import android.content.Context
import org.tensorflow.lite.support.label.Category
import org.tensorflow.lite.task.core.BaseOptions
import org.tensorflow.lite.task.text.nlclassifier.BertNLClassifier
import java.util.concurrent.Executors

/**
 * A helper class for performing text classification using TensorFlow Lite's BERT model.
 * This class initializes a BERT text classifier and provides methods for classifying text
 * in a background thread to avoid blocking the UI thread.
 *
 * @property context The application context, used for loading the BERT model.
 * @property listener A listener interface to handle results or errors from text classification.
 */
class TextClassificationHelper(
    val context: Context,
    val listener: TextResultsListener,
) {
    private val bertClassifier: BertNLClassifier
    private val executor = Executors.newSingleThreadExecutor() // Single-threaded executor

    /**
     * Initializes the BERT text classifier with the provided options.
     * The classifier is loaded from the asset file specified by [MOBILEBERT].
     */
    init {
        val options = BertNLClassifier.BertNLClassifierOptions.builder()
            .setBaseOptions(BaseOptions.builder().build())
            .build()
        bertClassifier = BertNLClassifier.createFromFileAndOptions(
            context,
            MOBILEBERT,
            options
        )
    }

    /**
     * Classifies the given text using the BERT classifier.
     * The classification process runs in a background thread, and the results are returned
     * via the provided callback.
     *
     * @param text The input text to classify.
     * @param callback A lambda function to receive the classification results as a list of [Category].
     */
    fun classify(text: String, callback: (List<Category>) -> Unit) {
        executor.execute {
            try {
                // Perform text classification
                val results = bertClassifier.classify(text)
                // Return results to the callback
                callback(results)
            } catch (e: Exception) {
                // Notify listener of an error
                listener.onError(e.message ?: "Classification failed")
            }
        }
    }

    /**
     * Listener interface for handling results or errors from text classification.
     */
    interface TextResultsListener {
        /**
         * Called when an error occurs during text classification.
         *
         * @param error The error message.
         */
        fun onError(error: String)

        /**
         * Called when text classification completes successfully.
         *
         * @param results The classification results as a list of [Category].
         * @param inferenceTime The time taken for classification, in milliseconds.
         */
        fun onResult(results: List<Category>, inferenceTime: Long)
    }

    companion object {
        /**
         * The file name of the BERT model located in the assets directory.
         */
        const val MOBILEBERT = "mobilebert.tflite"
    }
}

..






Now Available on Play Store

Download the app here.

Explore a wide range of UI templates and design demonstrations that simplify development and inspire creativity.


Comments