Professional Documents
Culture Documents
3.1.1. View
View
GroupView Control
Trong Android, giao diện người dùng được tạo bằng cách sử dụng các thành phần
gọi là View. Hệ thống Android có một cấu trúc phân cấp để sắp xếp các màn hình. Mỗi
màn hình (screen) bao gồm các bố cục (Layout) và các phần tử giao diện (widget) được
sắp xếp theo một trình tự cụ thể. Để hiển thị một màn hình, trong phương thức
onCreate của mỗi Activity, chúng ta cần gọi phương thức
setContentView(R.layout.main). Phương thức này sẽ tải giao diện từ tệp main.xml và
biên dịch nó thành mã bytecode để hiển thị trên màn hình thiết bị Android.
3.1.2. VIEWGROUP:
ViewGroup là sự mở rộng của class View hay nói cách khác ViewGroup chính là
các WidgetLayout được dùng để bố trí các đối tượng khác trong một screen.Có các loại
ViewGroup như sau:
- Linear Layout
LinearLayout được dùng để bố trí các thành phần giao diện theo chiều ngang hoặc
chiều dọc nhưng trên một line duy nhất mà không có xuống dòng.
fragment_home.xml
- Frame layout
FrameLayout được dùng để bố trí các đối tượng theo kiểu giống như làcác
Layer trong Photoshop. Những đối tượng nào thuộc Layer bên dưới thì sẽ bị che khuất
bởi các đối tượng thuộc Layer nằm trên. FrameLayer thường được sử dụng khi muốn tạo
ra các đối tượng có khung hình bên ngoài chẳng hạn như contact image button.
Đây là cách làm ra một button đơn giản. Đoạn mã trên sau khi chạy:
});
});
3.2.1. Navigation
Fragment là một phần tử giao diện hoặc hành vi có tính module và có thể tái sử
dụng, có thể kết hợp với các Fragment khác trong một Activity để tạo giao diện đa bảng.
Fragment được sử dụng để đại diện cho một phần của giao diện người dùng và thường
được sử dụng để xây dựng bố cục linh hoạt và đáp ứng cho cả điện thoại và máy tính
bảng.
Sử dụng câu lệnh <fracment /> để gọi các đối tượng như sau:
activity_main.xml
Ví dụ: Thử tạo các phần tử có hành vi liên quan đến các fragment khác như: Home,
Saved, Notification
carouselList.add("https://encrypted-tbn0.gstatic.com/images?
q=tbn:ANd9GcRL3s2YxzGwlKiBhyfg_N6HCS2BeurPm9ySzpXisRjvE0N8kZGcb4UvJSTlSTJvjCFy
fZ0&usqp=CAU");
carouselList.add("https://media.istockphoto.com/id/505266386/photo/
vibrant-sunrise-at-buttermere-with-reflections-and-snow-on-mountains.jpg?
s=612x612&w=0&k=20&c=MDDSSjCvRrGHRSQ_x-lXcR2_stsGT65Mz6Th5-BSfow=");
carouselList.add("https://media.istockphoto.com/id/516415148/photo/
dawn-at-buttermere-with-dramatic-clouds-and-lone-tree.jpg?
s=612x612&w=0&k=20&c=Tq1ztNX0LJSrGWo--iykXKHoU6pSQALBBXz1ZvwGYQk=");
carouselList.add("https://media.istockphoto.com/id/516415148/photo/
dawn-at-buttermere-with-dramatic-clouds-and-lone-tree.jpg?
s=612x612&w=0&k=20&c=Tq1ztNX0LJSrGWo--iykXKHoU6pSQALBBXz1ZvwGYQk=");
CarouselAdapter carouselAdapter = new
CarouselAdapter(carouselList,root.getContext());
carouselRecyclerView.setAdapter(carouselAdapter);
firebase.getCategory(recyclerView,categoryList);
firebase.getNews();
@Override
public void onResume() {
super.onResume();
adapter.notifyDataSetChanged();
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}
public HomeViewModel() {
listNewsItemLiveData = new MutableLiveData<>();
mText = new MutableLiveData<>();
mText.setValue("Nothing Here!!");
}
o Tại folder res -> navigation, tại file mobile_navigation tạo 3 UI destination tương
ứng với 3 file fragment XML “home, save, notification” như sau:
o Tại file MainActivity.java ta viết code chính để điều hướng các fragment “home,
save, notification”:
o public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
Tại folder res -> menu tạo file bottom_nav_menu.xml tương ứng với class
BottomNavigationView trong MainActivity.java dùng để điều hướng giao diện: HOME,
SAVED, NOTIFICATION:
3.2.2. Home:
fragment_home.xml
3.2.2.1 Carosel
- Để hiển thị danh sách dữ liệu trong carosel thì sử dụng RecyclerView và
Adapter. Nó được sử dụng để cập nhật nội dung của các mục danh sách và
thực hiện các tác vụ như gán dữ liệu vào các phần tử giao diện ImageView
trong mỗi mục của danh sách. Sử dụng thêm thư việc Picasso để lấy dữ liệu
hình ảnh từ trình duyệt.
<com.google.android.material.carousel.MaskableFrameLayout
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/carousel_item_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:circularflow_defaultRadius="12dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:foreground="?attr/selectableItemBackground"
app:shapeAppearanceOverlay="@style/roundedCorners"
app:shapeAppearance="?attr/shapeAppearanceCornerExtraLarge">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/carousel_image_view"
android:layout_width="match_parent"
android:adjustViewBounds="true"
android:layout_height="match_parent"
android:contentDescription="@string/title_logo"
android:scaleType="centerCrop"/>
</com.google.android.material.carousel.MaskableFrameLayout>
- Bước 3: Với những giá trị đã tạo ta tiếp tục viết code cho trong file
CaroselAdapter.java.java:
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int
viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.caroucel_item, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder,
@SuppressLint("RecyclerView") int position) {
Picasso.with(context).load(carousel_list.get(position)).into(holder.getImageVi
ew());
holder.getImageView().setOnClickListener(view -> {
// if (onClickListener != null) {
// onClickListener.onClick(position,
carousel_list.get(position));
// }
});
}
}
public ImageView getImageView() {
return imageView;
}
3.2.2.2. Category:
Bước 1: Tạo class Category để set() và get() name, id cho mục danh
sách như sau:
Tạo thêm class CategoryAdapter để liên kết dữ liệu giữa các mục trong danh sách tương
tự như Carosel. Nó được sử dụng để cập nhật nội dung của các mục danh sách và thực
hiện các tác vụ như gán dữ liệu vào các phần tử giao diện TextView trong mỗi mục của
danh sách.
public class CategoryAdapter extends
RecyclerView.Adapter<CategoryAdapter.ViewHolder> {
}
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
>
<com.google.android.material.chip.Chip
android:layout_marginEnd="6dp"
android:id="@+id/chip_item"
android:text="@string/title_logo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</FrameLayout>
- Bước 3: Với những giá trị đã tạo ta tiếp tục viết code cho trong file
CategoryAdapter.java.java:
private final ArrayList<Category> category_list;
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType)
{
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.catogory_item, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder,
@SuppressLint("RecyclerView") int position) {
holder.getTextView().setText(category_list.get(position).getName());
holder.getTextView().setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (onClickListener != null) {
onClickListener.onClick(position,
category_list.get(position));
}
}
});
}
}
public TextView getTextView() {
return textView;
}
}
3.2.2.3. News
Bước 1: Tạo class NewsItem để set() và get() các dữ liệu cần thiết cho mục
danh sách news như sau:
public class NewsItem implements Serializable {
}
Tạo thêm class NewsListAdapter để hiện dữ liệu liệu trong phần ListView:
public class NewsListAdapter extends BaseAdapter {
}
- Bước 3: Với những giá trị đã tạo ta tiếp tục viết code cho trong files
NewsItem.java và NewsListAdapter.java:
NewsItem:
@Override
public int getCount() {
return this.listNewsItem.size();
}
@Override
public Object getItem(int i) {
return this.listNewsItem.get(i);
}
@Override
public long getItemId(int i) {
return 0;
}
Picasso.with(context).load(item.getNews_image()).into(newsImage);
Picasso.with(context).load(item.getNews_author_avatar()).into(newsAuthorAvatar
);
if (item.getIs_saved()){
setActiveButton(savedBtn,item.getIs_saved());
}
savedBtn.setOnClickListener(view1 -> {;
boolean isAdd = !item.getIs_saved();
new Firebase().savedItem(item.getNew_id(),item.getIs_saved());
item.setIs_saved(isAdd);
setActiveButton(savedBtn, isAdd);
});
newsTitle.setText(item.getNews_title());
newsAuthorName.setText(item.getNews_author_name());
newsCreatedAt.setText(item.getNews_created_at());
return viewProduct;
}
Tại id: nav_view tức là thanh điều hướng đến các fragment khác bằng việc sử dụng
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:paddingVertical="12dp"
android:paddingHorizontal="6dp"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/user_id"
android:layout_marginEnd="12dp"
android:text="user id"
android:layout_width="0dp"
android:layout_weight="0.9"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/btn_sign_Out"
android:text="@string/sign_out"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
<ListView
android:id="@+id/saved_news"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="#00000000"
android:dividerHeight="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:layout_editor_absoluteY="238dp" />
<TextView
android:id="@+id/text_dashboard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:textAlignment="center"
android:textSize="20sp"
android:textAppearance="?android:textAppearanceMedium"
android:visibility="gone"/>
</LinearLayout>
Bước 3: Viết tiếp code cho method SavedFragment như sau:
SavedFragment:
o Sử dụng Data Binding trong FragmentSavedBinding, cho phép truy cập các phần
tử giao diện trong tệp layout fragment_saved.xml của SavedFragment bằng cách
sử dụng biến binding, giúp tương tác với các phần tử giao diện người dùng một
cách dễ dàng và linh hoạt hơn thông qua Data Binding.
o Sử dụng method onResumeđể thực hiện các nhiệm vụ như bắt đầu animation, làm
mới dữ liệu hoặc tải các tài nguyên cần thiết cho giao diện người dùng của
fragment khi nó trở nên hiển thị.
o Sử dụng method onDestroyView giúp giải phóng bộ nhớ và ngăn chặn rò rỉ bộ
nhớ. Khi view bị hủy bỏ, đó là thời điểm tốt để giải phóng tham chiếu đến các
view và tài nguyên liên quan đến fragment.
o Sử dụng FirebaseAuth để SignOut khỏi UserId người dùng hiện tại. Khi ấn Button
Sign Out sẽ đăng xuất trở về màn hình Sign In và xoá dữ liệu người dùng nếu
trước đó sử dụng tài khoản khách.
firebase.getSaved();
signOut.setOnClickListener(view -> {
auth.signOut();
});
userId.setText(auth.getUid());
savedViewModel.getText().observe(getViewLifecycleOwner(),
textView::setText);
//
savedViewModel.getList().observe(getViewLifecycleOwner(),listNewsItem::addAll)
;
return root;
}
@Override
public void onResume() {
super.onResume();
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
SavedViewModel:
Sử dụng kiến trúc MVVM( Model-View-ViewModel )
LiveData<ArrayList<NewsItem>> listNewsItemLiveData: Đây là một biến
kiểu LiveData chứa một danh sách (ArrayList) các mục tin tức (NewsItem). LiveData
này cho phép theo dõi các thay đổi trong danh sách mục tin tức.
SavedViewModel(): Đây là constructor của lớp SavedViewModel. Trong
constructor này, biến listNewsItemLiveData được khởi tạo và biến mText được khởi tạo
với giá trị ban đầu là "Nothing Here!!!".
LiveData<String> getText(): Phương thức này trả về biến mText dưới dạng
LiveData, cho phép các thành phần giao diện người dùng quan sát giá trị của nó. Phương
thức này được sử dụng để lấy giá trị chuỗi từ biến mText để hiển thị trong giao diện
người dùng.
LiveData<ArrayList<NewsItem>> getList(): Phương thức này trả về biến
listNewsItemLiveData dưới dạng LiveData, cho phép quan sát các thay đổi trong danh
sách mục tin tức. Phương thức này có thể được sử dụng để lấy danh sách mục tin tức và
hiển thị chúng trong giao diện người dùng.
public SavedViewModel() {
listNewsItemLiveData = new MutableLiveData<>();
mText = new MutableLiveData<>();
mText.setValue("Nothing Here!!!");
}
Bước 2: Tạo file fragment_notifications để hiển thị ra màn hình như sau:
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="6dp"
tools:context=".ui.notifications.NotificationsFragment">
<ListView
android:divider="#00000000"
android:dividerHeight="12dp"
android:id="@+id/notification_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/text_notifications"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:textAlignment="center"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Tiếp tục tạo file notification_items.xml để hiển thị thông tin chi tiết các thông báo:
<ImageView
android:background="@drawable/rounded_shape"
android:id="@+id/notification_icon"
android:src="@drawable/baseline_notifications_active_24"
android:contentDescription="@string/title_logo"
android:layout_width="wrap_content"
android:layout_marginEnd="12dp"
android:layout_height="wrap_content"
app:tint="@color/primary" />
<LinearLayout
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/notification_title"
android:text="title"
android:textSize="18sp"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/notification_content"
android:textSize="12sp"
android:textColor="@color/gray_200"
android:text="content"
android:layout_width="wrap_content"
android:layout_height="16dp"/>
</LinearLayout>
</LinearLayout>
new Firebase().getNotifications(getContext(),listView,notificationItems);
listView.setEmptyView(binding.textNotifications);
notificationsViewModel.getText().observe(getViewLifecycleOwner(),
textView::setText);
return root;
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
public NotificationsViewModel() {
mText = new MutableLiveData<>();
mText.setValue("Nothing Here!!!");
}
public LiveData<String> getText() {
return mText;
}
Bước 4: Tạo class NotificationItems.java được sử dụng để đại diện cho các
thông báo, get và set. Class này có các trường dữ liệu (properties) như noti_id,
title, content, và type, được sử dụng để lưu trữ thông tin liên quan đến một
thông báo.
o noti_id: Là một chuỗi (String) đại diện cho ID của thông báo.
o title: Là một chuỗi (String) đại diện cho tiêu đề của thông báo.
o content: Là một chuỗi (String) đại diện cho nội dung của thông báo.
o type: Là một chuỗi (String) đại diện cho loại thông báo.
public NotificationItems() {
}
@Override
public int getCount() {
return itemsArrayList.size();
}
@Override
public Object getItem(int i) {
return itemsArrayList.get(i);
}
@Override
public long getItemId(int i) {
return 0;
}
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
notification_title.setText(item.getTitle());
notification_content.setText(item.getContent());
return viewProduct;
}
Bước 1: Tạo class DetailActivity để hiển thị thông tin chi tiết về tin tức bao
gồm tiêu đề, tác giả, số lần xem, số lượt thích và mô tả. Người dùng cũng có
thể thích/không thích mục tin tức.
public class DetailActivity extends AppCompatActivity {
}
Bước 2: Tạo file activity_detail.xml để hiển thị ra màn hình như sau:
Bước 3: Tiếp tục viết code cho class DetailActivity.java như sau:
onCreate: Phương thức này được gọi khi hoạt động được tạo. Nó thiết lập giao
diện người dùng và xử lý các hành động liên quan đến chế độ xem chi tiết. Nó bao
gồm các bước sau:
o Đặt tiêu đề thanh hành động và cho phép nút quay lại.
o Khởi tạo và tìm các tham chiếu đến các thành phần giao diện người dùng
(ví dụ: TextView, ImageView, LinearLayout).
o Truy xuất mục tin tức (có thể được truyền qua intent).
o Khởi tạo lớp Firebase để tương tác với Firebase Realtime Database.
o Kiểm tra xem người dùng đã thích mục tin tức này trước đó chưa và thay
đổi màu nút thích tương ứng.
btn_like.setOnClickListener: Trình nghe này được kích hoạt khi người dùng
nhấn nút thích. Nó chuyển đổi chức năng thích/không thích và cập nhật số lượt
thích trong Firebase Realtime Database.
firebase.getDetail: Phương thức này truy xuất thông tin chi tiết về mục tin tức từ
Firebase Realtime Database, bao gồm hình ảnh biểu ngữ, thông tin tác giả, tiêu đề,
số lượt thích, số lượt xem, tên tác giả, và mô tả tin tức. Thông tin này sau đó được
sử dụng để điền vào các thành phần giao diện người dùng.
formatValue: Phương thức này định dạng giá trị số của số lần xem và số lượt
thích thành một định dạng thân thiện với người dùng hơn, ví dụ, chuyển đổi số lớn
thành K (nghìn), M (triệu), B (tỷ), v.v.
onOptionsItemSelected: Phương thức này được sử dụng để xử lý các tùy chọn
được chọn trong thanh hành động. Nếu người dùng chọn nút quay lại, hoạt động sẽ
kết thúc và người dùng trở lại màn hình trước đó.
formatValue: Phương thức này được sử dụng để định dạng số lượt xem và số lượt
thích.
TextView textView;
TextView tv_item_title, tv_item_like_count, tv_item_view_count,tv_author_name;
ImageView iv_item_banner,iv_author_avatar;
LinearLayout btn_like;
Firebase firebase;
@SuppressLint("MissingInflatedId")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detail);
ActionBar actionBar = getSupportActionBar();
iv_item_banner = findViewById(R.id.item_banner);
iv_author_avatar = findViewById(R.id.item_author_avatar);
tv_item_title = findViewById(R.id.item_title);
tv_item_view_count = findViewById(R.id.item_view_count);
tv_item_like_count = findViewById(R.id.item_like_count);
tv_author_name = findViewById(R.id.item_author_name);
textView = findViewById(R.id.item_detail);
imageViewLike.setColorFilter(ContextCompat.getColor(getApplicationContext(),R.
color.primary), android.graphics.PorterDuff.Mode.SRC_IN);
}
});
firebase.updateViewCount(items.getNew_id(),items.getView_count());
btn_like.setOnClickListener(view -> {
String id = items.getNew_id();
likesRef.child(id)
.get()
.addOnCompleteListener(task -> {
int like = items.getLike_count();
int color = R.color.primary;
if (task.getResult().exists()){
like -=1;
items.setLike_count(like);
likesRef.removeValue();
color = R.color.gray_200;
}
else {
like+=1;
likesRef.child(id).setValue(id);
}
DatabaseReference ref =
firebase.getmDatabase().child(Firebase.NEWS).child(id).child(Firebase.LIKE_CO
UNT);
ref.setValue(like);
imageViewLike.setColorFilter(ContextCompat.getColor(getApplicationContext(),co
lor), android.graphics.PorterDuff.Mode.SRC_IN);
});
});
firebase.getDetail(items.getNew_id()).addValueEventListener(new
ValueEventListener() {
@SuppressLint("SetTextI18n")
@Override
public void onDataChange(@NonNull DataSnapshot snapshot) {
NewsItem item = snapshot.getValue(NewsItem.class);
assert item != null;
Picasso.with(getApplicationContext()).load(item.getNews_image()).into(iv_item_
banner);
Picasso.with(getApplicationContext()).load(item.getNews_author_avatar()).into(
iv_author_avatar);
tv_item_title.setText(item.getNews_title());
tv_item_like_count.setText(formatValue(item.getLike_count()));
tv_item_view_count.setText(formatValue(item.getView_count()));
tv_author_name.setText(item.getNews_author_name()+" -
"+item.getNews_created_at());
textView.setText(item.getDetail());
}
@Override
public void onCancelled(@NonNull DatabaseError error) {
}
});
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == android.R.id.home) {
this.finish();
return true;
}
return super.onOptionsItemSelected(item);
}
public String formatValue(float value) {
String[] arr = {"", "K", "M", "B", "T", "P", "E"};
int index = 0;
while ((value / 1000) >= 1) {
value = value / 1000;
index++;
}
DecimalFormat decimalFormat = new DecimalFormat("#.##");
return String.format("%s %s", decimalFormat.format(value), arr[index]);
}