Xây dựng ứng dụng Viblo trên android bằng kotlin sử dụng lib jsoup (Phần 1)

Với chúng ta thì trang web viblo.asia đã quá quen thuộc rồi, nhưng việc xem nó trên di động không thích hợp cho lắm vì có nhiều thành phần không cần thiết - > Từ những điều đó mình đã lên ý tưởng viết 1 app Viblo bằng kotlin và sử dụng thư viện jsoup Sau đây mình sẽ viết 1 series các bài viết hướng dẫn thực hiện ý tưởng này

1. MainActivity

1.1. activity_main.xml!

  • Cấu trúc gồm 1 BottomNavigationView mình đặt lên trên cùng và 1 FrameLayout Trong đó BottomNavigationView sẽ có 3 tab là Post , Questions và Discussions

  • Full code:

<?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:background="@android:color/white"
    android:orientation="vertical"
    tools:context="com.asia.viblo.view.activity.home.MainActivity">

    <android.support.design.widget.BottomNavigationView
        android:id="@+id/bottomNavigationHome"
        android:layout_width="match_parent"
        android:layout_height="@dimen/size_56"
        android:layout_gravity="start"
        app:elevation="@dimen/size_1"
        app:itemBackground="@drawable/bg_bottom_navigation"
        app:itemIconTint="@color/selector_color_bottom_navigation"
        app:itemTextColor="@color/selector_color_bottom_navigation"
        app:menu="@menu/menu_navigation_items"/>

    <FrameLayout
        android:id="@+id/frameHome"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>
</LinearLayout>

1.2. menu_navigation_items.xml

Tạo 1 file menu giao diện cho BottomNavigationView ở thư mục res -> menu -> menu_navigation_items.xml Gồm 3 tab Post , Questions và Discussion

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/itemPost"
        android:icon="@drawable/ic_post"
        android:title="Post"/>
    <item
        android:id="@+id/itemQuestions"
        android:icon="@drawable/ic_questions"
        android:title="Questions"/>
    <item
        android:id="@+id/itemDiscussion"
        android:icon="@drawable/ic_discussion"
        android:title="Discussions"/>
</menu>

1.3. MainActivity

Khởi tạo listener cho BottomNavigationView

  private fun initBottomNavigationView() {
        bottomNavigationHome.setOnNavigationItemSelectedListener { item ->
            val fragment: Fragment = when (item.itemId) {
                R.id.itemPost -> PostFragment()
                R.id.itemQuestions -> QuestionsFragment()
                R.id.itemDiscussion -> DiscussionsFragment()
                else -> PostFragment()
            }
            supportFragmentManager.beginTransaction().replace(R.id.frameHome, fragment).commit()
            true
        }
        bottomNavigationHome.selectedItemId = R.id.itemPost
    }

2. BaseFragment

  • Cấu trúc của 1 fragment extend BaseFragment gồm có :
  • 1 Spinner ở trên cùng
  • Phần content ở giữa
  • Ở cuối cùng là 1 layout : include_layout_next_back_page.xml để chuyển trang

  • Full code : BaseFragment.kt

  • Tạo listener chọn trang

    • textPageNext để đến trang tiếp theo
    • textPageBack để trở lại trang trước đó
    • textPagePresent khi ấn vào đây sẽ hiển thị dialog chọn trang theo ý muốn của mình

 open fun initListener() {
        textPageNext.setOnClickListener {
            val pageNext = SharedPrefs.instance[keyPagePresent, String::class.java].toInt() + 1
            loadData(getLink(mPosition), pageNext.toString())
        }
        textPageBack.setOnClickListener {
            val pageBack = SharedPrefs.instance[keyPagePresent, String::class.java].toInt() - 1
            loadData(getLink(mPosition), pageBack.toString())
        }
        textPagePresent.setOnClickListener {
            val builder = AlertDialog.Builder(context)
            val dialogSelectPage = DialogSelectPage(context, null, this) as LinearLayout
            dialogSelectPage.txtTitle.text = getString(R.string.text_dialog_title)
            dialogSelectPage.txtMessage.text = String.format(
                    getString(R.string.text_dialog_message, "1", SharedPrefs.instance[keyMaxPage, String::class.java]))
            dialogSelectPage.editPage.setText(SharedPrefs.instance[keyPagePresent, String::class.java])
            builder.setView(dialogSelectPage)
            mAlertDialog = builder.show()
        }
  • Khởi tạo Spinner
open fun initSpinner() {
        initSpinner(null)
    }
    
 open fun initSpinner(params: String?) {
        if (!checkErrorNetwork(context)) return
        showProgressDialog()
        if (TextUtils.isEmpty(params)) {
            FeedBarAsyncTask(this).execute(getLink(mPosition))
        } else {
            FeedBarAsyncTask(this).execute(getLink(mPosition), params)
        }
    }
  • Function showProgressDialog khi đang load data
open fun showProgressDialog() {
        if (mProgressDialog.isShowing) {
            mProgressDialog.dismiss()
        }
        mProgressDialog.show()
    }
  • Function loadData
open fun loadData(url: String) {
        loadData(url, "")
    }

    open fun loadData(url: String, page: String) {
        if (!checkErrorNetwork(context)) return
        showProgressDialog()
    }
  • Hiển thị trang hiện tại trên tổng số trang
open fun getPagePresent(pagePresentStr: String, pageMaxStr: String): String {
        return pagePresentStr + "/" + pageMaxStr
    }
  • Function abstract fun getLink(type: Int): String để lấy link url

3. PostFragment

3.1. PostFragment.kt

 override fun getLink(type: Int): String {
      return when (type) {
          0 -> baseUrlViblo
          1 -> baseUrlSeries
          2 -> baseUrlEditorsChoice
          3 -> baseUrlTrending
          4 -> baseUrlVideos
          else -> baseUrlViblo
      }
  }
private fun initRecyclerPost() {
      mPostAdapter = PostAdapter(context, mPostList, this, this)
      recyclerPost.adapter = mPostAdapter
      recyclerPost.layoutManager = LinearLayoutManager(context)
  }
 override fun onUpdatePostData(postList: List<Post>?) {
      if (postList != null) {
          mPostList.clear()
          mPostList.addAll(postList)
          mPostAdapter.notifyDataSetChanged()
      }
      mProgressDialog.dismiss()
      updateViewNextBackBottom()
  }

3.2 fragment_post.xml

<LinearLayout 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"
              android:orientation="vertical"
              tools:context="com.asia.viblo.view.fragment.post.PostFragment">

    <Spinner
        android:id="@+id/spinnerPost"
        android:layout_width="@dimen/size_170"
        android:layout_height="@dimen/size_30"
        android:layout_margin="@dimen/size_10"
        android:visibility="invisible"/>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerPost"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>

    <include
        android:id="@+id/viewNextBack"
        layout="@layout/include_layout_next_back_page"/>
</LinearLayout>

4. LoadPostAsyncTask

4.1. Lưu vị trí trang hiện tại và trang lớn nhất

 try {
            val document = Jsoup.connect(baseUrl + page).get()
            val elements = document?.select(getCssQuery(baseUrl, TypeQuery.PAGE))
            elements!!
                    .map { it.select("li") }
                    .forEach { data ->
                        data.asSequence()
                                .map { it.getElementsByTag("a").text() }
                                .filterNotTo(pageList) { TextUtils.isEmpty(it) }
                    }
        } catch (ex: Exception) {
            ex.printStackTrace()
        }
        SharedPrefs.instance.put(keyMaxPage, if (pageList.isNotEmpty()) pageList.last() else "0")
        if (params.size == 1) {
            SharedPrefs.instance.put(keyPagePresent, "1")
        }

4.2. Dùng interface để trả dữ liệu về PostFragment.kt

 override fun onPostExecute(result: List<Post>?) {
        super.onPostExecute(result)
        mOnUpdatePostData.onUpdatePostData(result)
    }

4.3. Function getLinkPage()

  • Lấy ra trang cần load data nếu
  • trang truyền vào từ params < pageMax thì lấy trang đó
  • trang truyền vào từ params > pageMax thì lấy pageMax
 private fun getLinkPage(baseUrl: String?, page: String?): String {
        var pageCheck = page
        try {
            if (page != null) {
                val pageMaxStr = SharedPrefs.instance[keyMaxPage, String::class.java]
                if (!TextUtils.isEmpty(pageMaxStr)) {
                    val pageMax = pageMaxStr.toInt()
                    val pagePresent = page.toInt()
                    if (pagePresent > pageMax) {
                        pageCheck = pageMaxStr
                    }
                }
            }
        } catch (ex: Exception) {
            ex.printStackTrace()
        }
        if (!TextUtils.isEmpty(pageCheck)) {
            SharedPrefs.instance.put(keyPagePresent, pageCheck)
        }
        return when (baseUrl) {
            baseUrlViblo -> "/?page="
            else -> "?page="
        } + pageCheck
    }

5. Model

5.1. BaseModel.kt

package com.asia.viblo.model

import java.io.Serializable

/**
 * Created by FRAMGIA\vu.tuan.anh on 09/11/2017.
 */
open class BaseModel : Serializable {
    var avatar = ""
    var name = ""
    var time = ""
    var authorUrl = ""
    var title = ""
    var score = ""
    var views = ""
    var comments = ""
    var tags: MutableList<String> = arrayListOf()
    var tagUrlList: MutableList<String> = arrayListOf()
}

5.2. Post.kt

package com.asia.viblo.model.post

import com.asia.viblo.model.BaseModel

/**
 * Created by FRAMGIA\vu.tuan.anh on 27/10/2017.
 */
open class Post : BaseModel() {
    var postUrl = ""
    var clips = ""
    var posts = ""
    var reputation = ""
    var followers = ""
    var post = ""
    var isVideo = false
}

Hình ảnh

Hình 2

Code

Github

Tài liệu tham khảo

https://androidcoban.com/su-dung-thu-vien-jsoup-boc-html-trong-android.html https://viblo.asia/ https://kotlinlang.org/docs/reference/

Link apk

https://www.mediafire.com/file/5ln4272ix13w6nv/viblo.v24.11.2017.apk