Microsoft MVP성태의 닷넷 이야기
Phone: 18. C# MAUI - 안드로이드 플랫폼에서의 Activity 제어 [링크 복사], [링크+제목 복사],
조회: 9417
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
 
(연관된 글이 2개 있습니다.)
(시리즈 글이 4개 있습니다.)
Phone: 16. C# MAUI - /Download 등의 공용 디렉터리에 접근하는 방법
; https://www.sysnet.pe.kr/2/0/13631

Phone: 17. C# MAUI - Android 내에 Web 서비스 호스팅
; https://www.sysnet.pe.kr/2/0/13632

Phone: 18. C# MAUI - 안드로이드 플랫폼에서의 Activity 제어
; https://www.sysnet.pe.kr/2/0/13634

Phone: 21. C# MAUI - Android 환경에서의 파일 다운로드(DownloadManager)
; https://www.sysnet.pe.kr/2/0/13640




C# MAUI - 안드로이드 플랫폼에서의 Activity 제어

(이 글은 안드로이드 환경만을 대상으로 합니다.)

MAUI가 기본적으로 XAML을 사용한 UI를 지원하지만 전통적인 Activity를 사용하는 것도 가능합니다. 예를 하나 들어볼까요? 간단하게 Button Click을 하나 만들고,

<Button
        x:Name="btnNewActivity"
        Text="New Activity" 
        SemanticProperties.Hint="Click to show new Activity"
        Clicked="OnNewActivityClicked"
        HorizontalOptions="Fill" />

이벤트 핸들러에서 이런 식으로 Activity를 띄울 수 있습니다.

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
    }

    private void OnNewActivityClicked(object sender, EventArgs e)
    {
#if ANDROID
        var currentActivity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
        if (currentActivity == null)
        {
            return;
        }

        var newIntent = new Intent(currentActivity, typeof(NewActivity));
        currentActivity.StartActivity(newIntent);
#endif
    }
}

NewActivity 클래스는 .\Platforms\Android 경로에 대충 이렇게 추가해 두면 됩니다.

// NewActivity.cs

using Android.App;
using Android.OS;

namespace SimplePlayer;

[Activity]
internal class NewActivity : Activity
{
    protected override void OnCreate(Bundle? savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
    }
}

당연히 이렇게만 하면 버튼을 클릭했을 때 뜨는 NewActivity 화면은 비어 있게 됩니다.




그나저나, 제가 Xamarin Forms부터 이어져온 .NET MAUI에 대한 이력을 전혀 모르고 있습니다. ^^; 게다가 구조도 아직 눈에 안 들어와서 초보 사용자 수준으로만 다뤄보고 있는데요, 그래서 아직 모르는 것인지, 그런 방법이 없는 것인지 알 수 없지만 현재 .NET MAUI는 XAML을 이용한 폼 구성을 MainActivity에만 종속시키고 있는 듯합니다.

다시 말해, 제가 위에서 만든 NewActivity를 위해 XAML로 UI를 구성할 수 없다는 것입니다. (혹시 방법을 아시는 분은 덧글 부탁드립니다. ^^)

이와 관련해서는 검색해도 자료도 없고, 어느 정도 저렇다고 판단할 수밖에 없는 근거가 있다면, 바로 App.Current에 대한 접근이 static 필드라는 사실입니다.

public static Application? Current { get; set; }

여기서 Current는 .NET MAUI 프로젝트의 App.xaml/App.xaml.cs에 해당하는 인스턴스가 설정되는데요, 그렇다면 오직 하나의 XAML Application 구조만 생성된다는 것이고 그것이 MainActivity가 소유한 Window에만 종속돼 있을 것입니다. 이런 상황에서 다른 Activity에서 (이미 MainActivity가 소유한) XAML을 임의로 가져가서 쓴다는 것이 맞지 않습니다. 애초에 이게 가능하려면 AppShell이나 App이 다중으로 생성될 수 있는 구조여야 하고 그것과 Activity가 연결되었다는 정보가 있어야만 합니다.

하지만, .NET MAUI가 생성하는 MainActivity는 딱히 AppShell/MainPage XAML을 소유한다는 식의 정보는 소스코드 내에 어떤 식으로도 명시돼 있지 않습니다.

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true,
    ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density
    )]
public class MainActivity : MauiAppCompatActivity
{
    protected override void OnCreate(Bundle? savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
    }
}




자, 그래서 우리가 새로 생성한 Activity인 경우에는 전통적인 Android 방식의 UI 시스템을 따라야 합니다. 이를 위해 .\Platforms\Android\Resources\layout 디렉터리에 activity_new.axml 파일을 만들어 다음의 내용으로 채웁니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical" >
    <TextView android:id="@+id/txtOutput"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="Hello, world" />
    <Button android:id="@+id/btnSample"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Click" />
</LinearLayout>

그다음 NewActivity에서는 위의 layout을 이렇게 연결하면 됩니다.

[Activity]
internal class NewActivity : Activity
{
    TextView? _txtOutput;
    Android.Widget.Button? _btnSample;

    protected override void OnCreate(Bundle? savedInstanceState)
    {
        base.OnCreate(savedInstanceState);

        this.SetContentView(Resource.Layout.activity_new);

        _txtOutput = FindViewById<TextView>(Resource.Id.txtOutput);
        _btnSample = FindViewById<Android.Widget.Button>(Resource.Id.btnSample);

        if (_btnSample != null)
        {
            _btnSample.Click += _btnSample_Click;
        }
    }

    private void _btnSample_Click(object? sender, EventArgs e)
    {
        _txtOutput.Text = "Hello, .NET MAUI";
    }
}




그런데 여기서 한 가지 재미있는 점이 있습니다. (XAML과 연결된) MainActivity를 제외하고는, 위와 같이 임의로 생성한 Activity는 MAUI의 Navigation 기능인 Shell.Current.GoToAsync를 사용할 수 없습니다.

일례로, _btnSample_Click에서 다음과 같이 코드를 추가하고,

private async void _btnSample_Click(object? sender, EventArgs e)
{
    _txtOutput.Text = "Hello, .NET MAUI";

    await Shell.Current.GoToAsync($"//{nameof(MainPage)}");
}

실행 후, 버튼을 누르면 MainPage로의 전환이 되지 않습니다. 아래의 질문도 이와 관련된 것인데요,

Navigate to a page in .Net MAUI Anroid Activity
; https://stackoverflow.com/questions/77265785/navigate-to-a-page-in-net-maui-anroid-activity

다시 말해 저렇게 동작할 수는 없습니다. 사실 일면 이해는 됩니다. 위의 구조를 Windows 운영체제로 대입해 보면, WPF Window 내에 Page XAML이 구성돼 있는데 그 인스턴스를 그대로 다른 WPF Window의 Child로 동시에 종속시키는 것이 가능하지 않는 식입니다.

즉, 위의 명령어 실행은 Android의 axml layout UI 화면을 (이미 다른 Activity가 소유한) XAML 화면을 가져와 현재 Activity의 UI로 바꾸려는 시도가 됩니다.

그럼 어떻게 MainPage XAML로 갈 수 있을까요? 간단합니다. Activity 전환을 하면 됩니다. 따라서, Android의 방식에 따라 다음과 같이 Activity를 이동시켜야 합니다.

private void _btnSample_Click(object? sender, EventArgs e)
{
    // await Shell.Current.GoToAsync($"//{nameof(MainPage)}");

    var mainIntent = new Intent(this, typeof(MainActivity));
    StartActivity(mainIntent);
}




그런데, 여기서 또 한 가지 문제가 발생하는데요, 저 코드를 .NET 8/MAUI에서 실행하면 이런 오류 메시지를 보게 될 것입니다.

System.InvalidOperationException: 'Window was already created'

0xFFFFFFFFFFFFFFFF in Android.Runtime.RuntimeNativeMethods.monodroid_debugger_unhandled_exception   C#
0x1A in Android.Runtime.JNINativeWrapper._unhandled_exception at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:13,5 C#
0x1E in Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPL_V at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:126,26  C#
0x12 in Microsoft.Maui.Controls.Window.Microsoft.Maui.IWindow.Created at D:\a\_work\1\s\src\Controls\src\Core\Window\Window.cs:483,5    C#
0xB in Microsoft.Maui.LifecycleEvents.AppHostBuilderExtensions.<>c.<OnConfigureLifeCycle>b__2_0 at D:\a\_work\1\s\src\Core\src\Hosting\LifecycleEvents\AppHostBuilderExtensions.Android.cs:23,6 C#
0xD in Microsoft.Maui.MauiAppCompatActivity. at D:\a\_work\1\s\src\Core\src\Platform\Android\MauiAppCompatActivity.Lifecycle.cs:61,104  C#
0x1F in Microsoft.Maui.LifecycleEvents.LifecycleEventServiceExtensions.InvokeLifecycleEvents<Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnPostCreate> at D:\a\_work\1\s\src\Core\src\LifecycleEvents\LifecycleEventServiceExtensions.cs:31,5 C#
0x40 in Microsoft.Maui.MauiAppCompatActivity.OnPostCreate at D:\a\_work\1\s\src\Core\src\Platform\Android\MauiAppCompatActivity.Lifecycle.cs:61,4   C#
0x11 in Android.App.Activity.n_OnPostCreate_Landroid_os_Bundle_ at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/obj/Release/net8.0/android-34/mcw/Android.App.Activity.cs:4488,4 C#
0x9 in Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPL_V at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:125,5    C#

이슈를 보면 .NET 7 이하로는 괜찮았던 것 같은데 .NET 8부터 문제가 된다고 합니다.

MAUI Android build crashes when app is reopened from background. It throws the exception: 'Window was already created.' #18692
; https://github.com/dotnet/maui/issues/18692

위의 문제를 우회하는 방법이 하나 있는데요, 결국 OnPostCreate에서 예외가 발생한 것이기 때문에 아래와 같이 try/catch를 감싸주면 넘어가긴 합니다.

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true,
    ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density
    )]
public class MainActivity : MauiAppCompatActivity
{
    protected override void OnCreate(Bundle? savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
    }

    protected override void OnPostCreate(Bundle? savedInstanceState)
    {
        try
        {
            base.OnPostCreate(savedInstanceState);
        }
        catch (Exception e) // 하지만, 내부 코드의 어느 지점에서 예외가 발생했는지에 대해서는 알 수 없으므로 어떤 초기화가 생략됐을지 분석이 필요합니다.
        {
            System.Diagnostics.Trace.WriteLine(e.ToString());
        }
    }
}

위와 같은 현상이 발생하는 원인을 추측해 볼까요? 일단 예제 코드처럼 Intent 전환을 하면 Android는 새롭게 MainActivity 인스턴스를 생성해 처리합니다. 다시 말해 아래와 같은 Navigation Stack이 쌓여 있는 것입니다.

2: MainActivity // StartActivity(mainIntent)로 새로 쌓인 Activity
1: NewActivity  // currentActivity.StartActivity(newIntent)로 쌓인 Activity
0: MainActivity // 최초 App 실행 시 생성된 Activity

그렇다면 2개의 MainActivity 인스턴스가 생성됐고 XAML UI도 각각 새롭게 MainActivity에 붙어야 할 텐데요, 오류 메시지(Window was already created.)에서 알 수 있듯이 이미 한번 생성한 XAML UI를 중복 생성하는 바람에 예외 처리가 된 것입니다.

그래도 저 코드가 .NET 7 이하에서는 잘 동작했다고 하는 것을 보면 그전에는 (동작은 최초 생성한 MainActivity의 상태는 아니었겠지만) 중복 생성을 허용했었던 것을 .NET 8부터는 막은 것이 아닌가 생각됩니다. 물론 저만의 추측인데요, 그렇긴 한데 마이크로소프트는 저 이슈를 별다른 패치 없이 (아래에서 설명할 LaunchMode를 이용하라면서) 그냥 닫아버렸습니다.

그런 면에서 본다면, 엄밀히 MainActivity는 다중 인스턴스로 생성될 의미가 없습니다. 즉, 언제나/항상/무조건 LaunchMode 옵션을 SingleTask로 설정하는 것이 맞습니다.

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true,
    ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density
    , LaunchMode = LaunchMode.SingleTask // 참고: "그림으로 이해하는 Activity LaunchMode 실험"
    )]
public class MainActivity : MauiAppCompatActivity
{
    // ...[생략]...
}

저렇게 바꾸고 나면, 이제야 (OnPostCreate에서의 try/catch를 제거해도) 이 글의 예제에서 작성한 코드들이 아무런 문제 없이 동작하게 됩니다.




참고로, (SingleTask를 사용하지 않고) OnPostCreate에서의 try/catch를 사용하는 우회 방법이 통하지 않는 경우가 있습니다. 바로 MediaElement를 추가했을 때인데요,

<!-- 참고: C# - MAUI에서 MediaElement 사용 -->

<toolkit:MediaElement x:Name="mediaPlayer" />

재미있는 건 저렇게만 사용했을 때는 문제없지만 Event Handler를 걸게 되면,

<toolkit:MediaElement x:Name="mediaPlayer" Loaded="OnMediaLoaded" />

이제부터는 다른 Activity에서 MainActivity로 돌아올 때 이런 예외가 발생합니다.

System.ObjectDisposedException: 'Cannot access a disposed object.
Object name: 'CommunityToolkit.Maui.Core.Views.MauiMediaElement'.'

0xFFFFFFFFFFFFFFFF in Android.Runtime.RuntimeNativeMethods.monodroid_debugger_unhandled_exception   C#
0x1A in Android.Runtime.JNINativeWrapper._unhandled_exception at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:13,5 C#
0x23 in Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPLLL_L at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:368,26    C#
0x2E in Java.Interop.JniPeerMembers.AssertSelf at /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs:153,5    C#
0x1 in Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeVirtualVoidMethod at /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods_Invoke.cs:57,5    C#
...[생략]...
0x5A in Microsoft.Maui.Platform.MauiContextExtensions.ToPlatform at D:\a\_work\1\s\src\Core\src\Platform\Android\MauiContextExtensions.cs:96,4  C#
0x2A in Microsoft.Maui.Controls.Platform.Compatibility.ShellFragmentContainer.OnCreateView at D:\a\_work\1\s\src\Controls\src\Core\Compatibility\Handlers\Shell\Android\ShellFragmentContainer.cs:26,4  C#
0x24 in AndroidX.Fragment.App.Fragment.n_OnCreateView_Landroid_view_LayoutInflater_Landroid_view_ViewGroup_Landroid_os_Bundle_ at C:\a\_work\1\s\generated\androidx.fragment.fragment\obj\Release\net6.0-android\generated\src\AndroidX.Fragment.App.Fragment.cs:2031,4 C#
0xD in Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPLLL_L at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:367,5  C#

이번에는 OnPostCreate 단계에서 발생하지 않기 때문에 응용 프로그램은 예외로 인해 종료하게 됩니다. 아마도 이런 문제 때문인지는 알 수 없으나, MediaElement는 애당초 문서에서 LaunchMode를 SingleTask로 지정하라고 명시하고 있습니다.

Platform specific initialization
; https://learn.microsoft.com/en-us/dotnet/communitytoolkit/maui/views/mediaelement?tabs=android#platform-specific-initialization

To initialize the MediaElement on Android, the LaunchMode of the applications Activity must be set to LaunchMode.SingleTask and you must add ResizableActivity=true as per the following example


언젠가는 마이크로소프트가 다중 XAML 초기화를 Activity마다 지원하게 될 수도 있을 것입니다. 적어도 그때까지는, 이런저런 이유로 인해 MainActivity의 LaunchMode는 항상 SingleTask로 지정하는 것이 여러모로 정신 건강에 이로울 듯합니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 6/6/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2024-11-21 09시19분
Android쪽에서 activity 접근 할때 아래꺼 적어 놓고 참고 하고 있습니다. (Maui)
            //var context = Platform.AppContext;
            //Current Activity or null if not initialized or not started.
            //var activity = Platform.CurrentActivity;
            //var activity = await Platform.WaitForActivityAsync();
지현명

... 16  [17]  18  19  20  21  22  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13516정성태1/7/20249684닷넷: 2196. IIS - AppPool의 "Disable Overlapped Recycle" 옵션의 부작용
13515정성태1/6/20249631닷넷: 2195. async 메서드 내에서 C# 7의 discard 구문 활용 사례 [1]
13514정성태1/5/20249525개발 환경 구성: 702. IIS - AppPool의 "Disable Overlapped Recycle" 옵션
13513정성태1/5/20249623닷넷: 2194. C# - WebActivatorEx / System.Web의 PreApplicationStartMethod 특성
13512정성태1/4/20249873개발 환경 구성: 701. IIS - w3wp.exe 프로세스의 ASP.NET 런타임을 항상 Warmup 모드로 유지하는 preload Enabled 설정
13511정성태1/4/20249618닷넷: 2193. C# - ASP.NET Web Application + OpenAPI(Swashbuckle) 스펙 제공
13510정성태1/3/20249363닷넷: 2192. C# - 특정 실행 파일이 있는지 확인하는 방법 (Linux)
13509정성태1/3/202410548오류 유형: 887. .NET Core 2 이하의 프로젝트에서 System.Runtime.CompilerServices.Unsafe doesn't support netcoreapp2.0.
13508정성태1/3/202410038오류 유형: 886. ORA-28000: the account is locked
13507정성태1/2/202411122닷넷: 2191. C# - IPGlobalProperties를 이용해 netstat처럼 사용 중인 Socket 목록 구하는 방법파일 다운로드1
13506정성태12/29/202310269닷넷: 2190. C# - 닷넷 코어/5+에서 달라지는 System.Text.Encoding 지원
13505정성태12/27/202312103닷넷: 2189. C# - WebSocket 클라이언트를 닷넷으로 구현하는 예제 (System.Net.WebSockets)파일 다운로드1
13504정성태12/27/202311215닷넷: 2188. C# - ASP.NET Core SignalR로 구현하는 채팅 서비스 예제파일 다운로드1
13503정성태12/27/202310391Linux: 67. WSL 환경 + mlocate(locate) 도구의 /mnt 디렉터리 검색 문제
13502정성태12/26/202310931닷넷: 2187. C# - 다른 프로세스의 환경변수 읽는 예제파일 다운로드1
13501정성태12/25/202310270개발 환경 구성: 700. WSL + uwsgi - IPv6로 바인딩하는 방법
13500정성태12/24/202310785디버깅 기술: 194. Windbg - x64 가상 주소를 물리 주소로 변환
13498정성태12/23/202312455닷넷: 2186. 한국투자증권 KIS Developers OpenAPI의 C# 래퍼 버전 - eFriendOpenAPI NuGet 패키지
13497정성태12/22/202310626오류 유형: 885. Visual Studiio - error : Could not connect to the remote system. Please verify your connection settings, and that your machine is on the network and reachable.
13496정성태12/21/202310558Linux: 66. 리눅스 - 실행 중인 프로세스 내부의 환경변수 설정을 구하는 방법 (gdb)
13495정성태12/20/202310791Linux: 65. clang++로 공유 라이브러리의 -static 옵션 빌드가 가능할까요?
13494정성태12/20/202310896Linux: 64. Linux 응용 프로그램의 (C++) so 의존성 줄이기(ReleaseMinDependency) - 두 번째 이야기
13493정성태12/19/202310988닷넷: 2185. C# - object를 QueryString으로 직렬화하는 방법
13492정성태12/19/202310375개발 환경 구성: 699. WSL에 nopCommerce 예제 구성
13491정성태12/19/20239380Linux: 63. 리눅스 - 다중 그룹 또는 사용자를 리소스에 권한 부여
13490정성태12/19/202310105개발 환경 구성: 698. Golang - GLIBC 의존을 없애는 정적 빌드 방법
... 16  [17]  18  19  20  21  22  23  24  25  26  27  28  29  30  ...