Skip to content

Instantly share code, notes, and snippets.

@haerulmuttaqin
Created March 17, 2021 23:20
Show Gist options
  • Save haerulmuttaqin/e5d3c4d7b61707656efc10c49ed2d585 to your computer and use it in GitHub Desktop.
Save haerulmuttaqin/e5d3c4d7b61707656efc10c49ed2d585 to your computer and use it in GitHub Desktop.
#3 — Job Finder App (https://youtu.be/CINkzqxJtro)
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewModel"
type="id.haerulmuttaqin.jobfinder.ui.MainViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/blue_200">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:elevation="0dp">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapse"
android:layout_width="match_parent"
android:layout_height="280dp"
app:collapsedTitleTextAppearance="@style/collapsedTitleStyle"
app:contentScrim="?attr/colorPrimary"
app:expandedTitleMarginBottom="26dp"
app:expandedTitleMarginStart="26dp"
app:expandedTitleTextAppearance="@style/expandedTitleStyle"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
app:title="@string/app_name"
app:titleEnabled="true">
<RelativeLayout
app:layout_collapseMode="parallax"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/banner"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/banner_1" />
<TextView
android:id="@+id/welcomeText"
android:text="Hi, Welcome back"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="26dp"
android:layout_marginTop="40dp"
android:layout_marginEnd="10dp"
android:fontFamily="@font/book"
android:textSize="16sp"
android:textColor="@color/white"
android:layout_toStartOf="@id/notif"/>
<TextView
android:id="@+id/secondaryText"
android:text="Find your perfect job"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/welcomeText"
android:layout_marginStart="26dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="10dp"
android:layout_toStartOf="@id/notif"
android:textSize="26sp"
android:textColor="@color/white"
android:fontFamily="@font/bold" />
<ImageView
android:id="@+id/notif"
android:layout_width="26dp"
android:layout_height="40dp"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="42dp"
android:layout_marginEnd="16dp"
android:contentDescription="@string/app_name"
android:src="@drawable/ic_baseline_more_vert_24"
android:paddingBottom="10dp"
app:tint="@color/white"/>
<LinearLayout
android:id="@+id/searchLayout"
android:orientation="vertical"
android:layout_alignParentBottom="true"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginBottom="60dp"
app:strokeWidth="0.3dp"
app:cardCornerRadius="20dp"
app:cardElevation="0dp"
app:strokeColor="@color/blue_300"
app:cardBackgroundColor="@color/blue_250">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="60dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="What are you looking for?"
android:singleLine="true"
android:layout_centerVertical="true"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:textColor="@color/blue_050"
android:fontFamily="@font/book"
android:textSize="16dp"/>
<ImageView
android:src="@drawable/ic_round_search_24"
android:layout_width="25dp"
android:layout_height="60dp"
android:layout_alignParentEnd="true"
android:layout_marginEnd="16dp"
app:tint="@color/blue_050"/>
</RelativeLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</RelativeLayout>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:theme="@style/Theme.JobFinder.Toolbar"
android:fitsSystemWindows="true"
app:background="@android:color/transparent"
app:layout_collapseMode="pin"
app:popupTheme="@style/Theme.JobFinder.PopupOverlay"
app:titleTextColor="@color/white" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_rounded_top"
android:fillViewport="true"
android:fitsSystemWindows="true"
app:layout_anchor="@id/appbar"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:fillViewport="true">
<RelativeLayout
android:id="@+id/contentLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/markedTitle"
android:text="Marked Job"
android:textSize="16dp"
android:textColor="@color/gray_600"
android:layout_marginStart="21dp"
android:layout_marginEnd="21dp"
android:layout_marginTop="25dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/medium"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewMarked"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout_marginBottom="10dp"
android:layout_below="@id/markedTitle"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:nestedScrollingEnabled="true"
android:orientation="horizontal"/>
<TextView
android:id="@+id/recommendedTitle"
android:text="Recommended Job"
android:textSize="16dp"
android:textColor="@color/gray_600"
android:layout_marginStart="21dp"
android:layout_marginEnd="21dp"
android:layout_marginTop="25dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/recyclerViewMarked"
android:fontFamily="@font/medium"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:layout_marginBottom="10dp"
android:layout_below="@id/recommendedTitle"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</RelativeLayout>
</androidx.core.widget.NestedScrollView>
<com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/shimmer"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_marginTop="16dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:shimmer_duration="800"
app:shimmer_auto_start="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include layout="@layout/item_job_placeholder" />
<include layout="@layout/item_job_placeholder" />
<include layout="@layout/item_job_placeholder" />
<include layout="@layout/item_job_placeholder" />
<include layout="@layout/item_job_placeholder" />
<include layout="@layout/item_job_placeholder" />
<include layout="@layout/item_job_placeholder" />
<include layout="@layout/item_job_placeholder" />
<include layout="@layout/item_job_placeholder" />
</LinearLayout>
</com.facebook.shimmer.ShimmerFrameLayout>
<LinearLayout
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fillViewport="true"
android:clickable="false"
android:focusable="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:layout_width="250dp"
android:layout_height="250dp"
android:layout_gravity="center"
android:layout_marginTop="100dp"
android:src="@drawable/ic_undraw_empty" />
<TextView
android:id="@+id/textEmpty"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:text="No Data Found"
android:fontFamily="@font/medium"/>
<TextView
android:id="@+id/textEmptyErr"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingStart="16dp"
android:paddingBottom="16dp"
android:paddingEnd="16dp"
android:fontFamily="@font/book"/>
</LinearLayout>
<ImageView
android:id="@+id/shadow"
android:layout_width="match_parent"
android:layout_height="35dp"
android:layout_marginTop="16dp"
android:src="@drawable/bg_shadow_up_to_down"
android:visibility="visible" />
<ImageView
android:background="@drawable/bg_strip_round"
android:layout_width="40dp"
android:layout_height="5dp"
android:layout_marginTop="8dp"
android:layout_centerHorizontal="true"/>
</RelativeLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
import java.util.concurrent.TimeUnit;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import id.haerulmuttaqin.jobfinder.App;
import id.haerulmuttaqin.jobfinder.Constants;
import id.haerulmuttaqin.jobfinder.data.api.ApiInterface;
import id.haerulmuttaqin.jobfinder.data.api.ConnectionServer;
import id.haerulmuttaqin.jobfinder.data.storage.GithubJobRepository;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
@Module
public class AppModule {
@Provides @Singleton
ConnectionServer provideConnectionServer(ApiInterface apiInterface) {
return new ConnectionServer(apiInterface);
}
@Provides @Singleton
Interceptor provideLoggingInterceptor() {
HttpLoggingInterceptor localHttpLoggingInterceptor = new HttpLoggingInterceptor();
localHttpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
return localHttpLoggingInterceptor;
}
@Provides @Singleton
OkHttpClient provideOkHttp() {
return new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(chain -> {
Request original = chain.request();
Request.Builder builder = chain.request().newBuilder();
builder.addHeader(Constants.CONTENT_TYPE, Constants.APP_JSON);
builder.method(original.method(), original.body());
return chain.proceed(builder.build());
})
.addNetworkInterceptor(provideLoggingInterceptor())
.build();
}
@Provides @Singleton
Retrofit provideRetrofitClient() {
return new Retrofit.Builder().baseUrl(Constants.BASE_URL)
.client(provideOkHttp())
.addConverterFactory(GsonConverterFactory.create())
.build();
}
@Provides @Singleton
ApiInterface provideApiInterface(Retrofit retrofit) {
return retrofit.create(ApiInterface.class);
}
@Provides @Singleton
GithubJobRepository provideRepository(App application) {
return GithubJobRepository.getInstance(application);
}
}
import androidx.annotation.NonNull;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
@Entity
public class GithubJob {
@PrimaryKey
@SerializedName("id")
@Expose
@NonNull
public String id;
@SerializedName("type")
@Expose
public String type;
@SerializedName("url")
@Expose
public String url;
@SerializedName("created_at")
@Expose
public String createdAt;
@SerializedName("company")
@Expose
public String company;
@SerializedName("company_url")
@Expose
public String companyUrl;
@SerializedName("location")
@Expose
public String location;
@SerializedName("title")
@Expose
public String title;
@SerializedName("description")
@Expose
public String description;
@SerializedName("how_to_apply")
@Expose
public String howToApply;
@SerializedName("company_logo")
@Expose
public String companyLogo;
public int is_mark;
}
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import java.util.List;
import id.haerulmuttaqin.jobfinder.data.entity.GithubJob;
@Dao
public interface GithubJobDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insert(GithubJob githubJob);
@Query("SELECT * FROM githubjob ORDER BY createdAt DESC LIMIT 10")
LiveData<List<GithubJob>> getLiveData();
@Query("SELECT * FROM githubjob where is_mark = 1 ORDER BY createdAt DESC")
LiveData<List<GithubJob>> getLiveDataMarked();
@Query("SELECT * FROM githubjob where id = :id")
GithubJob getDataById(String id);
}
import android.content.Context;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import id.haerulmuttaqin.jobfinder.Constants;
import id.haerulmuttaqin.jobfinder.data.entity.GithubJob;
@Database(entities = GithubJob.class, version = 1)
public abstract class GithubJobDatabase extends RoomDatabase {
public abstract GithubJobDao githubJobDao();
public static Context context;
private static GithubJobDatabase instance;
public static GithubJobDatabase getDatabase(Context ctx) {
context = ctx;
if (instance == null) {
synchronized (GithubJobDatabase.class) {
if (instance == null) {
instance = Room.databaseBuilder(context, GithubJobDatabase.class, Constants.MASTER_DB)
.allowMainThreadQueries()
.setJournalMode(JournalMode.TRUNCATE)
.build();
}
}
}
return instance;
}
}
import android.content.Context;
import androidx.lifecycle.LiveData;
import java.util.List;
import id.haerulmuttaqin.jobfinder.data.entity.GithubJob;
public class GithubJobRepository {
private static GithubJobDatabase database;
private static GithubJobRepository repository;
public GithubJobRepository(Context context) {
database = GithubJobDatabase.getDatabase(context);
}
public static GithubJobRepository getInstance(Context context) {
if (repository == null) {
repository = new GithubJobRepository(context);
}
return repository;
}
public void updateMarkJob(GithubJob githubJob) {
githubJob.is_mark = githubJob.is_mark == 1 ? 0 : 1;
database.githubJobDao().insert(githubJob);
}
public void insert(GithubJob githubJob) {
if (database.githubJobDao().getDataById(githubJob.id) == null) {
database.githubJobDao().insert(githubJob);
}
}
public LiveData<List<GithubJob>> getLiveData() {
return database.githubJobDao().getLiveData();
}
public LiveData<List<GithubJob>> getLiveDataMarked() {
return database.githubJobDao().getLiveDataMarked();
}
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="item"
type="id.haerulmuttaqin.jobfinder.data.entity.GithubJob" />
<variable
name="viewModel"
type="id.haerulmuttaqin.jobfinder.ui.MainViewModel" />
</data>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_marginTop="8dp"
app:cardCornerRadius="10dp"
app:cardElevation="0dp"
app:strokeColor="#f1f1f1"
app:strokeWidth="1dp">
<LinearLayout
android:orientation="vertical"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="9dp"
android:padding="4dp">
<com.google.android.material.card.MaterialCardView
android:id="@+id/photoPreviewLayout"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginStart="6dp"
android:layout_marginTop="6dp"
android:src="@drawable/ic_photo"
app:strokeWidth="0.5dp"
app:strokeColor="#f1f1f1"
app:cardElevation="0dp"
app:cardCornerRadius="8dp">
<ImageView
android:id="@+id/photoPreview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerInside"
android:layout_margin="5dp"
tools:ignore="UnusedAttribute" />
<com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/progress"
android:layout_width="60dp"
android:layout_height="60dp"
android:orientation="vertical"
app:shimmer_auto_start="true"
app:shimmer_duration="800">
<View
android:layout_width="60dp"
android:layout_height="60dp"
android:gravity="center"
android:layout_gravity="center"
android:background="@color/gray_100" />
</com.facebook.shimmer.ShimmerFrameLayout>
</com.google.android.material.card.MaterialCardView>
<LinearLayout
android:layout_toEndOf="@id/photoPreviewLayout"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp">
<TextView
android:id="@+id/title"
android:ellipsize="end"
android:maxLines="2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="36dp"
android:text="@{item.title}"
android:textColor="@color/gray_500"
android:textSize="18sp"
android:layout_marginBottom="3dp"
android:fontFamily="@font/bold"/>
<RelativeLayout
android:layout_marginStart="14dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/lampIcon"
android:src="@drawable/ic_round_business_24"
app:tint="@color/gray_300"
android:layout_marginEnd="5dp"
android:layout_width="15dp"
android:layout_height="16sp"/>
<TextView
android:layout_toEndOf="@id/lampIcon"
android:id="@+id/lamp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/gray_400"
android:text="@{item.company}"
android:textSize="12sp"
android:singleLine="true"
android:ellipsize="end"
android:fontFamily="@font/book"/>
</RelativeLayout>
<RelativeLayout
android:layout_marginStart="14dp"
android:layout_marginTop="3dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/locIcon"
android:src="@drawable/ic_outline_location_on_24"
app:tint="@color/gray_300"
android:layout_marginEnd="5dp"
android:layout_width="15dp"
android:layout_height="16sp"/>
<TextView
android:layout_toEndOf="@id/locIcon"
android:id="@+id/address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/gray_400"
android:text="@{item.location}"
android:textSize="12sp"
android:singleLine="true"
android:ellipsize="end"
android:fontFamily="@font/book"/>
</RelativeLayout>
<RelativeLayout
android:layout_marginStart="14dp"
android:layout_marginTop="3dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:paddingStart="6dp"
android:paddingEnd="6dp"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:background="@drawable/bg_job_type"
android:textColor="@color/green_300"
android:text="@{item.type}"
android:textSize="10sp"
android:fontFamily="@font/book"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="6dp"
android:paddingStart="6dp"
android:paddingEnd="6dp"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:layout_alignParentEnd="true"
android:textColor="@color/gray_300"
android:text="@{viewModel.formatDate(item.createdAt)}"
android:textSize="11sp"
android:fontFamily="@font/book"/>
</RelativeLayout>
</LinearLayout>
<ImageView
android:onClick="@{()->viewModel.markJob(item)}"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:id="@+id/mark"
android:layout_width="30dp"
android:layout_height="40dp"
android:src="@{item.is_mark == 1 ? @drawable/ic_marked : @drawable/ic_mark}"
android:layout_alignParentEnd="true"
android:padding="3dp"
app:tint="@color/gray_200"/>
</RelativeLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="item"
type="id.haerulmuttaqin.jobfinder.data.entity.GithubJob" />
<variable
name="viewModel"
type="id.haerulmuttaqin.jobfinder.ui.MainViewModel" />
</data>
<com.google.android.material.card.MaterialCardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:layout_marginTop="8dp"
app:cardCornerRadius="10dp"
app:cardElevation="0dp"
app:strokeColor="#f1f1f1"
app:strokeWidth="1dp">
<LinearLayout
android:orientation="vertical"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
android:layout_width="200dp"
android:layout_height="wrap_content">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="9dp"
android:padding="4dp">
<com.google.android.material.card.MaterialCardView
android:id="@+id/photoPreviewLayout"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginStart="14dp"
android:layout_marginTop="6dp"
android:src="@drawable/ic_photo"
app:strokeWidth="0.5dp"
app:strokeColor="#f1f1f1"
app:cardElevation="0dp"
app:cardCornerRadius="8dp">
<ImageView
android:id="@+id/photoPreview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerInside"
android:layout_margin="5dp"
tools:ignore="UnusedAttribute" />
<com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/progress"
android:layout_width="60dp"
android:layout_height="60dp"
android:orientation="vertical"
app:shimmer_auto_start="true"
app:shimmer_duration="800">
<View
android:layout_width="60dp"
android:layout_height="60dp"
android:gravity="center"
android:layout_gravity="center"
android:background="@color/gray_100" />
</com.facebook.shimmer.ShimmerFrameLayout>
</com.google.android.material.card.MaterialCardView>
<LinearLayout
android:layout_below="@id/photoPreviewLayout"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp">
<TextView
android:id="@+id/title"
android:ellipsize="end"
android:maxLines="2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="@{item.title}"
android:textColor="@color/gray_500"
android:textSize="18sp"
android:layout_marginBottom="3dp"
android:fontFamily="@font/bold"/>
<TextView
android:id="@+id/lamp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:textColor="@color/gray_400"
android:text="@{item.company}"
android:textSize="12sp"
android:singleLine="true"
android:ellipsize="end"
android:fontFamily="@font/book"/>
</LinearLayout>
<ImageView
android:onClick="@{()->viewModel.markJob(item)}"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:id="@+id/mark"
android:layout_width="30dp"
android:layout_height="40dp"
android:src="@{item.is_mark == 1 ? @drawable/ic_marked : @drawable/ic_mark}"
android:layout_alignParentEnd="true"
android:padding="3dp"
app:tint="@color/gray_200"/>
</RelativeLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</layout>
import android.os.Bundle;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.snackbar.Snackbar;
import javax.inject.Inject;
import id.haerulmuttaqin.jobfinder.R;
import id.haerulmuttaqin.jobfinder.base.BaseActivity;
import id.haerulmuttaqin.jobfinder.data.api.ConnectionServer;
import id.haerulmuttaqin.jobfinder.data.storage.GithubJobRepository;
import id.haerulmuttaqin.jobfinder.databinding.ActivityMainBinding;
public class MainActivity extends BaseActivity<ActivityMainBinding, MainViewModel> implements MainViewModel.Navigator {
@Inject ConnectionServer server;
@Inject GithubJobRepository repository;
private ActivityMainBinding binding;
private MainViewModel viewModel;
@Override
public int getBindingVariable() {
return 0;
}
@Override
public int getLayoutId() {
return R.layout.activity_main;
}
@Override
public MainViewModel getViewModel() {
return viewModel;
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = getViewDataBinding();
viewModel = new ViewModelProvider(this, new MainViewModel.ModelFactory(this, server, repository)).get(MainViewModel.class);
viewModel.setNavigator(this);
viewModel.getJobFromServer();
viewModel.getLiveData().observe(this, githubJobs -> {
binding.recyclerView.setAdapter(new MainAdapter(githubJobs, viewModel));
});
viewModel.getLiveDataMarked().observe(this, githubJobs -> {
if (githubJobs.size() > 0) {
binding.markedTitle.setVisibility(View.VISIBLE);
binding.recyclerViewMarked.setVisibility(View.VISIBLE);
binding.recyclerViewMarked.setAdapter(new MainMarkedAdapter(githubJobs, viewModel));
} else {
binding.markedTitle.setVisibility(View.GONE);
binding.recyclerViewMarked.setVisibility(View.GONE);
}
});
binding.swipeRefresh.setOnRefreshListener(()->viewModel.getJobFromServer());
}
@Override
public void showProgress() {
binding.swipeRefresh.setRefreshing(true);
binding.emptyView.setVisibility(View.GONE);
binding.shimmer.setVisibility(View.VISIBLE);
binding.shimmer.startShimmer();
binding.contentLayout.setVisibility(View.GONE);
}
@Override
public void hideProgress() {
binding.swipeRefresh.setRefreshing(false);
binding.emptyView.setVisibility(View.GONE);
binding.shimmer.setVisibility(View.GONE);
binding.shimmer.stopShimmer();
binding.contentLayout.setVisibility(View.VISIBLE);
}
@Override
public void onGetResult(boolean status, String message) {
if (!status) { //<-- status result is FALSE
binding.textEmptyErr.setText(message);
binding.emptyView.setVisibility(View.VISIBLE);
} else {
binding.emptyView.setVisibility(View.GONE);
}
}
@Override
public void onMark(int mark, String title) {
Snackbar.make(binding.getRoot(), mark == 0 ? "\uD83D\uDE13 Unmark " + title : "\uD83D\uDE0D Marked " + title, Snackbar.LENGTH_SHORT).show();
}
}
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.databinding.DataBindingUtil;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import java.util.List;
import id.haerulmuttaqin.jobfinder.R;
import id.haerulmuttaqin.jobfinder.data.entity.GithubJob;
import id.haerulmuttaqin.jobfinder.databinding.ItemJobBinding;
public class MainAdapter extends RecyclerView.Adapter<MainAdapter.RecyclerViewAdapter> {
private List<GithubJob> data;
private MainViewModel viewModel;
protected MainAdapter(List<GithubJob> data, MainViewModel viewModel) {
this.data = data;
this.viewModel = viewModel;
}
@NonNull
@Override
public RecyclerViewAdapter onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
ItemJobBinding binding = DataBindingUtil.inflate(inflater, R.layout.item_job, parent, false);
return new RecyclerViewAdapter(binding);
}
public void clear() {
int size = data.size();
data.clear();
notifyItemRangeRemoved(0, size);
notifyDataSetChanged();
}
@Override
public void onBindViewHolder(RecyclerViewAdapter holder, int position) {
GithubJob items = data.get(position);
if(items != null) {
holder.bind(items, viewModel);
}
}
@Override
public int getItemCount() {
return data == null ? 0 : data.size();
}
public static class RecyclerViewAdapter extends RecyclerView.ViewHolder {
ItemJobBinding binding;
public RecyclerViewAdapter(ItemJobBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
@SuppressLint("UseCompatLoadingForDrawables")
void bind(@NonNull GithubJob data, MainViewModel viewModel) {
Glide.with(binding.photoPreview.getContext())
.load(data.companyLogo)
.error(binding.photoPreview.getContext().getDrawable(R.drawable.ic_round_business_center_24))
.transition(DrawableTransitionOptions.withCrossFade())
.listener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
binding.progress.stopShimmer();
binding.progress.setVisibility(View.GONE);
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
binding.progress.stopShimmer();
binding.progress.setVisibility(View.GONE);
return false;
}
})
.into(binding.photoPreview);
binding.setItem(data);
binding.setViewModel(viewModel);
binding.executePendingBindings();
}
}
}
import android.annotation.SuppressLint;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.databinding.DataBindingUtil;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import java.util.List;
import id.haerulmuttaqin.jobfinder.R;
import id.haerulmuttaqin.jobfinder.data.entity.GithubJob;
import id.haerulmuttaqin.jobfinder.databinding.ItemJobMarkedBinding;
public class MainMarkedAdapter extends RecyclerView.Adapter<MainMarkedAdapter.RecyclerViewAdapter> {
private List<GithubJob> data;
private MainViewModel viewModel;
protected MainMarkedAdapter(List<GithubJob> data, MainViewModel viewModel) {
this.data = data;
this.viewModel = viewModel;
}
@NonNull
@Override
public RecyclerViewAdapter onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
ItemJobMarkedBinding binding = DataBindingUtil.inflate(inflater, R.layout.item_job_marked, parent, false);
return new RecyclerViewAdapter(binding);
}
public void clear() {
int size = data.size();
data.clear();
notifyItemRangeRemoved(0, size);
notifyDataSetChanged();
}
@Override
public void onBindViewHolder(RecyclerViewAdapter holder, int position) {
GithubJob items = data.get(position);
if(items != null) {
holder.bind(items, viewModel);
}
}
@Override
public int getItemCount() {
return data == null ? 0 : data.size();
}
public static class RecyclerViewAdapter extends RecyclerView.ViewHolder {
ItemJobMarkedBinding binding;
public RecyclerViewAdapter(ItemJobMarkedBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
@SuppressLint("UseCompatLoadingForDrawables")
void bind(@NonNull GithubJob data, MainViewModel viewModel) {
Glide.with(binding.photoPreview.getContext())
.load(data.companyLogo)
.error(binding.photoPreview.getContext().getDrawable(R.drawable.ic_round_business_center_24))
.transition(DrawableTransitionOptions.withCrossFade())
.listener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
binding.progress.stopShimmer();
binding.progress.setVisibility(View.GONE);
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
binding.progress.stopShimmer();
binding.progress.setVisibility(View.GONE);
return false;
}
})
.into(binding.photoPreview);
binding.setItem(data);
binding.setViewModel(viewModel);
binding.executePendingBindings();
}
}
}
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import java.util.List;
import id.haerulmuttaqin.jobfinder.Utils;
import id.haerulmuttaqin.jobfinder.base.BaseViewModel;
import id.haerulmuttaqin.jobfinder.data.api.ConnectionServer;
import id.haerulmuttaqin.jobfinder.data.entity.GithubJob;
import id.haerulmuttaqin.jobfinder.data.storage.GithubJobRepository;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class MainViewModel extends BaseViewModel<MainViewModel.Navigator> {
MutableLiveData<List<GithubJob>> jobList = new MutableLiveData<>();
public MainViewModel(Context context, ConnectionServer connectionServer, GithubJobRepository repository) {
super(context, connectionServer, repository);
}
public LiveData<List<GithubJob>> getLiveData() {
return getRepository().getLiveData();
}
public LiveData<List<GithubJob>> getLiveDataMarked() {
return getRepository().getLiveDataMarked();
}
public String formatDate(String date){
return Utils.dateToTimeFormat(date);
}
public void getJobFromServer() {
getNavigator().showProgress();
getConnectionServer().getJobList().enqueue(new Callback<List<GithubJob>>() {
@Override
public void onResponse(Call<List<GithubJob>> call, Response<List<GithubJob>> response) {
if (response.isSuccessful() && response.body().size() > 0) {
for (GithubJob item : response.body()) {
item.createdAt = Utils.dateFormatter(item.createdAt); // format date
getRepository().insert(item);
}
getNavigator().onGetResult(true, "Success");
}
getNavigator().hideProgress();
}
@Override
public void onFailure(Call<List<GithubJob>> call, Throwable t) {
t.getLocalizedMessage();
getNavigator().hideProgress();
getNavigator().onGetResult(false, Utils.errorMessageHandler(call, t));
}
});
}
public void markJob(GithubJob githubJob) {
getRepository().updateMarkJob(githubJob);
getNavigator().onMark(githubJob.is_mark, githubJob.title);
}
public static class ModelFactory implements ViewModelProvider.Factory {
private Context context;
private ConnectionServer server;
private GithubJobRepository repository;
public ModelFactory(Context context, ConnectionServer server, GithubJobRepository repository) {
this.context = context;
this.server = server;
this.repository = repository;
}
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
return (T) new MainViewModel(context, server, repository);
}
}
interface Navigator {
void showProgress();
void hideProgress();
void onGetResult(boolean status, String message);
void onMark(int mark, String title);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment