• Latest
How to Build an Android Image Feed Application

How to Build an Android Image Feed Application

December 19, 2022
REST vs. Messaging for Microservices

REST vs. Messaging for Microservices

March 25, 2023
Exoprimal Beta, Deceive Inc Early Thoughts, Redfall Could Cut Required Connection & More Ep 467

Exoprimal Beta, Deceive Inc Early Thoughts, Redfall Could Cut Required Connection & More Ep 467

March 25, 2023
WhatsApp rolling out voice support for posting status updates

WhatsApp working on new short video messages

March 25, 2023
Bungie Honors Lance Reddick In This Week At Bungie Post, Zavala Still Has A Few Performances Coming

Bungie Honors Lance Reddick In This Week At Bungie Post, Zavala Still Has A Few Performances Coming

March 25, 2023
Mad World Drops New Launch Trailer, Release Date To Be Announced Next Week

Mad World Drops New Launch Trailer, Release Date To Be Announced Next Week

March 25, 2023
Reviews Featuring ‘Storyteller’, Plus ‘Atelier Ryza 3’ and Today’s Other Releases and Sales – TouchArcade

Reviews Featuring ‘Storyteller’, Plus ‘Atelier Ryza 3’ and Today’s Other Releases and Sales – TouchArcade

March 25, 2023
Redmi A2 and Redmi A2+ quietly debut at the low-end

Redmi A2 and Redmi A2+ quietly debut at the low-end

March 25, 2023
New Act And Season Mode Coming to Undecember In April

New Act And Season Mode Coming to Undecember In April

March 25, 2023
Digital Extremes Drops New “Venomess” Wayfinder, Coming In Season One At Launch

Digital Extremes Drops New “Venomess” Wayfinder, Coming In Season One At Launch

March 25, 2023
Pantheon Dev Vlog Talks Open World, Enjoying The Game With Limited Time, And More

Pantheon Dev Vlog Talks Open World, Enjoying The Game With Limited Time, And More

March 25, 2023
Lamp but not regular ?? product link in comment box+free shipping #shorts #gadgets #products

Lamp but not regular ?? product link in comment box+free shipping #shorts #gadgets #products

March 25, 2023
Diving Deep Into Sea Of Stars’ Nostalgic Soundtrack

Diving Deep Into Sea Of Stars’ Nostalgic Soundtrack

March 25, 2023
Advertise with us
Saturday, March 25, 2023
Bookmarks
  • Login
  • Register
GetUpdated
  • Game Updates
  • Mobile Gaming
  • Playstation News
  • Xbox News
  • Switch News
  • MMORPG
  • Game News
  • IGN
  • Retro Gaming
  • Tech News
  • Apple Updates
  • Jailbreak News
  • Mobile News
  • Software Development
  • Photography
  • Contact
No Result
View All Result
GetUpdated
No Result
View All Result
GetUpdated
No Result
View All Result
ADVERTISEMENT

How to Build an Android Image Feed Application

December 19, 2022
in Software Development
Reading Time:49 mins read
0 0
0
Share on FacebookShare on WhatsAppShare on Twitter


This tutorial will discuss how to build an Android image feed application using Amity Social Cloud. By the end of this tutorial, you will have built your own social app capable of algorithmically ranking image posts in an aggregated feed from your community.

We’ll start with prerequisites for creating a new network in Amity Portal and a project in Android Studio. Then we’ll discuss how to create the Gradle setup and initialize the Social SDK. After this, we’ll go through the implementation, and finally, we’ll code the components and build out the screens.

As a result, you’ll be able to transform your Android application into a powerful social product and engage your users with personalized feeds aggregating image posts from your community.

Prerequisites

Network Setup

  • If you don’t yet have an Amity account, please view the step-by-step guide on how to create a new network in Amity Portal.

Tools and IDE

You can download Android Studio 3.6 from the Android Studio page.

Android Studio provides a complete IDE, including an advanced code editor and app templates. It also contains tools for development, debugging, testing, and performance that makes it faster and easier to develop apps. You can use Android Studio to test your apps with a large range of preconfigured emulators, or on your own mobile device. You can also build production apps and publish apps on the Google Play store.

Android Studio is available for computers running Windows or Linux, and for Macs running macOS. The OpenJDK (Java Development Kit) is bundled with Android Studio.

Environment Setup

Create a New Android App

First of all, we will need to create an application in Android Studio.

1. Open Android Studio.

2. In the Welcome to Android Studio dialog, click Start a new Android Studio project.

Android Studio startscreen

3. Select the “No Activity” option and click Next.

Android Studio templates

4. Name your application and package name and select the minimum SDK as API 21.
Android project settings

Gradle Setup

There are a few key dependencies that we need to include in the application. The image feed app needs abilities to interact with ASC SDK and render images. To render images, we pick the Glide library. It is a simple and powerful image rendering library.

Project’s settings.gradle:

pluginManagement {
    repositories {
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven { url 'https://jitpack.io' }
    }
}
rootProject.name = "Amity Image Feed"
include ':app'

Application’s build.gradle:

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt'
}

android {
    compileSdk 32

    defaultConfig {
        applicationId "com.amity.imagefeed"
        minSdk 21
        targetSdk 32
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget="1.8"
    }
    packagingOptions {
        exclude 'META-INF/INDEX.LIST'
        exclude 'META-INF/io.netty.versions.properties'
    }
    buildFeatures {
        viewBinding true
        dataBinding true
    }
}

dependencies {

    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.4.1'
    implementation 'com.google.android.material:material:1.5.0'
    implementation 'androidx.navigation:navigation-fragment-ktx:2.4.2'
    implementation 'androidx.navigation:navigation-ui-ktx:2.4.2'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

    implementation 'com.github.AmityCo.Amity-Social-Cloud-SDK-Android:amity-sdk:5.16.0'
    implementation 'com.github.bumptech.glide:glide:4.13.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.13.0'
}

Social SDK Initialization

Before using the Social SDK, you will need to create a new SDK instance with your API key. Please find your account API key in Amity Social Cloud Console.

After logging in Console:

  1. Click Settings to expand the menu.
  2. Select Security.
  3. On the Security page, you can find the API key in the Keys section.

Amity API key

We currently support multi-data center capabilities for the following regions:

Amity multi-data center regions

Then attach the API key and AmityEndpoint in the newly created application class. Once you created the application class, don’t forget to declare it in AndroidManifest.xml as well.

class ImageFeedApp: Application() {

    override fun onCreate() {
        super.onCreate()
        AmityCoreClient.setup("apiKey", AmityEndpoint.SG)
    }

}

Implementation Overview

Screens

First of all, what we’re developing is an image feed application that would contain a few essential pages:

  • Login screen – The page where the user identifies themselves
  • Image feed screen – The main page that shows the image feed
  • Image post-creation screen – The page where the user can upload a new image post to the feed
  • Comment screen – he page where the user can add, edit, remove and view comments in an image post.

Architecture

As Google recommended – each application should have at least two layers:

  • The UI layer that displays application data on the screen
  • The data layer that contains the business logic of your app and exposes application data

You can add an additional layer called the domain layer to simplify and reuse the interactions between the UI and data layers. In this tutorial, we’d like to adopt the principle and build our application by using MVVM architecture (as described in the image below).

Amity overview architecture: presentation, viewmodel and data source

Presentation is basically an Activity, Fragment, or View which is responsible for displaying UI (User Interface) and giving UX (User Experience) of users. This component will interact with the View Model component to request, send, and save data or state.

ViewModel is fully responsible for communicating between the presentation layer and the data layer and also representing the state, structure, and behavior of the model in the data layer.

Data source is responsible for implementing business logic and holding data or state in the app (in this case, Amity Social SDK).

Navigation Graph

We now know that the essential screens are the login, image feed, post creation, and comment screens. Let’s start by creating four blank fragments and build a navigation graph for them. The navigation should begin with the login screen and then move on to the image feed screen. The image feed screen should also have links to the comment list and the post-creation screens.
Image feed screen should also have links to the comment list and the post-creation screens.

<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="@+id/nav_graph"
    app:startDestination="@id/LoginFragment">

    <fragment
        android:id="@+id/LoginFragment"
        android:name="com.amity.imagefeed.fragment.LoginFragment"
        android:label="Login Fragment">

        <action
            android:id="@+id/action_LoginFragment_to_ImageFeedFragment"
            app:destination="@id/ImageFeedFragment" ></action>
    </fragment>

    <fragment
        android:id="@+id/ImageFeedFragment"
        android:name="com.amity.imagefeed.fragment.ImageFeedFragment"
        android:label="Image Feed Fragment">

        <action
            android:id="@+id/action_ImageFeedFragment_to_LoginFragment"
            app:destination="@id/LoginFragment" ></action>

        <action
            android:id="@+id/action_ImageFeedFragment_to_CreatePostFragment"
            app:destination="@id/CreatePostFragment" ></action>

        <action
            android:id="@+id/action_ImageFeedFragment_to_CommentListFragment"
            app:destination="@id/CommentListFragment" ></action>
    </fragment>


    <fragment
        android:id="@+id/CreatePostFragment"
        android:name="com.amity.imagefeed.fragment.CreatePostFragment"
        android:label="Create Post Fragment">

        <action
            android:id="@+id/action_CreatePostFragment_to_ImageFeedFragment"
            app:destination="@id/ImageFeedFragment" ></action>
    </fragment>

    <fragment
        android:id="@+id/CommentListFragment"
        android:name="com.amity.imagefeed.fragment.CommentListFragment"
        android:label="Create Post Fragment">

        <action
            android:id="@+id/action_CreatePostFragment_to_ImageFeedFragment"
            app:destination="@id/ImageFeedFragment" ></action>
    </fragment>
</navigation>

Login Screen

In the past few sections, we already prepared everything for the app development. Now, it’s time to start implementing the application.

Create a Login Screen Layout

To build the login screen, the most important part is definitely a layout. Let’s build a simple login screen with a userId edit text input, a display name edit text input, and a login button by the following XML layout.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".activity.LoginActivity">


    <LinearLayout
        android:layout_centerVertical="true"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <EditText
            android:id="@+id/user_id_edit_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginHorizontal="32dp"
            android:hint="Enter user id" ></EditText>

        <EditText
            android:id="@+id/display_name_edit_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginHorizontal="32dp"
            android:hint="Enter Display Name" ></EditText>


        <Button
            android:id="@+id/login_button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_marginHorizontal="32dp"
            android:layout_marginTop="128dp"
            android:text="Login" ></Button>

    </LinearLayout>

</RelativeLayout>

Create a Login View Model

Before we create an activity, we’d like to separate business logic into a view model, as we mentioned earlier. However, since Amity Social SDK already handles the complexity on the data layer, we won’t need to create a data layer in the application at all.

The expected function inside a login view model would only be a login() function. We need a user to authenticate to the Amity Social Cloud system by using AmityCoreClient.login(userId).

class LoginViewModel : ViewModel() {

    fun login(
        userId: String,
        displayName: String,
        onLoginSuccess: () -> Unit,
        onLoginError: (throwable: Throwable) -> Unit
    ): Completable {
        return AmityCoreClient.login(userId)
            .displayName(displayName)
            .build()
            .submit()
            .subscribeOn(Schedulers.io())
            .doOnComplete { onLoginSuccess.invoke() }
            .doOnError { onLoginError.invoke(it) }
    }
}

Create a Login Fragment

Whoo! Ok, now we’re ready to connect everything we built into a fragment. When the user presses the login button, a fragment will interact with a view model to log in to the Amity Social Cloud system. The required fields will be retrieved from edit text inputs, which are userId and displayName. If the user is logged in properly, it will navigate to the main image feed screen. If an error occurs, it will show the toast that indicates the error.

class LoginFragment : Fragment() {
    private val viewModel: LoginViewModel by viewModels()

    private var binding: FragmentLoginBinding? = null

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentLoginBinding.inflate(inflater, container, false)
        return binding?.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding?.loginButton?.setOnClickListener {
            val userId = binding?.userIdEditText?.text.toString()
            val displayName = binding?.displayNameEditText?.text.toString()
            viewModel.login(
                userId = userId,
                displayName = displayName,
                onLoginSuccess = { navigateToImageFeedPage() },
                onLoginError = { presentErrorDialog(it) }
            )
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe()
        }
    }

    private fun navigateToImageFeedPage() {
        findNavController().navigate(R.id.action_LoginFragment_to_ImageFeedFragment)
    }

    private fun presentErrorDialog(throwable: Throwable) {
        Toast.makeText(context, throwable.message, Toast.LENGTH_SHORT).show()
    }
}

Lastly, we need an activity as a container for fragments. We’re going to create an empty activity that specifies our created navigation graph as navGraph.

Layout:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">

    <fragment
        android:id="@+id/nav_host_fragment_content_main"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/nav_image_feed" ></fragment>
</androidx.constraintlayout.widget.ConstraintLayout>

Activity class:

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }
}

Before trying to compile code for the first time, don’t also forget to declare MainActivity as a launcher activity as well.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.amity.imagefeed">

    <application
        android:name=".ImageFeedApp"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.AmityImageFeed.NoActionBar">

        <activity
            android:name=".activity.MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" ></action>
                <category android:name="android.intent.category.LAUNCHER" ></category>
            </intent-filter>
        </activity>

    </application>

</manifest>

Yay! we finally successfully created a simple login screen. Let’s continue building the image feed screen and image post-creation screen in the next part!

Login screen

Image Feed Screen

This screen is a little more complicated and also has a number of different components on it. So, before we start implementing it, let’s break it down as much as possible.

  • Image feed main container – The page that combines all components together (ImageFeedFragment)
  • Image feed ViewModel – The ViewModel class that stores and manages UI-related data in a  lifecycle (ImageFeedViewModel)
  • List of image posts – The list that shows a collection of image posts on the feed (ImageFeedAdapter)
  • Empty state  – The view when there are no posts on the feed.
  • Floating action button – The button that allows the user to create a new post.
  • Progress bar – The view when the feed is loading.

In the image feed screen, we can now see all of the necessary components. It’s time to put them into action and combine them together.

Create an Image Post Item Layout

We’re taking a bottom-up approach to creating the image feed screen, which means we’ll start with a RecyclerView holder layout first and an image feed screen afterward. There would be three main elements of the image post-item arrangement. First, there’s the header, which includes the poster’s avatar and display name, as well as the time it was posted. The posted image is the second section, followed by the number of comments and reactions. Phew. Let’s see what the layout XML would look like.

<?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">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp">

        <com.google.android.material.imageview.ShapeableImageView
            android:id="@+id/avatar_image_view"
            style="@style/AmityCircularAvatarStyle"
            android:layout_width="42dp"
            android:layout_height="42dp"
            android:layout_marginStart="16dp"
            android:background="@color/light_grey"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" ></com>

        <LinearLayout
            android:id="@+id/layout_posted_by"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="12dp"
            android:layout_marginBottom="12dp"
            app:layout_constraintStart_toEndOf="@id/avatar_image_view"
            app:layout_constraintTop_toTopOf="@id/avatar_image_view">

            <TextView
                android:id="@+id/display_name_text_view"
                style="@style/TextAppearance.AppCompat.Body1"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:ellipsize="end"
                android:maxLines="2"
                android:textAlignment="textStart"
                android:textColor="@color/primary_grey"
                android:textStyle="bold"
                app:layout_constraintStart_toStartOf="parent"
                tools:text="Brian Marty" ></TextView>

        </LinearLayout>

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="12dp"
            android:gravity="center_vertical"
            android:orientation="horizontal"
            app:layout_constraintLeft_toRightOf="@id/avatar_image_view"
            app:layout_constraintTop_toBottomOf="@id/layout_posted_by">

            <TextView
                android:id="@+id/post_time_text_view"
                style="@style/TextAppearance.Material3.BodyMedium"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="12dp"
                android:ellipsize="end"
                android:gravity="center_vertical"
                android:maxLines="1"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                tools:text="Posted on 8:34 PM,  25 March 2023" ></TextView>

        </LinearLayout>

    </androidx.constraintlayout.widget.ConstraintLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp">

        <ImageView
            android:id="@+id/item_gallery_post_image_imageview"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:adjustViewBounds="true"
            android:background="@color/light_grey"
            android:scaleType="centerCrop"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintDimensionRatio="H,1:1"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" ></ImageView>
    </androidx.constraintlayout.widget.ConstraintLayout>


    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="12dp">

        <LinearLayout
            android:id="@+id/reaction_status_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            tools:ignore="MissingConstraints">

            <TextView
                android:id="@+id/description_textview"
                style="@style/TextAppearance.Material3.BodyMedium"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="18dp"
                android:textColor="@color/primary_grey"
                android:textStyle="bold"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                tools:text="Brian Marty : my favourite place on earth" ></TextView>

        </LinearLayout>


        <LinearLayout
            android:id="@+id/post_action_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginEnd="16dp"
            android:layout_marginStart="12dp"
            android:gravity="center_vertical"
            android:orientation="horizontal"
            app:layout_constraintTop_toBottomOf="@id/reaction_status_view">

            <TextView
                android:padding="8dp"
                android:id="@+id/like_count_textview"
                style="@style/TextAppearance.AppCompat.Body1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:drawablePadding="12dp"
                android:gravity="center_vertical"
                android:textColor="@color/primary_grey"
                app:drawableStartCompat="@drawable/ic_like_reaction"
                tools:text="12 likes" ></TextView>


            <TextView
                android:id="@+id/comment_count_textview"
                style="@style/TextAppearance.AppCompat.Body1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="24dp"
                android:drawablePadding="12dp"
                android:padding="8dp"
                android:gravity="center_vertical"
                android:textColor="@color/primary_grey"
                app:drawableStartCompat="@drawable/ic_comment"
                tools:text="86 comments" ></TextView>

        </LinearLayout>

    </androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

Image post item layout

Create an Image Feed Screen Layout

For the image feed, we created a RecyclerView holder. Now we just need a RecyclerView and a container for it. As earlier said, we’ll have a page that displays a list of image posts, an empty state view when there are no posts, and a floating action button for creating new posts.

<?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="@color/white">

    <androidx.recyclerview.widget.RecyclerView
        android:clipToPadding="false"
        android:paddingTop="56dp"
        android:id="@+id/feed_recyclerview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" ></androidx>

    <LinearLayout
        android:visibility="gone"
        android:id="@+id/empty_feed_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical">

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="24dp"
            android:src="https://dzone.com/articles/@drawable/ic_ballon" ></ImageView>

        <TextView
            style="@style/TextAppearance.AppCompat.Body1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/amity_empty_feed"
            android:textColor="@color/black"
            android:textStyle="bold" ></TextView>

        <TextView
            android:id="@+id/tvEmptyGlobalFeed"
            style="@style/TextAppearance.AppCompat.Body1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="2dp"
            android:text="@string/amity_empty_feed_description"
            android:textColor="@color/black" ></TextView>

    </LinearLayout>

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fabCreatePost"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_alignParentBottom="true"
        android:layout_margin="24dp"
        android:contentDescription="@string/amity_add_post"
        app:srcCompat="@drawable/ic_add_post" ></com>

    <com.google.android.material.progressindicator.CircularProgressIndicator
        android:id="@+id/progress_bar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        android:layout_centerInParent="true"
        android:visibility="visible"
     ></com>


</RelativeLayout>

The empty state view of an image feed

Create an Image Feed ViewModel

The ViewModel would be simple and straightforward. In this view model, there is only one function: getFeed(). The function is in charge of getting posts via AmitySocialClient and passing PagingData<AmityPost> to the presentation layer.

@ExperimentalPagingApi
class ImageFeedViewModel : ViewModel() {

    fun getFeed(onFeedUpdated: (postPagingData: PagingData<AmityPost>) -> Unit): Completable {
        return AmitySocialClient.newFeedRepository()
            .getGlobalFeed()
            .build()
            .getPagingData()
            .doOnNext { onFeedUpdated.invoke(it) }
            .ignoreElements()
            .subscribeOn(Schedulers.io())
    }
}

Create an Image Feed Adapter

We now know that the data return PagingData<AmityPost> model for us to render in the image post item after we created a ViewModel. To properly construct a RecyclerView, two components are required for creating an image feed adapter: PagingAdapter and ViewHolder. Let’s construct both classes now that the layout XML has been prepared as well.

ViewHolder:

class ImagePostViewHolder(private val binding: ListItemPostBinding) :
    RecyclerView.ViewHolder(binding.root) {

    fun bind(post: AmityPost?) {
        presentHeader(post)
        presentContent(post)
        presentFooter(post)
    }

    private fun presentHeader(post: AmityPost?) {
        //render poster's avatar
        Glide.with(itemView)
            .load(post?.getPostedUser()?.getAvatar()?.getUrl(AmityImage.Size.SMALL))
            .transition(DrawableTransitionOptions.withCrossFade())
            .into(binding.avatarImageView)
        //render poster's display name
        binding.displayNameTextView.text = post?.getPostedUser()?.getDisplayName() ?: "Unknown user"
        //render posted time
        binding.postTimeTextView.text =
            post?.getCreatedAt()?.millis?.readableFeedPostTime(itemView.context) ?: ""
    }


    private fun presentContent(post: AmityPost?) {
        //render image post
        //clear image cache from the view first
        binding.itemGalleryPostImageImageview.setImageDrawable(null)
        //make sure that the post contains children posts
        if (post?.getChildren()?.isNotEmpty() == true) {
            val childPost = post.getChildren()[0]
            //make sure that the child post is an image post
            if (childPost.getData() is AmityPost.Data.IMAGE
                && (childPost.getData() as AmityPost.Data.IMAGE).getImage() != null
            ) {
                val image = (childPost.getData() as AmityPost.Data.IMAGE).getImage()
                Glide.with(itemView)
                    .load(image?.getUrl(AmityImage.Size.LARGE))
                    .transition(DrawableTransitionOptions.withCrossFade())
                    .into(binding.itemGalleryPostImageImageview)
            }
        }
        //render image post description
        val postDescription = (post?.getData() as? AmityPost.Data.TEXT)?.getText() ?: ""
        val displayName = post?.getPostedUser()?.getDisplayName() ?: "Unknown user"
        binding.descriptionTextview.text = "$displayName : $postDescription"
    }

    private fun presentFooter(post: AmityPost?) {
        //render like count
        binding.likeCountTextview.text = getLikeCountString(post?.getReactionCount() ?: 0)
        //render comment count
        binding.commentCountTextview.text = getCommentCountString(post?.getCommentCount() ?: 0)
    }

    private fun getLikeCountString(likeCount: Int): String {
        return itemView.context.resources.getQuantityString(
            R.plurals.amity_number_of_likes,
            likeCount,
            likeCount
        )
    }

    private fun getCommentCountString(reactionCount: Int): String {
        return itemView.context.resources.getQuantityString(
            R.plurals.amity_number_of_comments,
            reactionCount,
            reactionCount
        )
    }
}

PagingAdapter:

class ImagePostAdapter :
    PagingDataAdapter<AmityPost, ImagePostViewHolder>(ImagePostDiffCallback()) {

    override fun onBindViewHolder(holder: ImagePostViewHolder, position: Int) {
        holder.bind(getItem(position))
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImagePostViewHolder {
        return ImagePostViewHolder(
            ListItemPostBinding.inflate(
                LayoutInflater.from(parent.context),
                parent,
                false
            )
        )
    }
}


private class ImagePostDiffCallback : DiffUtil.ItemCallback<AmityPost>() {

    override fun areItemsTheSame(oldItem: AmityPost, newItem: AmityPost): Boolean {
        return oldItem.getPostId() == newItem.getPostId()
    }

    override fun areContentsTheSame(oldItem: AmityPost, newItem: AmityPost): Boolean {
        return oldItem.getPostId() == newItem.getPostId()
                && oldItem.getUpdatedAt() == newItem.getUpdatedAt()
                && oldItem.getReactionCount() == newItem.getReactionCount()
    }
}

After we created the simple ViewHolder and adapter, let’s add a few more functionalities to the footer part. The footer should be able to add or remove a like reaction when clicking the like button, as well as the comment button, and it should navigate the user to the comment list screen as well. The like button will only be highlighted if the current user already reacted to the post.

private fun presentFooter(post: AmityPost?) {
        //render like count
        binding.likeCountTextview.text = getLikeCountString(post?.getReactionCount() ?: 0)
        //render comment count
        binding.commentCountTextview.text = getCommentCountString(post?.getCommentCount() ?: 0)

        val isLikedByMe = post?.getMyReactions()?.contains("like") == true
        val context = binding.root.context
        val highlightedColor = ContextCompat.getColor(context, R.color.teal_700)
        val inactiveColor = ContextCompat.getColor(context, R.color.dark_grey)
        if (isLikedByMe) {
            //present highlighted color if the post is liked by me
            setLikeTextViewDrawableColor(highlightedColor)
        } else {
            //present inactive color if the post isn't liked by me
            setLikeTextViewDrawableColor(inactiveColor)
        }
        //add or remove a like reaction when clicking like textview
        binding.likeCountTextview.setOnClickListener {
            if (isLikedByMe) {
                post?.react()?.removeReaction("like")?.subscribe()
            } else {
                post?.react()?.addReaction("like")?.subscribe()
            }
        }
        //navigate to comment list screen when clicking comment textview
        binding.commentCountTextview.setOnClickListener {
            Navigation.findNavController(binding.root).navigate(R.id.action_ImageFeedFragment_to_CommentListFragment)
        }
 }

private fun setLikeTextViewDrawableColor(@ColorInt color: Int) {
        for (drawable in binding.likeCountTextview.compoundDrawablesRelative) {
            if (drawable != null) {
                drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
            }
        }
        binding.likeCountTextview.setTextColor(color)
 }

Create an Image Feed Fragment

Cool! Now it’s time to put everything we’ve worked on together. This is the most intriguing aspect of the screen. Layouts, an adaptor, and a ViewModel have all been prepared. All of this will be combined into a fragment. The fragment will be in charge of interacting with the ViewModel and updating the view states. We need additionally deal with the loading, loading, and empty states of the feed. So, here it is!

@ExperimentalPagingApi
class ImageFeedFragment : Fragment() {

    private val viewModel: ImageFeedViewModel by viewModels()
    private lateinit var adapter: ImagePostAdapter

    private var binding: FragmentImageFeedBinding? = null

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentImageFeedBinding.inflate(inflater, container, false)
        return binding?.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        setupRecyclerView()
        getFeedData()
    }

    private fun setupRecyclerView() {
        adapter = ImagePostAdapter()
        binding?.feedRecyclerview?.layoutManager = LinearLayoutManager(context)
        binding?.feedRecyclerview?.adapter = adapter
        adapter.addLoadStateListener { loadStates ->
            when (loadStates.mediator?.refresh) {
                is LoadState.NotLoading -> {
                    handleEmptyState(adapter.itemCount)
                }
            }
        }
    }

    private fun getFeedData() {
        viewModel.getFeed {
            lifecycleScope.launch {
                adapter.submitData(it)
            }
        }
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe()
    }

    private fun handleEmptyState(itemCount: Int) {
        binding?.progressBar?.visibility = View.GONE
        binding?.emptyFeedView?.visibility = if (itemCount > 0) View.GONE else View.VISIBLE
    }
}

Brilliant!! Let’s try to run the app and see how it goes.

An Android image feed within an application

Image Post Creation Screen

Now that we can see pre-created image posts on a feed, it’s the moment to create a new post! Of course, we will consistently use the same structure of this screen as the image feed screen: a fragment, ViewModel and XML layout.

Create an Image Post Creation Layout

This screen’s functionality should be straightforward: an EditTextView for the post description, a TextView for the image attachment button, an ImageView for the selected image preview, and finally, a button to create the post.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:clipToPadding="false"
    android:paddingTop="100dp">

    <androidx.appcompat.widget.AppCompatEditText
        android:id="@+id/create_post_edittext"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:layout_marginTop="64dp"
        android:hint="@string/amity_post_create_hint"
        app:layout_constraintTop_toTopOf="parent" ></androidx>

    <TextView
        android:id="@+id/attach_image_textview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:text="@string/amity_attach_image"
        android:textColor="@color/purple_500"
        android:textSize="16dp"
        android:textStyle="bold"
        app:layout_constraintTop_toBottomOf="@id/create_post_edittext" ></TextView>

    <ImageView
        android:visibility="gone"
        android:id="@+id/create_post_imageview"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="16dp"
        android:adjustViewBounds="true"
        android:background="@color/light_grey"
        android:scaleType="centerCrop"
        app:layout_constraintDimensionRatio="H,1:1"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/attach_image_textview" ></ImageView>

    <TextView
        android:background="@color/purple_500"
        android:textColor="@color/white"
        android:textStyle="bold"
        android:gravity="center"
        android:textSize="20dp"
        android:text="@string/amity_create_post"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        android:layout_width="0dp"
        android:layout_height="56dp" ></TextView>


</androidx.constraintlayout.widget.ConstraintLayout>

Create an Image Post Creation Fragment

Select Image From File Picker

The most interesting part of the screen could be selecting an image from the user’s device. To access users’ images we will use the ACTION_OPEN_DOCUMENT intent action that allows users to select a specific document or file to open. Additionally, we can specify the type of the document as image/* to scope only image documents. Upon getting a document URI returned, we can use ContentResolver.takePersistableUriPermission in order to persist the permission across restarts. Once the image is chosen, we also need to render the image preview by using Glide as well.

class CreatePostFragment : Fragment() {

    private val viewModel: CreatePostViewModel by viewModels()
    private var binding: FragmentCreatePostBinding? = null

    private val openDocumentResult =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult())
        { result: ActivityResult ->
            if (result.resultCode == Activity.RESULT_OK) {
                result.data?.data?.also { imageUri ->
                    requireActivity().contentResolver.takePersistableUriPermission(
                        imageUri,
                        Intent.FLAG_GRANT_READ_URI_PERMISSION
                    )
                    renderPreviewImage(imageUri)
                }
            }
        }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentCreatePostBinding.inflate(inflater, container, false)
        return binding?.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding?.attachImageTextview?.setOnClickListener { openDocumentPicker() }
    }

    private fun openDocumentPicker() {
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
            type = "image/*"
            addCategory(Intent.CATEGORY_OPENABLE)
        }
        openDocumentResult.launch(intent)
    }

    private fun renderPreviewImage(uri: Uri) {
        binding?.createPostImageview?.let {
            it.visibility = View.VISIBLE
            Glide.with(requireContext())
                .load(uri)
                .transition(DrawableTransitionOptions.withCrossFade())
                .into(it)
        }
    }
}

Create an Image Post Creation ViewModel

This screen’s view model is primarily responsible for creating an image post. Amity Social Cloud SDK requires two steps to create an image post: the first is to upload the image, and the second is to create a post using the image (see more details in this documentation).

class CreatePostViewModel : ViewModel() {

    fun createImagePost(
        postText: String,
        postImage: Uri,
        onPostCreationSuccess: (AmityPost) -> Unit,
        onPostCreationError: (throwable: Throwable) -> Unit
    ): Completable {
        return uploadImage(postImage)
            .flatMap {
                AmitySocialClient.newPostRepository()
                    .createPost()
                    .targetMe()
                    .image(images = arrayOf(it))
                    .text(text = postText)
                    .build()
                    .post()
            }
            .subscribeOn(Schedulers.io())
            .doOnError { onPostCreationError.invoke(it) }
            .doOnSuccess { onPostCreationSuccess(it) }
            .ignoreElement()
    }

    private fun uploadImage(imageUri: Uri): Single<AmityImage> {
        return AmityCoreClient.newFileRepository()
            .uploadImage(uri = imageUri)
            .build()
            .transfer()
            .doOnNext {
                when (it) {
                    is AmityUploadResult.ERROR -> {
                        throw it.getError()
                    }
                    is AmityUploadResult.CANCELLED -> {
                        throw UPLOAD_CANCELLED_EXCEPTION
                    }
                }
            }
            .filter { it is AmityUploadResult.COMPLETE }
            .map { (it as AmityUploadResult.COMPLETE).getFile() }
            .firstOrError()
    }
}

val UPLOAD_CANCELLED_EXCEPTION = Exception("Upload has been cancelled")

Now let’s go back to the CreatePostFragment and connect a function in viewModel to it. In the previous section, we’re able to choose an image and render the preview already, we will now pass that image URI to the ViewModel to create a new post.

class CreatePostFragment : Fragment() {

    private val viewModel: CreatePostViewModel by viewModels()
    private var binding: FragmentCreatePostBinding? = null

    private var imageUri: Uri? = null

    private val openDocumentResult =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult())
        { result: ActivityResult ->
            if (result.resultCode == Activity.RESULT_OK) {
                result.data?.data?.also { imageUri ->
                    requireActivity().contentResolver.takePersistableUriPermission(
                        imageUri,
                        Intent.FLAG_GRANT_READ_URI_PERMISSION
                    )
                    renderPreviewImage(imageUri)
                    this.imageUri = imageUri
                }
            }
        }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentCreatePostBinding.inflate(inflater, container, false)
        return binding?.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding?.attachImageTextview?.setOnClickListener { openDocumentPicker() }
        binding?.createPostButton?.setOnClickListener { createPost() }
    }

    private fun openDocumentPicker() {
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
            type = "image/*"
            addCategory(Intent.CATEGORY_OPENABLE)
        }
        openDocumentResult.launch(intent)
    }

    private fun renderPreviewImage(uri: Uri) {
        binding?.createPostImageview?.let {
            it.visibility = View.VISIBLE
            Glide.with(requireContext())
                .load(uri)
                .transition(DrawableTransitionOptions.withCrossFade())
                .into(it)
        }
    }

    private fun createPost() {
        showToast("Creating a post, please wait..")
        val postText = binding?.createPostEdittext?.text.toString()
        if (postText.isNotBlank() && imageUri != null) {
            viewModel.createImagePost(postText = postText,
                postImage = imageUri!!,
                onPostCreationError = {
                    showToast("Post creation error ${it.message}")
                },
                onPostCreationSuccess = {
                    findNavController().navigate(R.id.action_CreatePostFragment_to_ImageFeedFragment)
                })
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe()
        } else {
            showToast("Either text or image is empty")
        }
    }

    private fun showToast(message: String) {
        Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
    }
}

All set for the image post-creation screen! Let’s run the application and see how awesome it is.

Image post creation with viewModel

Comment List Screen

The structure of the screen is very similar to that of the image feed screen. Let’s break it down as much as possible.

  • Comment list container – The page that combines all components together (CommentListFragment)
  • Comment list ViewModel – The ViewModel class that stores and manages UI-related data in a  lifecycle (CommentListViewModel)
  • List of comments – The list that shows a collection of comments on the post (CommentAdapter)
  • Empty state  – The view when there are no comments on the post
  • Progress bar – The view when the comment is loading.

Create a Comment Item Layout

We’re again taking a bottom-up approach to creating the image feed screen, which means we’ll start with a RecyclerView holder layout first, and a comment list screen afterwards. The comment item layout will be almost the same as the image feed item layout: the header, which includes the commenter’s avatar and display name, as well as the time it was created, and secondly, the comment text, followed by the number of reactions.

<?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="wrap_content"
    android:orientation="vertical">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp">

        <com.google.android.material.imageview.ShapeableImageView
            android:id="@+id/avatar_image_view"
            style="@style/AmityCircularAvatarStyle"
            android:layout_width="42dp"
            android:layout_height="42dp"
            android:layout_marginStart="16dp"
            android:adjustViewBounds="true"
            android:scaleType="centerCrop"
            android:background="@color/light_grey"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" ></com>

        <LinearLayout
            android:id="@+id/layout_posted_by"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="12dp"
            android:layout_marginBottom="12dp"
            app:layout_constraintStart_toEndOf="@id/avatar_image_view"
            app:layout_constraintTop_toTopOf="@id/avatar_image_view">

            <TextView
                android:id="@+id/display_name_text_view"
                style="@style/TextAppearance.AppCompat.Body1"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:ellipsize="end"
                android:maxLines="2"
                android:textAlignment="textStart"
                android:textColor="@color/primary_grey"
                android:textStyle="bold"
                app:layout_constraintStart_toStartOf="parent"
                tools:text="Brian Marty" ></TextView>

        </LinearLayout>

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="12dp"
            android:gravity="center_vertical"
            android:orientation="horizontal"
            app:layout_constraintLeft_toRightOf="@id/avatar_image_view"
            app:layout_constraintTop_toBottomOf="@id/layout_posted_by">

            <TextView
                android:id="@+id/comment_time_text_view"
                style="@style/TextAppearance.Material3.BodyMedium"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="12dp"
                android:ellipsize="end"
                android:gravity="center_vertical"
                android:maxLines="1"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                tools:text="2 hours" ></TextView>

        </LinearLayout>

    </androidx.constraintlayout.widget.ConstraintLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp">



    </androidx.constraintlayout.widget.ConstraintLayout>


    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
  >

        <LinearLayout
            android:id="@+id/reaction_status_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            tools:ignore="MissingConstraints">

            <TextView
                android:id="@+id/description_textview"
                style="@style/TextAppearance.Material3.BodyMedium"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="18dp"
                android:textColor="@color/primary_grey"
                android:textStyle="bold"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                tools:text="My favourite place on earth" ></TextView>

        </LinearLayout>


        <LinearLayout
            android:id="@+id/post_action_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="4dp"
            android:layout_marginEnd="16dp"
            android:layout_marginStart="12dp"
            android:gravity="center_vertical"
            android:orientation="horizontal"
            app:layout_constraintTop_toBottomOf="@id/reaction_status_view">

            <TextView
                android:padding="8dp"
                android:id="@+id/like_count_textview"
                style="@style/TextAppearance.AppCompat.Body1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:drawablePadding="12dp"
                android:gravity="center_vertical"
                android:textColor="@color/primary_grey"
                app:drawableStartCompat="@drawable/ic_like_reaction"
                tools:text="12 likes" ></TextView>

        </LinearLayout>
        
        <RelativeLayout
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toBottomOf="@id/post_action_view"
            android:background="@color/light_grey"
            android:layout_width="match_parent"
            android:layout_height="1dp"></RelativeLayout>

    </androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

Comment item layout

Create a Comment List Screen Layout

As described in the previous section, we only need a RecyclerView to display a comment collection and a compose bar to write a new comment for the comment list screen’s layout.

<?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="@color/white">

    <androidx.recyclerview.widget.RecyclerView
        android:clipToPadding="false"
        android:paddingTop="56dp"
        android:layout_marginBottom="56dp"
        android:id="@+id/comment_recyclerview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" ></androidx>

    <LinearLayout
        android:id="@+id/empty_comment_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"
        android:visibility="visible">

        <TextView
            style="@style/TextAppearance.AppCompat.Body1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/amity_empty_comment"
            android:textColor="@color/black"
            android:textStyle="bold" ></TextView>

        <TextView
            style="@style/TextAppearance.AppCompat.Body1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="2dp"
            android:text="@string/amity_empty_comment_description"
            android:textColor="@color/black" ></TextView>

    </LinearLayout>


    <com.google.android.material.progressindicator.CircularProgressIndicator
        android:id="@+id/progress_bar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        android:layout_centerInParent="true"
        android:visibility="visible"
        ></com>


    <RelativeLayout
        android:background="@color/white"
        android:elevation="15dp"
        android:layout_alignParentBottom="true"
        android:layout_width="match_parent"
        android:paddingTop="4dp"
        android:paddingBottom="4dp"
        android:layout_height="wrap_content">

        <EditText
            android:hint="@string/amity_post_create_hint"
            android:id="@+id/comment_edit_text"
            android:layout_marginEnd="84dp"
            android:layout_centerVertical="true"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"></EditText>

        <TextView
            android:id="@+id/comment_create_text_view"
            android:textSize="18dp"
            android:layout_alignParentEnd="true"
            android:layout_centerVertical="true"
            android:textStyle="bold"
            android:layout_marginEnd="16dp"
            android:textColor="@color/purple_500"
            android:text="@string/amity_comment_create"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"></TextView>

    </RelativeLayout>


</RelativeLayout>

Create a Comment List ViewModel

The ViewModel must support the screen’s functions of getting a comment collection and creating a new comment. The two functions are handled by a comment repository provided by AmitySoicialClient.

class CommentListViewModel : ViewModel() {

    fun getComments(
        postId: String,
        onCommentListUpdated: (commentPagedList: PagedList<AmityComment>) -> Unit
    ): Completable {
        return AmitySocialClient.newCommentRepository()
            .getComments()
            .post(postId = postId)
            .includeDeleted(false)
            .sortBy(AmityCommentSortOption.LAST_CREATED)
            .build()
            .query()
            .doOnNext { onCommentListUpdated.invoke(it) }
            .ignoreElements()
            .subscribeOn(Schedulers.io())
    }

    fun createComment(
        postId: String,
        commentText: String,
        onCommentCreationSuccess: (AmityComment) -> Unit,
        onCommentCreationError: (throwable: Throwable) -> Unit
    ): Completable {
        return AmitySocialClient.newCommentRepository()
            .createComment()
            .post(postId = postId)
            .with()
            .text(text = commentText)
            .build()
            .send()
            .doOnSuccess { onCommentCreationSuccess.invoke(it) }
            .doOnError { onCommentCreationError.invoke(it) }
            .ignoreElement()
            .subscribeOn(Schedulers.io())
    }
}

Create a Comment List Adapter

We now know that the data return PagedList<AmityComment> model for us to render in the image post item after we created a ViewModel. To properly construct a RecyclerView, two components are required for creating an image feed adapter: PagedListAdapter and ViewHolder. We replicated the majority of the rendering logic from ImageFeedAdapter because they have nearly identical presentation perspectives.

ViewHolder:

class CommentViewHolder(private val binding: ListItemCommentBinding) :
    RecyclerView.ViewHolder(binding.root) {

    fun bind(comment: AmityComment?) {
        presentHeader(comment)
        presentContent(comment)
        presentFooter(comment)
    }

    private fun presentHeader(comment: AmityComment?) {
        //render commenter's avatar
        Glide.with(itemView)
            .load(comment?.getUser()?.getAvatar()?.getUrl(AmityImage.Size.SMALL))
            .transition(DrawableTransitionOptions.withCrossFade())
            .into(binding.avatarImageView)
        //render commenter's display name
        binding.displayNameTextView.text = comment?.getUser()?.getDisplayName() ?: "Unknown user"
        //render commented time
        binding.commentTimeTextView.text =
            comment?.getCreatedAt()?.millis?.readableFeedPostTime(itemView.context) ?: ""
    }


    private fun presentContent(comment: AmityComment?) {
        comment?.getData()
        //make sure that the comment contains text data
        if (comment?.getData() is AmityComment.Data.TEXT) {
            val commentText = (comment.getData() as AmityComment.Data.TEXT).getText()
            binding.descriptionTextview.text = commentText
        }
    }

    private fun presentFooter(comment: AmityComment?) {
        //render like count
        binding.likeCountTextview.text = getLikeCountString(comment?.getReactionCount() ?: 0)

        val isLikedByMe = comment?.getMyReactions()?.contains("like") == true
        val context = binding.root.context
        val highlightedColor = ContextCompat.getColor(context, R.color.teal_700)
        val inactiveColor = ContextCompat.getColor(context, R.color.dark_grey)
        if (isLikedByMe) {
            //present highlighted color if the comment is liked by me
            setLikeTextViewDrawableColor(highlightedColor)
        } else {
            //present inactive color if the comment isn't liked by me
            setLikeTextViewDrawableColor(inactiveColor)
        }
        //add or remove a like reaction when clicking like textview
        binding.likeCountTextview.setOnClickListener {
            if (isLikedByMe) {
                comment?.react()?.removeReaction("like")?.subscribe()
            } else {
                comment?.react()?.addReaction("like")?.subscribe()
            }
        }
    }

    private fun setLikeTextViewDrawableColor(@ColorInt color: Int) {
        for (drawable in binding.likeCountTextview.compoundDrawablesRelative) {
            if (drawable != null) {
                drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
            }
        }
        binding.likeCountTextview.setTextColor(color)
    }

    private fun getLikeCountString(likeCount: Int): String {
        return itemView.context.resources.getQuantityString(
            R.plurals.amity_number_of_likes,
            likeCount,
            likeCount
        )
    }
}

PagedListAdapter:

class CommentAdapter :
    PagedListAdapter<AmityComment, CommentViewHolder>(CommentDiffCallback()) {

    override fun onBindViewHolder(holder: CommentViewHolder, position: Int) {
        holder.bind(getItem(position))
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommentViewHolder {
        return CommentViewHolder(
            ListItemCommentBinding.inflate(
                LayoutInflater.from(parent.context),
                parent,
                false
            )
        )
    }
}


private class CommentDiffCallback : DiffUtil.ItemCallback<AmityComment>() {

    override fun areItemsTheSame(oldItem: AmityComment, newItem: AmityComment): Boolean {
        return oldItem.getCommentId() == newItem.getCommentId()
    }

    override fun areContentsTheSame(oldItem: AmityComment, newItem: AmityComment): Boolean {
        return oldItem.getCommentId() == newItem.getCommentId()
                && oldItem.getUpdatedAt() == newItem.getUpdatedAt()
                && oldItem.getReactionCount() == newItem.getReactionCount()
    }
}

Create a Comment List Fragment

Pheww! It will be the last time we put all of our efforts together. A ViewModel, an adapter, and layouts have all been created. Everything will be condensed into a fragment. The ViewModel will interact with the fragment, and the view states will be updated. We also have to deal with the comments that are loading, loaded, and empty states.

class CommentListFragment : Fragment() {

    private lateinit var adapter: CommentAdapter
    private val viewModel: CommentListViewModel by viewModels()
    private var binding: FragmentCommentListBinding? = null

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentCommentListBinding.inflate(inflater, container, false)
        return binding?.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        setupRecyclerView()
        getComments()
        binding?.commentCreateTextView?.setOnClickListener { createComment() }
    }

    private fun setupRecyclerView() {
        adapter = CommentAdapter()
        binding?.commentRecyclerview?.layoutManager = LinearLayoutManager(context)
        binding?.commentRecyclerview?.adapter = adapter
    }

    private fun getComments() {
        val postId = arguments?.getString("postId")
        postId?.let {
            viewModel.getComments(postId = postId) {
                lifecycleScope.launch {
                    handleEmptyState(it.size)
                    adapter.submitList(it)
                }
            }
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe()
        }
    }

    private fun createComment() {
        showToast("Creating comment.. please wait")
        val postId = arguments?.getString("postId")
        val commentText = binding?.commentEditText?.text ?: ""
        if (commentText.isNotBlank() && postId != null) {
            viewModel.createComment(postId = postId, commentText = commentText.toString(),
                onCommentCreationSuccess = {
                    binding?.commentEditText?.setText("")
                    showToast("Comment was created successfully")
                },
                onCommentCreationError = {
                    binding?.commentEditText?.setText("")
                    showToast("Comment error : ${it.message}")
                })
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe()
        } else {
            showToast("Comment error : postId or comment is empty")
        }
    }

    private fun handleEmptyState(itemCount: Int) {
        binding?.progressBar?.visibility = View.GONE
        binding?.emptyCommentView?.visibility = if (itemCount > 0) View.GONE else View.VISIBLE
    }

    private fun showToast(message: String) {
        Snackbar.make(binding!!.root, message, LENGTH_SHORT).show()
    }
}

The end result of the tutorial: an Android image feed app

Yessssss, our app is now completed! we hope you enjoyed this tutorial. If you have any obstacles while implementing this tutorial, feel free to explore the code from this repository.



Source link

ShareSendTweet
Previous Post

OnePlus 11 and Buds Pro 2 global launch set to take place on February 7

Next Post

The best Game Pass additions of 2022: March

Related Posts

REST vs. Messaging for Microservices

March 25, 2023
0
0
REST vs. Messaging for Microservices
Software Development

This is an article from DZone's 2023 Software Integration Trend Report.For more: Read the Report A microservices architecture is an...

Read more

Matter vs. Thread – A Head-to-Head Comparison!

March 25, 2023
0
0
Matter vs. Thread – A Head-to-Head Comparison!
Software Development

Modern technology has made communication easier than ever before. From smartphones to other smart devices, we can use a single...

Read more
Next Post
The best Game Pass additions of 2022: March

The best Game Pass additions of 2022: March

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

© 2021 GetUpdated – MW.

  • About
  • Advertise
  • Privacy & Policy
  • Terms & Conditions
  • Contact

No Result
View All Result
  • Game Updates
  • Mobile Gaming
  • Playstation News
  • Xbox News
  • Switch News
  • MMORPG
  • Game News
  • IGN
  • Retro Gaming
  • Tech News
  • Apple Updates
  • Jailbreak News
  • Mobile News
  • Software Development
  • Photography
  • Contact

Welcome Back!

Login to your account below

Forgotten Password? Sign Up

Create New Account!

Fill the forms bellow to register

All fields are required. Log In

Retrieve your password

Please enter your username or email address to reset your password.

Log In
Are you sure want to unlock this post?
Unlock left : 0
Are you sure want to cancel subscription?