Cordova series simplifies complexity: creating Cordova components suitable for all scenarios

Foreword

In the opening chapter of my previous article A Preliminary Study of Cordova, I mentioned that one of the significant limitations of Cordova in Android application development is that our Activity must inherit the CordovaActivity provided by it. This design is particularly restrictive for projects that pursue personalized UI design.
In fact, it is understandable that Cordova mainly aims to provide a convenient framework for front-end developers so that they can focus on writing HTML, CSS and JS code to meet business needs and ensure that these codes can run on iOS and Android platforms.
This is very suitable for pure H5 development, where Cordova acts as a container for H5 content. However, in the projects I encountered, the pure H5 development approach is actually uncommon.
H5 can achieve elegant cross-platform functionality in theory, but in practice, we need to consider multiple key factors, such as compatibility, performance, and maturity of interacting with native applications.
Therefore, this article is mainly to provide Android development students with a component suitable for all scenarios based on Cordova encapsulation, so that based on the original functions of Cordova, we can break through the restriction of having to inherit CordovaActivity and customize ours as we like. ui, and provide richer functions to better support our business needs.

Goals & amp; Ideas

Goals
  • Maintain the original functionality and usage of Cordova.
  • Get rid of the restriction of inheriting Activity and make it as flexible to use as Webview control.
  • Provides Activity and Fragment-based encapsulation to simplify development and support UI personalization.
  • Provide callbacks of key nodes to meet broader business needs.
Ideas

Taking the entry source code of CordovaActivity as the starting point, we will find that the amount of code in CordovaActivity is not large, and its core functions are concentrated on a few key nodes, such as processing SplashScreen, loading configuration, initializing variables, creating CordovaInterface, etc. Therefore, one idea is to extract the core code of CordovaWebView and CordovaWebViewEngine in CordovaActivity, encapsulate it into a smaller-grained custom view for external use, and provide template classes for Activity and Fragment to expose the custom UI interface. In this way, the core goal can be achieved.

The specific code implementation has been uploaded to github, and interested readers are welcome to check it out. The following mainly introduces the use of components in detail.

Component usage

Add dependencies
implementation("com.xeonyu:cordova-webcontainer:1.0.5")
Inherit CordovaWebContainerActivity

This method is suitable for most business scenarios, is easy to integrate and keeps the UI flexible.

Layout example

<?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"
    tools:context="com.yzq.demo.activity.WebContainerActivity">

    <!--title bar-->
    <androidx.appcompat.widget.Toolbar
        android:id="@ + id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/purple_200"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:titleTextColor="@color/white" />

    <!--Progress bar-->
    <ProgressBar
        android:id="@ + id/progressbar"
        style="@android:style/Widget.ProgressBar.Horizontal"
        android:layout_width="match_parent"
        android:layout_height="5dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/toolbar" />

    <!--Web container control based on Cordova encapsulation-->
    <com.yzq.cordova_webcontainer.CordovaWebContainer
        android:id="@ + id/web_container"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@id/progressbar"
        app:layout_goneMarginTop="5dp" />

    <!--Floating button-->
    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@ + id/reload_fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:contentDescription="Refresh"
        android:src="@drawable/refresh"
        android:tint="@color/white"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Code example:

/**
 * @description Usage example inherited from WebcontainerActivity
 * @author yuzhiqiang ([email protected])
 */

class WebContainerActivity : CordovaWebContainerActivity() {<!-- -->
    private lateinit var binding: ActivityWebContainerBinding
    private val TAG = "WebContainerActivity"

    /*Layout initialization*/
    override fun initContentView() {<!-- -->
        binding = ActivityWebContainerBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }


    /*Initialize Webcontainer control*/
    override fun initWebContainer(): CordovaWebContainer {<!-- -->
        with(binding) {<!-- -->
            webContainer.run {<!-- -->
                /**
                 * Initialize webcontainer
                 */
                init(
                    this@WebContainerActivity,
                    this@WebContainerActivity
                )

             
        return binding.webContainer
    }


    override fun onCreate(savedInstanceState: Bundle?) {<!-- -->
        super.onCreate(savedInstanceState)
        val url = "https://baidu.com/"
        binding.webContainer.loadUrl(url)
        binding.webContainer.setOnPageScrollChangedListener {<!-- --> xOffset, yOffset, oldxOffset, oldyOffset ->
            Log.i(TAG, "yOffset:$yOffset,oldyOffset:$oldyOffset")
        }
        binding.reloadFab.setOnClickListener {<!-- -->
            binding.webContainer.reload()

        }
    }

}
Use in Fragment

Supports use in Fragment, just inherit CordovaWebContainerFragment, the api follows CordovaWebContainerActivity
consistent. It should be noted that the Fragment’s host Activity needs to handle the page result callback method.

Rewrite the following two methods in the host Activity. The writing method is fixed.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {<!-- -->
        super.onActivityResult(requestCode, resultCode, data)
      
        currentFragment?.onActivityResult(requestCode, resultCode, data)
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray,
    ) {<!-- -->
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
      
        currentFragment?.onRequestPermissionsResult(requestCode, permissions, grantResults)
    }

Fragment can be used normally, sample code

package com.yzq.demo.fragment

/**
 * @description Example of use in Fragment
 * @author yuzhiqiang ([email protected])
 */

class WebContainerFragment(val webUrl: String) : CordovaWebContainerFragment() {<!-- -->
    private lateinit var rootView: View
    private lateinit var webContainer: CordovaWebContainer

    override fun initContentView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View {<!-- -->
        rootView = layoutInflater.inflate(R.layout.fragment_web_container, container, false)
        return rootView
    }

    override fun initWebContainer(): CordovaWebContainer {<!-- -->
        webContainer = rootView.findViewById(R.id.web_container)
        webContainer.init(requireActivity() as AppCompatActivity, this)
        return webContainer
    }

    override fun initWidget() {<!-- -->
        if (webUrl.isNotEmpty()) {<!-- -->
            webContainer.loadUrl(url = webUrl)
        } else {<!-- -->
            webContainer.loadUrl()
        }
    }


}
Used as a custom view

If you do not want to inherit the specified Activity, you can use CordovaWebContainer as a custom view.

package com.yzq.demo.activity


/**
 * @description Example of using the Webcontainer control directly, suitable for more flexible scenarios, for example, you do not want to inherit the specified Activity
 * @author yuzhiqiang ([email protected])
 */

class MainActivity : AppCompatActivity() {<!-- -->
    private lateinit var binding: ActivityMainBinding
    private val TAG = "MainActivity"
    override fun onCreate(savedInstanceState: Bundle?) {<!-- -->
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.run {<!-- -->
            toolbar.title = "Usage of webview based on Cordova"
         
            /*initialization*/
            webContainer.init(this@MainActivity, this@MainActivity)
            /*Load url*/
// val url = "https://www.baidu.com/"
            webContainer.loadUrl()

        }

    }


    //Fixed writing method
    override fun onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) {<!-- -->
        super.onSaveInstanceState(outState, outPersistentState)
        binding.webContainer.onSaveInstanceState(outState)
    }
    //Fixed writing method
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {<!-- -->
        super.onActivityResult(requestCode, resultCode, data)
        binding.webContainer.onActivityResult(requestCode, resultCode, data)

    }
    //Fixed writing method
    override fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?) {<!-- -->
        binding.webContainer.startActivityForResult(requestCode)
        super.startActivityForResult(intent, requestCode, options)
    }
//Fixed writing method
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray,
    ) {<!-- -->
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        binding.webContainer.onRequestPermissionsResult(requestCode, permissions, grantResults)
    }
}

API

List the main APIs. For specific usage, please refer to the sample code on github.

Initialize WebContainer

Pass in Activity and LifecycleOwner

/*Initialization (required)*/
webContainer.init(this, this)
Loading url
/*required*/
webContainer.loadUrl(url)
Key event callback
webContainer.addPagePbserver(PageObserver)

Supports following event monitoring

Request interception processing
/*Optional interception request is equivalent to shouldInterceptRequest. Remember to use this*/
                webContainer.webviewClient.interceptRequest {<!-- --> view, request, response ->
                    val url = request.url.toString()
                    Log.i(TAG, "interceptRequest:$url")
                    return@interceptRequest response
                }
loadurl processing
/*Optional to process the URL to be loaded, which is equivalent to shouldOverrideUrlLoading*/
                webContainer.webviewClient.overrideUrlLoading {<!-- --> view, request ->
                    Log.i(TAG, "overrideUrlLoading:${<!-- -->request.url}")
                    request.url.toString().let {<!-- -->
                        if (it.startsWith("baidu://")) {<!-- -->
                            return@overrideUrlLoading true
                        }
                    }
                    return@overrideUrlLoading false
                }
Scroll monitoring
webContainer.setOnPageScrollChangedListener {<!-- --> xOffset, yOffset, oldxOffset, oldyOffset ->
            Log.i(TAG, "yOffset:$yOffset,oldyOffset:$oldyOffset")
        }

Other uses are the same as webview api

Confusion

The necessary obfuscation rules are already included inside the component.

#Cordova
-keep class org.apache.cordova.**{*;}
-keep interface org.apache.cordova.**{*;}
-keep enum org.apache.cordova.**{*;}

This is a full scene component based on Cordova encapsulation. If you think this component is helpful to you, please give it a star on GitHub. I hope it can help you.

github address: cordova-webcontainer

Thanks for reading, please like and support if you find it helpful. If you have any questions or suggestions, please leave a message in the comment area. If you need to reprint, please indicate the source: Yu Zhiqiang’s blog, thank you!