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
Explore a wide range of UI templates and design demonstrations that simplify development and inspire creativity.
Comments
Post a Comment